Flutter 应用开发:布局实例

一、Flutter 布局实例

通过对 Flutter Widget的学习,掌握了基本的 Widget 的使用,接下来通过使用学习过的 Widget,实现一个小实例,巩固知识。

在平时的使用过程中,我发现用到最多的控件有:Text,Image,Contianer,Column,Row,ListView,Expand ,Stack 等。通过这些基础的 Widget 或者这些 Widget 的组合基本能实现我们的需求。

下面通过一个小实例,演示如果通过一些基础控件来实现一个完整的 Flutter 应用程序,最主要的是要熟悉控件的使用。

效果:


widget layout.gif

主要是实现了两个 tab 页,主页面点击 item 可以跳转到详情页。简单的逻辑是这样,主要看如何布局实现这样的效果。

二、主程序搭建

这里我新建了四个文件,一个是主程序架构页面,一个是主页,一个是我的,一个是详情。如下:


Flutter 应用开发:布局实例_第1张图片
image1.png

首先说一下项目的主体以及底部导航,这个示例里面用到了两个 tab,也就是两个页面,点击 tab 会切换界面,当然中间的图标,也就是 fab,也能相应点击事件,这里没有加。

在 Flutter 里,Scaffold 这个 Widget 代表脚手架,这里 Flutter 已经帮我们封装好了一些基本的控件,方便我们快速开发, Scaffold 里面包含了 :

  • appBar:导航栏
  • drawer:抽屉菜单
  • bottomNavigationBar:底部导航
  • fab:floatingActionButton

通过使用 Scaffold 我们就可以快速开发一个页面。
首先看一下主页的 Scaffold:

    return Scaffold(
        backgroundColor: Colors.white,
        body: list[_index],
        floatingActionButton:
        Container(
          width: 50,
          height: 50,
          child:
          FloatingActionButton(
            heroTag: "main_fab",
            isExtended:true,
            onPressed: () {
                  print("to do sth");
              },
            //图标颜色
            elevation: 8,
            highlightElevation: 5,
            child: Icon(Icons.camera),
          ),

        ),

        floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked,
        bottomNavigationBar:
        Container(
          child:BottomAppBar(
            //这个 shape 是 底部导航的压缩效果
            shape: CircularNotchedRectangle(),
            //child: tabs(),
            //阴影效果
            elevation: 20,
            //圆弧弧度
            notchMargin: 15,
            child:tabs(),
          ),
          height: 70,
        )
    );

简单说一下,这里面:

body

也就是主页面,list 集合中包含我们事先定义好的页面,点击按钮,修改 _index 的值,也就相当于切换了页面。

floatingActionButton

这里 fab 图标使用了 icon,当然也能使用其他的 widget,如 image ,或者 svg 等。

floatingActionButtonLocation

floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked 这个表示 Fab 的样式,centerDocked 就是表示居中陷入的样式,还有其他的样式,如右下角显示等,这里可以自行尝试。

bottomNavigationBar

bottomNavigationBar 也就是底部导航,这里我指定了一个函数 tabs(),返回底部 widget :

    Row tabs() {
      return Row(
        mainAxisSize: MainAxisSize.max,
        mainAxisAlignment: MainAxisAlignment.spaceAround,
        children: [

          new Container(
            child: IconButton(
              icon: Icon(Icons.home),
              color:_index == 0? Colors.blue:Colors.orangeAccent,
              onPressed: () {

                setState(() {
                  _index = 0;
                });

              },
            ),
          ),

          IconButton(
            icon: Icon(Icons.person),
            color:_index == 1? Colors.blue:Colors.orangeAccent,
            onPressed: () {
              setState(() {
                _index = 1;
              });
            },
          ),

        ],
      );
    }

这里的 tabs 函数直接返回了行布局,也就是我们看到的底部的两个 tab 按钮,这里的 按钮用 Icon 来实现,当然也能用其他的 widget 来实现,如在用一个列布局,上面是图标,下面是文字等。

每个 tab 的 onPressed 中监听点击事件,当点击是修改 -index 的值,通过 setState 来刷新,这样就达到了切换页面的效果,同事通过对 _index 的判断修改点击时的颜色。

最后,完整的代码:

import 'dart:io';

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_widget/home_page.dart';
import 'package:flutter_widget/my_page.dart';

void main() {

  if (Platform.isAndroid) {

    /**
     * 设置状态栏颜色
     */
    SystemUiOverlayStyle systemUiOverlayStyle = SystemUiOverlayStyle(statusBarColor: Colors.transparent);
    SystemChrome.setSystemUIOverlayStyle(systemUiOverlayStyle);

    /**
     * 强制竖屏
     */
    SystemChrome.setPreferredOrientations([
      DeviceOrientation.portraitUp,
      DeviceOrientation.portraitDown
    ]);

  }

  runApp(MyApp());

}



class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Widget',
      theme: ThemeData(
        primarySwatch: Colors.blue,
        primaryColor: Colors.blue,
      ),
      home: MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({Key key, this.title}) : super(key: key);

  final String title;

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State {


  //主页 tab 索引
  int _index = 0;
  Color _tabColor = Colors.blue;

  @override
  void initState() {
    super.initState();
    list..add(new HomePage())..add(new MyPage());
  }

  List list = new List();

  @override
  Widget build(BuildContext context) {


    Row tabs() {
      return Row(
        mainAxisSize: MainAxisSize.max,
        mainAxisAlignment: MainAxisAlignment.spaceAround,
        children: [

          new Container(
            child: IconButton(
              icon: Icon(Icons.home),
              color:_index == 0? Colors.blue:Colors.orangeAccent,
              onPressed: () {

                setState(() {
                  _index = 0;
                });

              },
            ),
          ),

          IconButton(
            icon: Icon(Icons.person),
            color:_index == 1? Colors.blue:Colors.orangeAccent,
            onPressed: () {
              setState(() {
                _index = 1;
              });
            },
          ),

        ],
      );
    }



    return Scaffold(
        backgroundColor: Colors.white,
        body: list[_index],
        floatingActionButton:
        Container(
          width: 50,
          height: 50,
          child:
          FloatingActionButton(
            heroTag: "main_fab",
            isExtended:true,
            onPressed: () {
                  print("to do sth");
              },
            //图标颜色
            elevation: 8,
            highlightElevation: 5,
            child: Icon(Icons.camera),
          ),

        ),

        floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked,
        bottomNavigationBar:
        Container(
          child:BottomAppBar(
            //这个 shape 是 底部导航的压缩效果
            shape: CircularNotchedRectangle(),
            //child: tabs(),
            //阴影效果
            elevation: 20,
            //圆弧弧度
            notchMargin: 15,
            child:tabs(),
          ),
          height: 70,
        )
    );
  }
}

三、界面布局

主页布局

主页的的整体结构:

class HomePageState extends State {

  List subjects = [];
  @override
  void initState() {
    loadData();
  }

  @override
  Widget build(BuildContext context) {

    return Scaffold(
      appBar: AppBar(
        title: Text("当前热映电影"),
      ),
      body: Center(
        child: getBody(),
      ),
    );

 }
}

initState() 去加载数据,这里是通过 dio 去请求数据的。

body 中根据数据构造布局。

loadData
  loadData() async {
    String loadRUL = "https://douban.uieee.com/v2/movie/in_theaters";
    try {
      Response response = await Dio().get(loadRUL);
      print(response);
      var result = json.decode(response.toString());
      setState(() {
        subjects = result['subjects'];
      });

    } catch (e) {
      print(e);
    }
  }

loadData() 会通过 Dio 请求数据,返回结果保存到 subjects 集合中。

getBody:
  getBody() {
    if (subjects.length != 0) {
      return
        ListView.builder(
            itemCount: subjects.length,
            itemBuilder: (BuildContext context, int position) {
              return getItem(subjects[position]);
            });

    } else {
      ///这个是 ios 风格的加载菊花。
      return CupertinoActivityIndicator();
    }
  }

这里面返回了 listView,通过 getItem 返回每一项布局

getItem
  getItem(var subject) {
//    演员列表
    var avatars = List.generate(subject['casts'].length, (int index) =>
        Container(
          margin: EdgeInsets.only(left: index.toDouble() == 0.0 ? 0.0 : 16.0),
          child: CircleAvatar(
              backgroundColor: Colors.white10,
              backgroundImage: NetworkImage(
                  subject['casts'][index]['avatars']['small']
              )
          ),
        ),
    );

    var row = Container(
      margin: EdgeInsets.all(4.0),
      child:
      Row(
        children: [
          ClipRRect(
            borderRadius: BorderRadius.circular(4.0),
            child: Image.network(
              subject['images']['large'],
              width: 100.0, height: 150.0,
              fit: BoxFit.fill,
            ),
          ),

          Expanded(
              child:
              Stack(
                children: [
                  Container(
                  margin: EdgeInsets.only(left: 8.0),
                  height: 150.0,
                  alignment: Alignment.centerLeft,
                  child: Column(
                    crossAxisAlignment: CrossAxisAlignment.start,
                    mainAxisAlignment: MainAxisAlignment.center,

                    children: [
                      Text(
                        subject['title'],
                        style: TextStyle(
                          fontWeight: FontWeight.bold,
                          fontSize: 18.0,
                        ),
                        maxLines: 1,
                      ),

                      Text(
                          "类型:${subject['genres'].join("、")}"
                      ),

                      Text(
                          '导演:${subject['directors'][0]['name']}'
                      ),

                      Container(
                        margin: EdgeInsets.only(top: 8.0),
                        child: Row(
                          children: [
                            Text('主演:'),
                            Row(
                              children: avatars,
                            )
                          ],
                        ),
                      )
                    ],
                  ),
                ),

                  Positioned(
                    top: 15,
                    right: 2,
                    child: new     Text(
                      '${subject['rating']['average']} 分',
                      style: TextStyle(
                          fontFamily: 'GloriaHallelujah',
                          color: Colors.redAccent,
                          fontSize: 16.0
                      ),
                    ),

                  )
                ],
              ),
          )

        ],
      ),
    );


   return InkWell(
      child:  Card(
        child: row,
      ),
      onTap: (){

        Navigator.push(context,
            PageRouteBuilder(
                transitionDuration: Duration(microseconds: 100),
                pageBuilder: (BuildContext context, Animation animation,
                    Animation secondaryAnimation) {
                  return new FadeTransition(
                    opacity: animation,
                    child: DetailPage(id:  subject['id'].toString(),),
                  );
                })
        );

      },
    );

  }

getItem 里是每一项的布局,根布局是一个 InkWell,这个 widget 有水波纹效果,同时能够响应点击事件,跳转到详情页面。跳转路由里面重写了 PageRouteBuilder,可以自定义过度动画。在跳转到 DetailPage 的时候,传递了一个参数 id 过去。

InkWell 中指定子 widget 为 row,也就是行布局,可以看到每个 item 是一个行布局,左边的封面,右边的三行文字,同时还有一个分数,这里的分数通过 Positioned 来定位,当然就需要 Stack 了。

完整代码:

import 'dart:convert';

import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_widget/detail_page.dart';
import 'package:dio/dio.dart';


class HomePage extends StatefulWidget {

  @override
  State createState() => HomePageState();
}


class HomePageState extends State {

  List subjects = [];
  @override
  void initState() {
    loadData();
  }

  @override
  Widget build(BuildContext context) {

    return Scaffold(
      appBar: AppBar(
        title: Text("当前热映电影"),
      ),
      body: Center(
        child: getBody(),
      ),
    );
  }

  loadData() async {
    String loadRUL = "https://douban.uieee.com/v2/movie/in_theaters";
    try {
      Response response = await Dio().get(loadRUL);
      print(response);
      var result = json.decode(response.toString());
      setState(() {
        subjects = result['subjects'];
      });

    } catch (e) {
      print(e);
    }
  }


  getItem(var subject) {
//    演员列表
    var avatars = List.generate(subject['casts'].length, (int index) =>
        Container(
          margin: EdgeInsets.only(left: index.toDouble() == 0.0 ? 0.0 : 16.0),
          child: CircleAvatar(
              backgroundColor: Colors.white10,
              backgroundImage: NetworkImage(
                  subject['casts'][index]['avatars']['small']
              )
          ),
        ),
    );

    var row = Container(
      margin: EdgeInsets.all(4.0),
      child:
      Row(
        children: [
          ClipRRect(
            borderRadius: BorderRadius.circular(4.0),
            child: Image.network(
              subject['images']['large'],
              width: 100.0, height: 150.0,
              fit: BoxFit.fill,
            ),
          ),

          Expanded(
              child:
              Stack(
                children: [
                  Container(
                  margin: EdgeInsets.only(left: 8.0),
                  height: 150.0,
                  alignment: Alignment.centerLeft,
                  child: Column(
                    crossAxisAlignment: CrossAxisAlignment.start,
                    mainAxisAlignment: MainAxisAlignment.center,

                    children: [
                      Text(
                        subject['title'],
                        style: TextStyle(
                          fontWeight: FontWeight.bold,
                          fontSize: 18.0,
                        ),
                        maxLines: 1,
                      ),

                      Text(
                          "类型:${subject['genres'].join("、")}"
                      ),

                      Text(
                          '导演:${subject['directors'][0]['name']}'
                      ),

                      Container(
                        margin: EdgeInsets.only(top: 8.0),
                        child: Row(
                          children: [
                            Text('主演:'),
                            Row(
                              children: avatars,
                            )
                          ],
                        ),
                      )
                    ],
                  ),
                ),

                  Positioned(
                    top: 15,
                    right: 2,
                    child: new     Text(
                      '${subject['rating']['average']} 分',
                      style: TextStyle(
                          fontFamily: 'GloriaHallelujah',
                          color: Colors.redAccent,
                          fontSize: 16.0
                      ),
                    ),

                  )
                ],
              ),
          )

        ],
      ),
    );


   return InkWell(
      child:  Card(
        child: row,
      ),
      onTap: (){

        Navigator.push(context,
            PageRouteBuilder(
                transitionDuration: Duration(microseconds: 100),
                pageBuilder: (BuildContext context, Animation animation,
                    Animation secondaryAnimation) {
                  return new FadeTransition(
                    opacity: animation,
                    child: DetailPage(id:  subject['id'].toString(),),
                  );
                })
        );

      },
    );

  }

  getBody() {
    if (subjects.length != 0) {
      return
        ListView.builder(
            itemCount: subjects.length,
            itemBuilder: (BuildContext context, int position) {
              return getItem(subjects[position]);
            });

    } else {
      ///这个是 ios 风格的加载菊花。
      return CupertinoActivityIndicator();
    }
  }
}
详情页

详情页比较简单,代码如下:

import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:dio/dio.dart';
class DetailPage extends StatefulWidget {

  String id;
  DetailPage({this.id});
  @override
  State createState() => DetailPageState();
}

class DetailPageState extends State {

  ///movie id
  String movieId = "";
  ///封面图片 url
  String imageUrl = "";
  ///豆瓣评分
  String score = "";
  ///简介
  String summary = "";
  ///电影名称
  String alt_titile = "";

  @override
  void initState() {
    movieId = widget.id;
    initMovieData();
  }

  initMovieData() async {
    ///电影详情地址
    String  movieDetail = "https://douban.uieee.com/v2/movie/$movieId";

    try {
      Response response2 = await Dio().get(movieDetail);
      var result = json.decode(response2.toString());

      score = result['rating']['average'];
      alt_titile = result['alt_title'];
      imageUrl = result['image'];
      summary = result['summary'];

    setState(() {
    });

    } catch (e) {
      print(e);
    }
  }

  @override
  Widget build(BuildContext context) {

    return Scaffold(
      body: CustomScrollView(
        primary:false,
        slivers: [
          SliverAppBar(
            automaticallyImplyLeading:false,
            pinned: true,
            expandedHeight:150,
            flexibleSpace: FlexibleSpaceBar(
              titlePadding: EdgeInsets.only(left:40,top: 0,bottom: 30),
              background:
              Image.network(
                imageUrl, fit: BoxFit.cover,)
            ),
          ),

          new SliverFixedExtentList(
            itemExtent: 50,
            delegate: new SliverChildBuilderDelegate(
                    (BuildContext context, int index) {
                  //创建列表项
                  return new Container(
                    color: Colors.white,
                    alignment: Alignment.centerLeft,
                    padding: EdgeInsets.only(left: 25),
                    child:  Text("$alt_titile",),
                  );
                },
                childCount: 1 //50个列表项
            ),
          ),

          new SliverFixedExtentList(
            itemExtent:10,
            delegate: new SliverChildBuilderDelegate((BuildContext context, int index) {
              return new Container(
                alignment: Alignment.center,
              );
            },
                childCount: 1
            ),
          ),

          new SliverFixedExtentList(
            itemExtent:50,
            delegate: new SliverChildBuilderDelegate(
                    (BuildContext context, int index) {

                      return new Container(
                        color: Colors.white,
                        alignment: Alignment.centerLeft,
                        padding: EdgeInsets.only(left: 25),
                        child:  Text("豆瓣评分:$score",),
                      );
                },
                childCount: 1 //50个列表项
            ),
          ),

          new SliverFixedExtentList(
            itemExtent:10,
            delegate: new SliverChildBuilderDelegate((BuildContext context, int index) {
              return new Container(
                alignment: Alignment.center,
              );
            },
                childCount: 1
            ),
          ),

          new SliverFixedExtentList(
            itemExtent:250,

            delegate: new SliverChildBuilderDelegate(
                    (BuildContext context, int index) {
                  return new Container(
                    padding: EdgeInsets.symmetric(horizontal: 25),
                    color: Colors.white,
                    child: new ListView(
                      children: [
                        Text("简介:$summary"),
                      ],
                    ),
                  );
                },
                childCount: 1 //50个列表项
            ),
          ),

        ],
      ),
    );

  }

}

详情界面里面,用到了头部SliverAppBar。
SliverAppBar对应AppBar,两者不同之处在于SliverAppBar可以集成到CustomScrollView。SliverAppBar可以结合FlexibleSpaceBar实现Material Design中头部伸缩的模型。为了使用 SliverAppBar,因此 body 根布局指定为 CustomScrollView。
我为了显示一个分割的效果,这个分割条我也用 SliverFixedExtentList 来实现。

我的界面

我的界面同详情页差不多。

import 'package:flutter/material.dart';


class MyPage extends StatefulWidget {

  @override
  State createState() => MyPageState();
}

class MyPageState extends State {


  @override
  Widget build(BuildContext context) {

    return Scaffold(
      body: CustomScrollView(
        primary:false,
        slivers: [
          SliverAppBar(
            automaticallyImplyLeading:false,
            pinned: true,
            expandedHeight:150,
            flexibleSpace: FlexibleSpaceBar(


              title: Container(
                margin: EdgeInsets.only(top: 80),
                alignment: Alignment.center,
                child: Column(
                  mainAxisAlignment: MainAxisAlignment.center,
                  crossAxisAlignment: CrossAxisAlignment.center,
                  children: [
                    Container(
                      width: 40,
                      height: 40,
                      child:
                      ClipOval(
                        child:  Image.asset("images/avatar.jpg",fit: BoxFit.cover,))),
                    Container(
                      child:  new Text("个人中心", ),
                    ),
                  ],
                ),


              ),
              centerTitle: true,
              background: Image.asset(
                "images/bg.jpg", fit: BoxFit.cover,),
            ),


          ),

          new SliverFixedExtentList(
            itemExtent: 50,
            delegate: new SliverChildBuilderDelegate(
                    (BuildContext context, int index) {
                  //创建列表项
                  return new Container(
                    alignment: Alignment.centerLeft,
                    margin: EdgeInsets.only(left: 25),
                    child:  Text("简介:介绍一下自己吧~",),
                  );
                },
                childCount: 1 //50个列表项
            ),
          ),

          new SliverFixedExtentList(
            itemExtent:10,
            delegate: new SliverChildBuilderDelegate((BuildContext context, int index) {
                  return new Container(
                    alignment: Alignment.center,
                    child: new Text(''),
                  );
                },
                childCount: 1
            ),
          ),


          new SliverFixedExtentList(
            itemExtent:50,
            delegate: new SliverChildBuilderDelegate(
                    (BuildContext context, int index) {
                  //创建列表项
                  return new InkWell(
                      child:
                      new Container(
                        alignment: Alignment.centerLeft,
                        child:Row(
                          children: [


                            Expanded(
                              child: Row(
                                children: [

                                  Container(
                                    margin: EdgeInsets.only(left: 25),
                                    child: Icon(Icons.settings,color: Colors.blue,),
                                  ),


                                  Container(
                                    margin: EdgeInsets.only(left: 10),
                                    child:  Text("设置",style: TextStyle(color: Colors.blue),),
                                  ),
                                ],
                              ),
                              flex: 1,
                            ),

                            Expanded(
                              child: Container(
                                width: 20,
                                height:20,
                                alignment: Alignment.centerRight,
                                margin: EdgeInsets.only(right: 5),
                                child:Icon(Icons.arrow_forward_ios,color:Colors.blue),
                              ),
                              flex: 1,
                            ),


                          ],
                        ),
                      ),


                      );

                },
                childCount: 1 //50个列表项
            ),
          ),


          new SliverFixedExtentList(
            itemExtent: 4,
            delegate: new SliverChildBuilderDelegate(
                    (BuildContext context, int index) {
                  //创建列表项
                  return new Container(
                    alignment: Alignment.center,
                    child: new Text(''),
                  );
                },
                childCount: 1 //50个列表项
            ),
          ),

          ///分享
          new SliverFixedExtentList(
            itemExtent: 50,
            delegate: new SliverChildBuilderDelegate(
                    (BuildContext context, int index) {
                  //创建列表项

                  return new InkWell(

                      child: new Container(
                        alignment: Alignment.centerLeft,
                        child:Row(
                          children: [

                            Expanded(
                              child: Row(
                                children: [
                                  Container(
                                    margin: EdgeInsets.only(left: 25),
                                    child:  Icon(Icons.share,color: Colors.blue,),
                                  ),
                                  Container(
                                    margin: EdgeInsets.only(left: 10),
                                    child:  Text("分享",style: TextStyle(color: Colors.blue),),
                                  ),

                                ],
                              ),
                              flex: 1,
                            ),



                           Expanded(
                             child:  Container(
                               width: 20,
                               height:20,
                               alignment: Alignment.centerRight,
                               margin: EdgeInsets.only(right: 5),
                               child:Icon(Icons.arrow_forward_ios,color:Colors.blue),
                             ),
                             flex: 1,
                           ),

                          ],
                        ),
                      ),


                  );

                },
                childCount: 1 //50个列表项
            ),
          ),


          new SliverFixedExtentList(
            itemExtent: 4,
            delegate: new SliverChildBuilderDelegate(
                    (BuildContext context, int index) {
                  //创建列表项
                  return new Container(
                    alignment: Alignment.center,
                    child: new Text(''),
                  );
                },
                childCount: 1 //50个列表项
            ),
          ),

          ///关于
          new SliverFixedExtentList(
            itemExtent: 50,
            delegate: new SliverChildBuilderDelegate(
                    (BuildContext context, int index) {

                  return new InkWell(
                      child: new Container(
                        alignment: Alignment.centerLeft,
                        child:Row(
                          children: [

                            Expanded(
                              child: Row(
                                children: [
                                  Container(
                                    margin: EdgeInsets.only(left: 25),
                                    child: Icon(Icons.insert_drive_file,color: Colors.blue,),
                                  ),

                                  Container(
                                    margin: EdgeInsets.only(left: 10),
                                    child:  Text("关于",style: TextStyle(color: Colors.blue),),
                                  ),

                                ],
                              ),
                              flex: 1,
                            ),


                           Expanded(
                             child:  Container(
                               width: 20,
                               height: 20,
                               alignment: Alignment.centerRight,
                               margin: EdgeInsets.only(right: 5),
                               child:Icon(Icons.arrow_forward_ios,color:Colors.blue),
                             ),
                             flex: 1,
                           ),

                          ],
                        ),
                      ),

                  );

                },
                childCount: 1 //50个列表项
            ),
          ),
        ],
      ),
    );
  }

}

SliverAppBar 里面的头像和名字和用 title 来实现,圆形头像用 ClipOval。

四、总结

总体来说,使用 Flutter 实现界面布局还是很容易的,实现复杂布局也不是很难,主要就是 widget 的组合嵌套等,就是代码可能显着乱一些。

实例地址:github

你可能感兴趣的:(Flutter 应用开发:布局实例)