1. Flutter区别于其他方案的关键技术

  • Flutter 使用 Native 引擎渲染视图
  • React Native 之类的框架,只是通过 JavaScript 虚拟机扩展调用系统组件,由 Android 和 iOS 系统进行组件的渲染;Flutter 则是自己完成了组件渲染的闭环。
  • UI线程使用Dart来构建视图结构数据,这些数据会在GPU线程进行图层合成,随后交给Skia引擎加工成GPU数据,而这些数据会通过OpenGL最终提供给GPU渲染.
  • 使用Dart语言,同时支持JIT(动态编译,需要用的时候编译,开发的时候)和AOT(静态编译,先编译好,正式包的时候)
  • Flutter入门知识 - 图1

2. Widget的设计思路和基本原理

  • 核心设计思想: 一切皆Widget
  • Flutter入门知识 - 图2
  • Widget是不可变的,当视图渲染的配置信息发生变化时,Flutter会选择重建Widget树的方法进行数据更新.
  • 数据驱动UI构建
  • Widget本身不涉及实际渲染位图,它是轻量级的数据结构,重建成本低.
  • Element是Widget的一个实例化对象,它承载了视图构建的上下文数据,是连接结构化的配置信息到完成最终渲染的桥梁
  • Element同时持有Widget和RenderObject,最后负责渲染的是RenderObject
  • Flutter展示过程: 布局,绘制,合成,渲染

3. State的选择

  • Widget有StatelessWidget和StatefulWidget
  • StatefulWidget对应有交互,需要动态变化视觉效果的场景,StatelessWidget则用于处理静态的,无状态的视图展示
  • Flutter的视图开发是声明式的,其核心设计思想就将视图和数据分离
  • Widget生命周期内,State中的任何更改都将强制Widget重新构建
  • StatelessWidget,如Text,Container,Row,Column等.它们一旦创建成功就不再关心,也不相应任何数据变化进行重绘
  • 避免无谓的StatefulWidget使用,可以提高Flutter应用渲染性能.

4. 生命周期

4.1 Widget 视图生命周期

  • 生命周期其实是State的
  • Flutter入门知识 - 图3
  • State生命周期分为3个阶段
    1. 创建(插入视图树)
    2. 更新(在视图树中存在)
    3. 销毁(从视图树中移除)
  • 创建
    1. 构造方法
    2. initState
    3. didChangeDependencies
    4. build
  • 更新
    1. setState->build
    2. disUpdateWidget->build
    3. didChangeDependencies->build
  • 销毁
    1. deactivate
    2. dispose
  • 构造方法: 接收父Widget传递的初始化UI配置数据
  • initState: State对象被插入视图树的时候被调用,在这里做初始化工作
  • didChangeDependencies: 处理State对象依赖关系变化,initState()调用结束后会被调用
  • build: 构建视图,在这里根据父Widget传递过来的初始化配置数据,以及State状态,创建一个Widget返回
  • setState: 当状态数据发生变化时,调用这个方法,告诉Flutter,数据变了,根据更新后的数据重建UI
  • didChangeDependencies: State对象的依赖关系发生变化时(系统语言Locale或应用主题更改),系统会通知State调用此方法
  • didUpdateWidget: 当Widget的配置发生变化时,如父Widget触发重建,热重载时,会被调用.
  • deactivate: 组件的可见状态发生变化,State会被暂时从视图树中移除. 页面切换时,上一个页面的State对象在视图树种的位置发生了变化,会先调用deactivate,再调用build.
  • dispose: 当State被永久地从视图树中移除,比如关闭页面.到这里时,组件就要销毁了,这里做最终的资源释放,移除监听,清理环境.
  • Flutter入门知识 - 图4

  • | 方法名 | 功能 | 调用时机 | 调用次数 | | —- | —- | —- | —- | | 构造方法 | 接收父Widget传递的初始化UI配置数据 | 创建State时 | 1 | | initState | 与渲染相关的初始化工作 | 在State被插入视图树时 | 1 | | didChangeDependencies | 处理State对象依赖关系变化 | initState后及State对象依赖关系变化时 | >=1 | | build | 构建视图 | State准备好数据需要渲染时 | >=1 | | setState | 触发视图重建 | 需要刷新UI时 | >=1 | | didUpdateWidget | 处理Widget的配置变化 | 父Widget setState触发子Widget重建时 | >=1 | | deactivate | 组件被移除 | 组件不可视 | >=1 | | dispose | 组件被销毁 | 组件被永久移除 | 1 |

4.2 App(也是Widget) 生命周期

  • 利用WidgetsBindingObserver类

  1. abstract class WidgetsBindingObserver {
  2. //页面pop
  3. Future<bool> didPopRoute() => Future<bool>.value(false);
  4. //页面push
  5. Future<bool> didPushRoute(String route) => Future<bool>.value(false);
  6. //系统窗口相关改变回调,如旋转
  7. void didChangeMetrics() { }
  8. //文本缩放系数变化
  9. void didChangeTextScaleFactor() { }
  10. //系统亮度变化
  11. void didChangePlatformBrightness() { }
  12. //本地化语言变化
  13. void didChangeLocales(List<Locale> locale) { }
  14. //App生命周期变化
  15. void didChangeAppLifecycleState(AppLifecycleState state) { }
  16. //内存警告回调
  17. void didHaveMemoryPressure() { }
  18. //Accessibility相关特性回调
  19. void didChangeAccessibilityFeatures() {}
  20. }
  • 在didChangeAppLifecycleState回调函数中,AppLifecycleState参数是枚举类,它是Flutter对App生命周期状态的封装.
    • resumed 可见的,并能响应用户输入
    • inactive: 处在不活动状态,无法处理用户响应
    • paused: 不可见并不能响应用户的输入,但是在后台继续活动中
  • 在initState中注册监听器,在dispose中移除监听器
  1. class _MyHomePageState extends State<MyHomePage> with WidgetsBindingObserver{
  2. ...
  3. @override
  4. @mustCallSuper
  5. void initState() {
  6. super.initState();
  7. WidgetsBinding.instance.addObserver(this);//注册监听器
  8. }
  9. @override
  10. @mustCallSuper
  11. void dispose(){
  12. super.dispose();
  13. WidgetsBinding.instance.removeObserver(this);//移除监听器
  14. }
  15. @override
  16. void didChangeAppLifecycleState(AppLifecycleState state) async {
  17. print("$state");
  18. if (state == AppLifecycleState.resumed) {
  19. //do sth
  20. }
  21. }
  22. }
  • 后台(paused)切入前台: AppLifecycleState.inactive->AppLifecycleState.resumed
  • 前台(resumed)退回到后台: AppLifecycleState.inactive->AppLifecycleState.paused
  • WidgetsBingding提供了单次Frame绘制回调,以及实时Frame绘制回调两种机制.
  • 单次
  1. WidgetsBinding.instance.addPostFrameCallback((_){
  2. print("单次Frame绘制回调");//只回调一次
  3. });
  • 实时绘制
  1. WidgetsBinding.instance.addPersistentFrameCallback((_){
  2. print("实时Frame绘制回调");//每帧都回调
  3. });

5. 文本

  • Text,单一样式. 构造参数分为2类
    • 控制整体文本布局的参数: 对齐方式textAlign,文本排版方向textDirection,文本显示最大行数 maxLines、文本截断规则 overflow 等
    • 控制文本展示样式的参数: 统一封装到style参数中,字体名称fontFamily,字体大小fontSize,文本颜色color,文本阴影shadows等
  1. Text(
  2. '文本是视图系统中的常见控件,用来显示一段特定样式的字符串,就比如Android里的TextView,或是iOS中的UILabel。',
  3. textAlign: TextAlign.center,//居中显示
  4. style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20, color: Colors.red),//20号红色粗体展示
  5. );
  • TextSpan,可展示混合样式.(类似SpannableString)
  1. TextStyle blackStyle = TextStyle(fontWeight: FontWeight.normal, fontSize: 20, color: Colors.black); //黑色样式
  2. TextStyle redStyle = TextStyle(fontWeight: FontWeight.bold, fontSize: 20, color: Colors.red); //红色样式
  3. Text.rich(
  4. TextSpan(
  5. children: <TextSpan>[
  6. TextSpan(text:'文本是视图系统中常见的控件,它用来显示一段特定样式的字符串,类似', style: redStyle), //第1个片段,红色样式
  7. TextSpan(text:'Android', style: blackStyle), //第1个片段,黑色样式
  8. TextSpan(text:'中的', style:redStyle), //第1个片段,红色样式
  9. TextSpan(text:'TextView', style: blackStyle) //第1个片段,黑色样式
  10. ]),
  11. textAlign: TextAlign.center,
  12. );

6. 图片

  • Image
    • 加载本地资源图片,如 Image.asset(‘images/logo.png’);
    • 加载本地(File 文件)图片,如 Image.file(new File(’/storage/xxx/xxx/test.jpg’));
    • 加载网络图片,如 Image.network(‘http://xxx/xxx/test.gif‘)
  • 填充模式fit,拉伸 centerSlice,重复模式repeat
  • Image通过内部ImageProvider根据缓存状态,触发异步加载流程,通知_imageState(Image这种控件肯定不是静态的撒,得需要一个State)刷新UI.
  • FadeInImage,可以提供占位图,加载动画等.
  1. FadeInImage.assetNetwork(
  2. placeholder: 'assets/loading.gif', //gif占位
  3. image: 'https://xxx/xxx/xxx.jpg',
  4. fit: BoxFit.cover, //图片拉伸模式
  5. width: 200,
  6. height: 200,
  7. )
  • 图片默认缓存到内存,LRU(最近最少使用),如需缓存到本地则需要使用第三方的CachedNetworkImage(还提供了错误展示图片)控件

7. 按钮

  • FloatingActionButton 圆形按钮
  • RaisedButton,凸起的按钮,和Android默认的Button长得一样丑
  • FlatButton,扁平的按钮,默认透明背景,被点击后呈现灰色背景
  1. FloatingActionButton(onPressed: () => print('FloatingActionButton pressed'),child: Text('Btn'),);
  2. FlatButton(onPressed: () => print('FlatButton pressed'),child: Text('Btn'),);
  3. RaisedButton(onPressed: () => print('RaisedButton pressed'),child: Text('Btn'),);
  • onPressed参数用于设置回调,如果参数为空,则按钮会被禁用
  • child参数用于控制控件长什么样子
  • 其他丰富api
  1. FlatButton(
  2. color: Colors.yellow, //设置背景色为黄色
  3. shape:BeveledRectangleBorder(borderRadius: BorderRadius.circular(20.0)), //设置斜角矩形边框
  4. colorBrightness: Brightness.light, //确保文字按钮为深色
  5. onPressed: () => print('FlatButton pressed'),
  6. child: Row(children: <Widget>[Icon(Icons.add), Text("Add")],)
  7. );
  • Button都是由RawMaterialButton承载视觉,Image都是RawImage,Text是RichText。它们都继承自RenderObjectWidget,而RenderObjectWidget的父类就是Widget。

8. ListView

8.1 ListView

  • 同时支持垂直方向和水平方向滚动
  • 创建子视图方式 | 构造函数名 | 特点 | 适用场景 | 适用频次 | | —- | —- | —- | —- | | ListView | 一次性创建好全部子Widget | 适用于展示少量连续子Widget的场景 | 中 | | ListView.builder | 提供子Widget创建方法,仅在需要展示的时候才创建 | 适用于子Widget较多,且视觉效果呈现某种规律性的场景 | 高 | | ListView.separated | 与ListView.builder类似,并提供了自定义分割线的功能 | 与ListView.builder场景类似 | 中 |
  • 第一种 ListView 直接构建
  1. ListView(
  2. children: <Widget>[
  3. //设置ListTile组件的标题与图标
  4. ListTile(leading: Icon(Icons.map), title: Text('Map')),
  5. ListTile(leading: Icon(Icons.mail), title: Text('Mail')),
  6. ListTile(leading: Icon(Icons.message), title: Text('Message')),
  7. ]);
  • 第二种 ListView.builder.itemExtent 并不是一个必填参数。但,对于定高的列表项元素,我强烈建议你提前设置好这个参数的值。
  1. ListView.builder(
  2. //itemCount,表示列表项的数量,如果为空,则表示 ListView 为无限列表
  3. itemCount: 100, //元素个数
  4. itemExtent: 50.0, //列表项高度
  5. itemBuilder: (BuildContext context, int index) => ListTile(title: Text("title $index"), subtitle: Text("body $index"))
  6. );
  • 第三种 ListView.separated
  1. //使用ListView.separated设置分割线
  2. ListView.separated(
  3. itemCount: 100,
  4. separatorBuilder: (BuildContext context, int index) => index %2 ==0? Divider(color: Colors.green) : Divider(color: Colors.red),//index为偶数,创建绿色分割线;index为奇数,则创建红色分割线
  5. itemBuilder: (BuildContext context, int index) => ListTile(title: Text("title $index"), subtitle: Text("body $index"))//创建子Widget
  6. )

8.2 CustomScrollView

  • CustomScrollView是用来处理多个需要自定义滑动效果的Widget.在CustomScrollView中,这些彼此独立的,可滑动的Widget被统称为Sliver.
  • 比如ListView 的 Sliver 实现为 SliverList,AppBar 的 Sliver 实现为 SliverAppBar
  • 这些Sliver不再维护各自的滚动状态,交由CustomScrollView统一管理,最终实现滑动效果的一致性
  1. CustomScrollView(
  2. slivers: <Widget>[
  3. SliverAppBar(//SliverAppBar作为头图控件
  4. title: Text('CustomScrollView Demo'),//标题
  5. floating: true,//设置悬浮样式
  6. flexibleSpace: Image.network("https://xx.jpg",fit:BoxFit.cover),//设置悬浮头图背景
  7. expandedHeight: 300,//头图控件高度
  8. ),
  9. SliverList(//SliverList作为列表控件
  10. delegate: SliverChildBuilderDelegate(
  11. (context, index) => ListTile(title: Text('Item #$index')),//列表项创建方法
  12. childCount: 100,//列表元素个数
  13. ),
  14. ),
  15. ]);

8.3 ScrollController

  • ScrollController用于对ListView进行滚动信息的监听,以及相应的滚动控制.
  1. class MyControllerAppState extends State<MyControllerApp> {
  2. //ListView控制器
  3. ScrollController _controller;
  4. //标识目前是否需要启用top按钮
  5. bool isToTop = false;
  6. @override
  7. void initState() {
  8. _controller = ScrollController();
  9. _controller.addListener(() {
  10. //ListView向下滚动1000 则启用top按钮
  11. if (_controller.offset > 1000) {
  12. setState(() {
  13. isToTop = true;
  14. });
  15. } else if (_controller.offset < 300) {
  16. //向下滚动不足300,则禁用按钮
  17. setState(() {
  18. isToTop = false;
  19. });
  20. }
  21. });
  22. super.initState();
  23. }
  24. @override
  25. Widget build(BuildContext context) {
  26. return MaterialApp(
  27. home: Scaffold(
  28. body: ListView.builder(
  29. //将控制器传入
  30. controller: _controller,
  31. itemCount: 100,
  32. itemExtent: 100,
  33. itemBuilder: (context, index) =>
  34. ListTile(title: Text('index $index'))),
  35. floatingActionButton: RaisedButton(
  36. //如果isToTop是true则滑动到顶部,否则禁用按钮
  37. onPressed: isToTop
  38. ? () {
  39. //滑动到顶部
  40. _controller.animateTo(0.0,
  41. duration: Duration(microseconds: 200),
  42. curve: Curves.ease);
  43. }
  44. : null,
  45. child: Text('top'),
  46. ),
  47. ),
  48. );
  49. }
  50. @override
  51. void dispose() {
  52. _controller.dispose();
  53. super.dispose();
  54. }
  55. }

8.4 NotificationListener

  • NotificationListener是一个Widget,需要将ListView添加到NotificationListener中
  1. class MyListenerApp extends StatelessWidget {
  2. @override
  3. Widget build(BuildContext context) {
  4. return MaterialApp(
  5. home: Scaffold(
  6. body: NotificationListener<ScrollNotification>(
  7. //添加NotificationListener作为父容器
  8. //注册通知回调
  9. onNotification: (scrollNotification) {
  10. //开始滑动
  11. if (scrollNotification is ScrollStartNotification) {
  12. //scrollNotification.metrics.pixels 滑动的位置
  13. print('scroll start ${scrollNotification.metrics.pixels}');
  14. } else if (scrollNotification is ScrollUpdateNotification) {
  15. //滑动中
  16. print('scroll update');
  17. } else if (scrollNotification is ScrollEndNotification) {
  18. //滑动结束
  19. print('scroll end');
  20. }
  21. return null;
  22. },
  23. child: ListView.builder(
  24. itemCount: 100,
  25. itemExtent: 70,
  26. itemBuilder: (context, index) => ListTile(
  27. title: Text('index $index'),
  28. )),
  29. ),
  30. ),
  31. );
  32. }
  33. }

9. 布局容器

9.1 Container,Padding,Center

  • Container内部提供了间距,背景样式,圆角边框等基础属性,可以控制子Widget的摆放方式(居中,左,右)
  • Padding 设置间距,将Widget放里面
  • Center 设置居中,将Widget放里面
  1. getContainer() {
  2. return Container(
  3. child: Center(
  4. child: Text('Container(容器)在UI框架中是一个很常见的概念,Flutter也不例外。'),
  5. ),
  6. //内边距
  7. padding: EdgeInsets.all(18.0),
  8. //外边距
  9. margin: EdgeInsets.all(44.0),
  10. width: 180.0,
  11. height: 240,
  12. //子Widget居中对齐
  13. /* alignment: Alignment.center,*/
  14. //Container样式
  15. decoration: BoxDecoration(
  16. //背景色
  17. color: Colors.red,
  18. //圆角边框
  19. borderRadius: BorderRadius.circular(10.0),
  20. ),
  21. );
  22. }
  23. getPadding() {
  24. //只需要设置边距 可以使用Padding
  25. return Padding(
  26. padding: EdgeInsets.all(44.0),
  27. child: Text('我是Padding'),
  28. );
  29. }
  30. getCenter() {
  31. //直接居中
  32. return Center(
  33. child: Text('center text'),
  34. );
  35. }

9.2 Row,Column,Expanded

  • Row是水平布局
  • Column是垂直布局
  • Expanded表示将剩余的空间,如何分配
  • Row 与 Column 自身的大小由父widget的大小、子widget的大小、以及mainSize设置共同决定(mainAxisSize和crossAxisSize)
    • 主轴(纵轴)值为max:主轴(纵轴)大小等于屏幕主轴(纵轴)方向大小或者父widget主轴(纵轴)方向大小
    • 主轴(纵轴)值为min: 所有子widget组合在一起的主轴(纵轴)大小
  1. //Row的用法示范
  2. Row(
  3. children: <Widget>[
  4. Container(color: Colors.yellow, width: 60, height: 80,),
  5. Container(color: Colors.red, width: 100, height: 180,),
  6. Container(color: Colors.black, width: 60, height: 80,),
  7. Container(color: Colors.green, width: 60, height: 80,),
  8. ],
  9. );
  10. //Column的用法示范
  11. Column(
  12. children: <Widget>[
  13. Container(color: Colors.yellow, width: 60, height: 80,),
  14. Container(color: Colors.red, width: 100, height: 180,),
  15. Container(color: Colors.black, width: 60, height: 80,),
  16. Container(color: Colors.green, width: 60, height: 80,),
  17. ],
  18. );
  19. //第一个和最后一个平分
  20. Row(
  21. children: <Widget>[
  22. Expanded(flex: 1, child: Container(color: Colors.yellow, height: 60)), //设置了flex=1,因此宽度由Expanded来分配
  23. Container(color: Colors.red, width: 100, height: 180,),
  24. Container(color: Colors.black, width: 60, height: 80,),
  25. Expanded(flex: 1, child: Container(color: Colors.green,height: 60),)/设置了flex=1,因此宽度由Expanded来分配
  26. ],
  27. );

对齐方式

  • Flutter入门知识 - 图5
  • 根据主轴与纵轴,设置子Widget在这两个方向上的对齐规则mainAxisAlignment与crossAxisAlignment.比如主轴方向start表示靠左对齐,center表示横向居中对齐,end表示靠右对齐,spaceEvenly表示按固定间距对齐;而纵轴方向start则表示靠上对齐,center表示纵向居中对齐,end表示靠下对齐.看图
  • Flutter入门知识 - 图6
  • Flutter入门知识 - 图7

控制大小

  • 如果想让容器与子Widget在主轴上完全匹配,需要通过设置Row的mainAxisSize参数为MainAxisSize.min,由所有子Widget来决定主轴方向的容器长度,即主轴方向的长度尽可能小.类似wrap_content. mainAxisSize: MainAxisSize.min, //让容器宽度与所有子Widget的宽度一致

9.3 Stack,Positioned

  • Stack,类似FrameLayout.
  • Stack提供了层叠布局的容器,而Positioned则提供了设置子Widget位置的能力.
  1. Stack(
  2. children: <Widget>[
  3. Container(color: Colors.yellow, width: 300, height: 300),//黄色容器
  4. Positioned(
  5. left: 18.0,
  6. top: 18.0,
  7. child: Container(color: Colors.green, width: 50, height: 50),//叠加在黄色容器之上的绿色控件
  8. ),
  9. Positioned(
  10. left: 18.0,
  11. top:70.0,
  12. child: Text("Stack提供了层叠布局的容器"),//叠加在黄色容器之上的文本
  13. )
  14. ],
  15. )
  • Positioned只能在Stack中使用.

10. 自定义控件

10.1 组合控件

  • 将多个控件组合在一起

10.2 自定义控件

  • CustomPaint是用来承接自绘控件的容器,并不负责真正的绘制.
  • 画布是canvas,画笔是Paint.
  • 画成什么样子由CustomPainter来控制,将CustomPainter设置给容器CustomPaint的painter属性,我们就完成了一个自绘组件的封装
  • Paint,其实和Android中的差不多,可以配置它的各种属性,比如颜色、样式、粗细等;而画布 Canvas,则提供了各种常见的绘制方法,比如画线 drawLine、画矩形 drawRect、画点 DrawPoint、画路径 drawPath、画圆 drawCircle、画圆弧 drawArc 等。
  1. class WheelPainter extends CustomPainter {
  2. Paint getColoredPaint(Color color) {
  3. Paint paint = Paint();
  4. paint.color = color;
  5. return paint;
  6. }
  7. @override
  8. void paint(Canvas canvas, Size size) {
  9. //半径
  10. double wheelSize = min(size.width, size.height) / 2;
  11. //分成6份
  12. double nbElem = 6;
  13. //角度
  14. double radius = (2 * pi) / nbElem;
  15. //包裹饼图的矩形框 center:相对于原点的偏移量
  16. Rect boundingRect = Rect.fromCircle(
  17. center: Offset(wheelSize, wheelSize), radius: wheelSize);
  18. //每次画1/6圆
  19. canvas.drawArc(
  20. boundingRect, 0, radius, true, getColoredPaint(Colors.orange));
  21. canvas.drawArc(
  22. boundingRect, radius, radius, true, getColoredPaint(Colors.green));
  23. canvas.drawArc(
  24. boundingRect, radius * 2, radius, true, getColoredPaint(Colors.red));
  25. canvas.drawArc(
  26. boundingRect, radius * 3, radius, true, getColoredPaint(Colors.blue));
  27. canvas.drawArc(
  28. boundingRect, radius * 4, radius, true, getColoredPaint(Colors.pink));
  29. canvas.drawArc(boundingRect, radius * 5, radius, true,
  30. getColoredPaint(Colors.deepOrange));
  31. }
  32. @override
  33. bool shouldRepaint(CustomPainter oldDelegate) {
  34. //判断是否需要重绘,简单做下比较
  35. return oldDelegate != this;
  36. }
  37. }
  38. class Cake extends StatelessWidget {
  39. @override
  40. Widget build(BuildContext context) {
  41. //CustomPaint是用来承载自定义View的容器,需要自定义一个画笔,得继承自CustomPainter
  42. return CustomPaint(
  43. size: Size(200, 200),
  44. painter: WheelPainter(),
  45. );
  46. }
  47. }

11. 主题定制

  • 视觉效果是易变的,我们将这些变化的部分抽离出来,把提供不同视觉效果的资源和配置按照主题进行归类,整合到一个统一的中间层去管理,这样我们就能实现主题的管理和切换.
  • Flutter中由ThemeData来统一管理主题的配置信息
  • ThemeData中涵盖了Material Design规范的可自定义部分样式,比如应用明暗模式 brightness、应用主色调 primaryColor、应用次级色调 accentColor、文本字体 fontFamily、输入框光标颜色 cursorColor 等。
  • 全局统一的视觉风格:
  1. MaterialApp(
  2. title: 'Flutter Demo',//标题
  3. theme: ThemeData(//设置主题
  4. brightness: Brightness.dark,//设置明暗模式为暗色
  5. accentColor: Colors.black,//(按钮)Widget前景色为黑色
  6. primaryColor: Colors.cyan,//主色调为青色
  7. iconTheme:IconThemeData(color: Colors.yellow),//设置icon主题色为黄色
  8. textTheme: TextTheme(body1: TextStyle(color: Colors.red))//设置文本颜色为红色
  9. ),
  10. home: MyHomePage(title: 'Flutter Demo Home Page'),
  11. );
  • 局部主题: 需要使用Theme来对App的主题进行局部覆盖,Theme是一个单子Widget容器,将控件放里面就可以控制主题了.

    • 局部新建主题: 如果不想继承任何App全局的颜色或字体样式,可以直接新建一个ThemeData实例,依次设置对应的样式.

      1. // 新建主题
      2. Theme(
      3. data: ThemeData(iconTheme: IconThemeData(color: Colors.red)),
      4. child: Icon(Icons.favorite)
      5. );
    • 继承主题: 如果不想在局部重写所有的样式,则可以继承App的主题,使用copyWith方法,只更新部分样式

      1. // 继承主题
      2. Theme(
      3. data: Theme.of(context).copyWith(iconTheme: IconThemeData(color: Colors.green)),
      4. child: Icon(Icons.feedback)
      5. );
  • 主题另一个用途是样式复用.

  1. Container(
  2. color: Theme.of(context).primaryColor,//容器背景色复用应用主题色
  3. child: Text(
  4. 'Text with a background color',
  5. style: Theme.of(context).textTheme.title,//Text组件文本样式复用应用文本样式
  6. ));

12. 依赖管理

  • 可以把资源房任意目录,只需要使用根目录下的pubspec.yaml文件,对这些资源的所在位置进行显示声明就行.

12.1 图片

  1. flutter:
  2. assets:
  3. - assets/background.jpg #挨个指定资源路径
  4. - assets/loading.gif #挨个指定资源路径
  5. - assets/result.json #挨个指定资源路径
  6. - assets/icons/ #子目录批量指定
  7. - assets/ #根目录也是可以批量指定的
  • Flutter遵循了基于像素密度的管理方式,如1.0x,2.0x,3.0x.Flutter会根据当前设备分辨率加载最接近设备像素比例的图片资源
  • 想让Flutter适配不同的分辨率,只需要将其他分辨率的图片放到对应的分辨率子目录中.
  1. 目录如下
  2. assets
  3. ├── background.jpg //1.0x图
  4. ├── 2.0x
  5. └── background.jpg //2.0x图
  6. └── 3.0x
  7. pubspec.yaml文件声明:
  8. flutter:
  9. assets:
  10. - assets/background.jpg #1.0x图资源

12.2 字体

  1. fonts:
  2. - family: RobotoCondensed #字体名字
  3. fonts:
  4. - asset: assets/fonts/RobotoCondensed-Regular.ttf #普通字体
  5. - asset: assets/fonts/RobotoCondensed-Italic.ttf
  6. style: italic #斜体
  7. - asset: assets/fonts/RobotoCondensed-Bold.ttf
  8. weight: 700 #粗体

12.3 三方库 三方组件库

  • Dart提供包管理工具: Pub,管理代码和资源
  • 对于包,通常是指定版本区间,而很少直接指定特定版本.
  • 多人协作时,建议将Dart和Flutter的SDK环境写死,统一团队的开发环境.避免因为跨SDK版本出现的API差异而导致工程问题.
  1. dependencies:
  2. //1. #路径依赖
  3. package1:
  4. path: ../package1/
  5. //2. github
  6. date_format:
  7. git:
  8. url: https://github.com/xxx/package2.git #git依赖
  9. //3. pub上面的
  10. date_format: 1.0.6

13. 手势识别

  • 底层原始指针事件: 用户的触摸数据,如手指接触屏幕 PointerDownEvent、手指在屏幕上移动 PointerMoveEvent、手指抬起 PointerUpEvent,以及触摸取消 PointerCancelEvent.
  1. Listener(
  2. child: Container(
  3. color: Colors.red,//背景色红色
  4. width: 300,
  5. height: 300,
  6. ),
  7. onPointerDown: (event) => print("down $event"),//手势按下回调
  8. onPointerMove: (event) => print("move $event"),//手势移动回调
  9. onPointerUp: (event) => print("up $event"),//手势抬起回调
  10. );
  • 冒泡分发机制: 将触摸事件交给最内层的组件去响应,事件会从这个最内层的组件开始,沿着组件树向根节点向上冒泡分发.
  • 封装了底层指针事件手势语义的Gesture,平常一般使用GestureDetector.如点击 onTap、双击 onDoubleTap、长按 onLongPress、拖拽 onPanUpdate、缩放 onScaleUpdate 等。
  1. //红色container坐标
  2. double _top = 0.0;
  3. double _left = 0.0;
  4. Stack(//使用Stack组件去叠加视图,便于直接控制视图坐标
  5. children: <Widget>[
  6. Positioned(
  7. top: _top,
  8. left: _left,
  9. child: GestureDetector(//手势识别
  10. child: Container(color: Colors.red,width: 50,height: 50),//红色子视图
  11. onTap: ()=>print("Tap"),//点击回调
  12. onDoubleTap: ()=>print("Double Tap"),//双击回调
  13. onLongPress: ()=>print("Long Press"),//长按回调
  14. onPanUpdate: (e) {//拖动回调
  15. setState(() {
  16. //更新位置
  17. _left += e.delta.dx;
  18. _top += e.delta.dy;
  19. });
  20. },
  21. ),
  22. )
  23. ],
  24. );
  • 事件处理机制: Flutter会使用手势竞技场来进行各个手势的PK,以保证最后只有一个手势能够响应用户行为.
  • 手势冲突只是手势的语义化识别过程,对于底层指针事件是不会冲突的.
  • 父子都有点击事件的情况 因为子视图在父视图的上面,所以如果点击区域在子视图区域,子视图响应事件.

14. 跨组件共享数据

视图层级比较深的UI样式,直接通过属性传值会导致很多中间层增加冗余属性.

14.1 InheritedWidget

  • 共享父Widget的属性

14.2 Notification

  • 从下往上的数据传递,在父Widget中监听来自子Widget的事件

14.3 EventBus

  • EventBus 不依赖Widget树 这是事件总线,666
  • 遵循发布订阅 模式

14.4 对比

方式 数据流动方式 使用场景
属性传值 父到子 简单数据传递
InheritedWidget 父到子 跨层数据传递
Notification 子到父 状态通知
EventBus 发布订阅 消息批量同步

15. 路由管理

  • Route是页面的抽象,主要负责创建对应的界面,接收参数,响应Navigator打开和关闭
  • Navigator则会维护一个路由栈管理Route,Route打开即入栈,Route关闭即出栈.
  • 基本路由: 创建一个MaterialPageRoute实例,调用Navigator.push方法将新页面压到堆栈的顶部
  1. class FirstScreen extends StatelessWidget {
  2. @override
  3. Widget build(BuildContext context) {
  4. return RaisedButton(
  5. //打开页面
  6. onPressed: ()=> Navigator.push(context, MaterialPageRoute(builder: (context) => SecondScreen()));
  7. );
  8. }
  9. }
  10. class SecondPage extends StatelessWidget {
  11. @override
  12. Widget build(BuildContext context) {
  13. return RaisedButton(
  14. // 回退页面
  15. onPressed: ()=> Navigator.pop(context)
  16. );
  17. }
  18. }
  • 命名路由: 简化路由管理,命名路由.给页面起一个名字,然后通过名字打开
  1. MaterialApp(
  2. ...
  3. //注册路由
  4. routes:{
  5. "second_page":(context)=>SecondPage(),
  6. },
  7. );
  8. //使用名字打开页面
  9. Navigator.pushNamed(context,"second_page");
  • 错误路由处理,统一返回UnknownPage
  1. MaterialApp(
  2. ...
  3. //注册路由
  4. routes:{
  5. "second_page":(context)=>SecondPage(),
  6. },
  7. //错误路由处理,统一返回UnknownPage
  8. onUnknownRoute: (RouteSettings setting) => MaterialPageRoute(builder: (context) => UnknownPage()),
  9. );
  10. //使用错误名字打开页面
  11. Navigator.pushNamed(context,"unknown_page");
  • 页面参数: Flutter提供了路由参数的机制,可以在打开路由时传递相关参数,在目标页面通过RouteSettings来获取页面参数
  1. //打开页面时传递字符串参数
  2. Navigator.of(context).pushNamed("second_page", arguments: "Hey");
  3. class SecondPage extends StatelessWidget {
  4. @override
  5. Widget build(BuildContext context) {
  6. //取出路由参数
  7. String msg = ModalRoute.of(context).settings.arguments as String;
  8. return Text(msg);
  9. }
  10. }
  • 返回参数(类似startActivityForResult): 在push目标页面时,可以设置目标页面关闭时监听函数,以获取返回参数.而目标页面可以在关闭路由时传递相关参数.
  1. class SecondPage extends StatelessWidget {
  2. @override
  3. Widget build(BuildContext context) {
  4. return Scaffold(
  5. body: Column(
  6. children: <Widget>[
  7. Text('Message from first screen: $msg'),
  8. RaisedButton(
  9. child: Text('back'),
  10. //页面关闭时传递参数
  11. onPressed: ()=> Navigator.pop(context,"Hi")
  12. )
  13. ]
  14. ));
  15. }
  16. }
  17. class _FirstPageState extends State<FirstPage> {
  18. String _msg='';
  19. @override
  20. Widget build(BuildContext context) {
  21. return new Scaffold(
  22. body: Column(children: <Widget>[
  23. RaisedButton(
  24. child: Text('命名路由(参数&回调)'),
  25. //打开页面,并监听页面关闭时传递的参数
  26. onPressed: ()=> Navigator.pushNamed(context, "third_page",arguments: "Hey").then((msg)=>setState(()=>_msg=msg)),
  27. ),
  28. Text('Message from Second screen: $_msg'),
  29. ],),
  30. );
  31. }
  32. }
  • Navigator.push
    A->B->C->D,如何从 D页面 pop 到 B 呢? Navigator.popUntil(context,ModalRoute.withName('B'));