Flutter 初探

115 阅读14分钟

前言

最近学习了SwiftUIFlutter,说实话两个学下来感觉太像了,然后心累了。用swiftUI重构了公司的一个老项目,总体代码量比以前少了三分之二,而且包体积小具有原生的流畅性,但是需要ios14以上才能运行。后来用Flutter重构了公司最近的一个项目,安卓和ios共享一套UI还是嘛香的,但是遇到组件开发的话,还是需要分开来单独封装的,flutter项目打包的ipa包体积比较大,毕竟需要把flutter渲染引擎打包进去没法避免,流畅性的话有些地方感觉没有原生那么完美,也可能是我没优化好,总体上比那些基于webview的跨平台方案流畅太多,也比RN开发起来舒服多了。最后说一下,swiftUI和Flutter在界面绘制上都是层层嵌套,只是Flutter比swiftUI嵌套的更多一点,加上swift和最新的Flutter都是空安全的,导致有时候我切换代码写的时候会暂时性的混乱,因为编写界面的风格太像了.....好了 上一份学习笔记,我怕后面如果转行了会忘记flutter怎么玩了,哈哈!!!

Flutter生命周期

什么是生命周期?说白了就是回调方法(函数),让你知道我封装好的这个Widget它处于什么样的状态了! 有什么作用?监听Widget的事件、初始化数据、创建数据、发送网络请求、内存管理、销毁数据、销毁监听者、销毁Timer等等。Flutter中万物皆为Widget,即有状态:StatefluWidget,和无状态:StatelessWidget

StatelessWidget:无状态

  • build方法:无状态widget只有这一个生命周期方法

StatefluWidget:有状态,包含两个对象WidgetState

初始化阶段:

  • CreateState:State的构造方法,一旦执行这个回调,Mounted=true
  • initState:这个回调里一般会做一些数据初始化的工作

组件创建阶段:

  • didChangeDependencies:依赖的InheritedWidget发生变化之后或者组件依赖的全局 state 发生了变化时,也会调用 build,例如系统语言等、主题色等。
  • build:绘制组件,当调用setState方法,会重新调用build进行渲染
  • didupdateWidget:这个函数基本上是伴随着build调用而调用的

组件销毁阶段:

  • deactivate:State 被暂时从视图树中移除时会调用这个方法,相当于安装中的onpause回调
  • dispose:当Widget彻底销毁的时候

整个生命周期过程如下图所示: image.png

APP生命周期

如果我们开发APP的时候需要监听应用什么时候进入了后台,什么时候又恢复进入了前台怎么办?通过混入WidgetsBindingObserver,比如

class MyAPP extends StatefulWidget {
  const MyAPP({Key? key}) : super(key: key);

  @override
  State<MyAPP> createState() => _MyAPPState();
}

class _MyAPPState extends State<MyAPP> with WidgetsBindingObserver{
  @override
  void initState() {
    super.initState();
    WidgetsBinding.instance.addObserver(this);
  }
  @override
  void didChangeAppLifecycleState(AppLifecycleState state) {
    super.didChangeAppLifecycleState(state);
    print('didChangeAppLifecycleState');
    if (state == AppLifecycleState.resumed) {
      print('resumed:');
    } else if (state == AppLifecycleState.inactive) {
      print('inactive');
    } else if (state == AppLifecycleState.paused) {
      print('paused');
    } else if (state == AppLifecycleState.detached) {
      print('detached');
    }
  }
  @override
  Widget build(BuildContext context) {
    return Container();
  }
}

一定要是StatefulWidget,然后混入WidgetsBindingObserver,实现didChangeAppLifecycleState方法,并且在initState添加监听实例。 可以看到一共有四种状态

  • resumed:前台运行,应用程序可见,对用户输入作出反应
  • inactive:应用处于非活跃状态,没有接收用户输入,比如接听电话
  • paused:后台运行,应用程序不可见,对用户输入没有反应
  • detached:应用被进程中移除

三棵树

Flutter渲染的流程中,有三颗重要的树,即Widget树、Element树、Render树!并不是所有的Widget都会被独立渲染,只有继承RenderObjectWidget的才会创建RenderObject对象,Flutter引擎是针对Render树进行渲染,所以在屏幕上完成渲染的是Render树! 每一个Widget都会创建一个Element对象隐式调用createElement方法。

当Element加入Element树中它会创建三种Element - RenderElement:主要是创建RenderObject对象,继承RenderObjectWidget的Widget会创建RenderElement,创建RanderElement,Flutter会调用mount方法,调用createRanderObject方法

  • StatefulElement:继承ComponentElement,StatefulWidget会创建StatefulElement,调用createState方法,创建State,将Widget赋值给state, 调用state的build方法并且将自己(Element)传出去,build里面的context就是Widget的Element
  • StatelessElement:继承ComponentElement,StatelessWidget会创建StatelessElement ,主要就是调用build方法 并且将自己(Element)传出去

key的使用

我个人觉得理解flutter中key的使用,可以更好的理解Render的渲染,大多数时候我们不需要使用到key,但是在修改或删除widget集合的时候,我们会发现界面并没有按照我们预想的那样排版,这时候我们就会发现key的重要性,这里推荐一个关于key比较详细的博客

我们知道Widget和Element是一一对应的,Widget只是一个配置且无法修改,而 Element才是真正被使用的对象,并可以修改。举个简单的例子,定义橙、红、绿三种颜色的widget集合List,点击按钮移除集合中的第一个橙色的widget,会发现定义在statelessWidget里,橙色widget确实被移除了,但是定义在statefluWidget里,widget被移除了,但是第一个widget依然是橙色。why?注意Widget中的一个函数canUpdate(),Element有没有更新取决于这个函数

@immutable abstract class Widget extends DiagnosticableTree { 
...
static bool canUpdate(Widget oldWidget, Widget newWidget) { 
return oldWidget.runtimeType == newWidget.runtimeType && oldWidget.key == newWidget.key; 
}
  • statelessWidget:注意这里的颜色是存储在Widget中的,所以第一个橙色的widget被移除是很好理解的,widget不存在了颜色肯定也被移除了。
  • statefluWidget:移除第一个widget,canUpdate 对两个(新老) Widget 的 类型 和 key 进行比较,从而判断出当前的 Element 是否需要更新,当canUpdate 方法返回 true 说明不需要替换Element,当canUpdate 方法返回 false时,说明需要替换Element。在没有使用key的情况下,canUpdate返回true,所以Element没有更新,注意这里的颜色是定义在Element里的,所以第一个widget虽然移除了,但是Element还是原来的。

image.png

总结

了解了StatefluWidget的生命周期函数,我们知道build会被多次触发而重新渲染,比如调用如下函数都会触发build渲染:

  • setState
  • didChangeDependencies
  • didUpdateWidget

但是为了性能,我们要尽可能的避免频繁或者不必要的渲染,可以通过以下方式:

  • provider:利用这个第三方库实现局部刷新,或者单独封装一个statefullWidget,在里面单独setState。provider的目标就是把所有widget都变成statelessWidget
  • 尽可能的避免无效刷新:比如有这样一个需求,只有输入用户名和密码时登录按钮才可以点击。我们通常会定义一个bool变量actived来控制登录按钮是否可以点击,然后会在TextField的回调函数onChanged中去判断输入框里是否有值来改变actived变量,最后调用setState属性组件,但是这样会导致很多无效的刷新,如果在setState之前判断一下两次actived是否相等,只有在不相等时才调用setState就可以避免无效的刷新
  • 尽量使用const来定义一些不变的Widget,这相当于缓存一个Widget并复用它。
  • 尽量给Widget指定大小,避免不必要的Layout计算,比如ListView的itemExtent使用,该参数如果不为null,则会强制children的“长度”为itemExtent的值,可以提前计算“长度”

Widget与布局

万物都是Widget,简单记录几个widget

  • MaterialApp(App素材)

    • home属性需要一个Widget
    • Scaffold小部件,导航栏可以设置文字、颜色,而且可以自定义Widget,body属性
    • debugShowCheckedModeBanner属性,是否显示Debug标记(便于我们在调试版本中做操作)
  • ListView

    • 类似iOS中的TableView
    • 构造方法:ListView.builder(itemCount,itemBuilder),参数:itemCount:当前这个listView总共有多少个item。参数:itemBuilder,是一个回调函数。function(BuildContext context,int index),indext:目前要返回的cell的index。说白了,就是现在给我返回的第几个item
  • Container

    • 类似iOS的UIView。一个空的小部件。很常用
    • margin属性,内边距。让我内部的小部件往里面缩。EdgetInsets.all(10)。上下左右往里面缩10个px。每一个视图Widget都可以看成一个矩形
    • color属性,当前这个Widget的颜色。技巧:当我们布局某个Widget的时候,先给个颜色。便于我们调整布局
  • Image

    • Image.network(url)构造函数
  • SizedBox

    • 用来占位的小部件。在复杂的布局中很常用。

Flutter弹性布局

  • Center:让子部件在本部件的居中位置显示

  • Container中相对位置属性

    • Alignment。参数:x 和 y
    • 原点在中间位置
  • Row&Column

    • 横向布局Row。子部件按照主轴方向(横向)排列。主轴方向从左到右
    • 纵向布局Column。子部件按照主轴方向(纵向)排列。主轴方向从上到下
    • 每一个UI部件都可以看成一个矩形的“盒子”
    • 每一个盒子都有外边距Margin和内边距padding.
    • 主轴:MainAxisAlignment
      • spaceBetween: 剩下的空间平均分布到小部件之间!!
      • spaceAround: 剩下的空间平均分布到小部件周围!!
      • spaceEvenly:剩下的空间和小部件一起平均分!!
      • start 向主轴开始的方向对齐。
      • end 向主轴结束的方向对齐。
      • center 主轴方向居中对齐。
    • 交叉轴:CrossAxisAlignment 垂直于主轴方向
      • baseline:文字底部对齐
      • center:交叉轴方向居中对齐。
      • end:向交叉轴结束的方向对齐。
      • start:向交叉轴开始的方向对齐。
      • stretch:填满交叉轴方向
  • Stack

    • Stack是多层布局
    • 它的主轴方向是从内向外
      • alignment属性:可以定位。
      • Alignment(x,y)x和y取值
      • 范围-1.0 到 1.0
      • x=0,y=0 为中心
  • Positioned小部件

    • left、top、right、bottom 4个属性定位
    • 参数是像素位置
  • AspectRatio 宽高比小部件

    • 它的设置影响父布局
    • sepectRatio属性:宽高比。
    • 当父布局同时有宽度和高度,那么宽高比失效
  • Expanded 填充布局

    • 在主轴方向不会剩下间隙。将被Expanded包装的部件进行拉伸和压缩
    • 主轴横向,宽度设置没有意义
    • 主轴纵向,高度设置没有意义
    • 当Text被Expanded包装后,文字可以自动换行。这也被称作灵活布局。

Dart异步和多线程

Dart的事件循环

在Dart中,实际上有两种队列:

  • 事件队列(event queue),包含所有的外来事件:I/O、mouse events、drawing events、timers、isolate之间的信息传递。
  • 微任务队列(microtask queue),表示一个短时间内就会完成的异步任务。它的优先级最高,高于event queue,只要队列中还有任务,就可以一直霸占着事件循环,microtask queue添加的任务主要是由 Dart内部产生。

异步Future对象

  • 通过工厂构造方法创建Future对象。默认情况下是往事件队列中插入事件,当有空余的时间就去执行
  • 可以通过Future.microtask 方法来向微任务队列中插入一个任务,这样就会提高他执行的效率。
  • async 和 await 。如果Future内部代码希望同步执行,则使用await修饰。被async修饰的函数为异步执行。
  • Future结果处理,Future.then 用来注册一个Future完成时要调用的回调, Future.catchError注册一个回调,来捕捉Future的error
  • Future.catchError回调只处理原始Future抛出的错误,不能处理回调函数抛出的错误
  • onError只能处理当前Future的错误
  • Future.whenComplete 在Future完成之后总是会调用,不管是错误导致的完成还是正常执行完毕。

下面看两个示例加深一下理解,示例1:

void main() {
  testFuture3();
}
void testFuture3() async{
  print('1');
  Future(() => print('A')).then((value) => print('A结束'));
  Future(() => print('B')).then((value) => print('B结束'));

  scheduleMicrotask(() {
    print('微任务C');
  });
  sleep(Duration(seconds: 2));
  print('3');
}

输出:

flutter: 1
flutter: 3
flutter: 微任务C
flutter: A
flutter: A结束
flutter: B
flutter: B结束

分析: 先打印1能理解,为什么第二个打印3?都阻塞线程了还先打印?要注意优先级,Main > MicroTask > EventQueue

示例2:

void main() {
  testFuture4();
}

void testFuture4() {
  Future x1 = Future(() => null);
  Future x2 = x1.then((value) {
    print('6');
    //微任务
    scheduleMicrotask(() => print('7'));
  });

  x2.then((value) => print('8'));

  Future x = Future(() => print('1'));
  x.then((value) {
    print('4');
    Future(() => print('9'));
  }).then((value) => print('10'));

  Future(() => print('2'));

  scheduleMicrotask(() => print('3'));

  print('5');
}

输出:

flutter: 5
flutter: 3
flutter: 6
flutter: 8
flutter: 7
flutter: 1
flutter: 4
flutter: 10
flutter: 2
flutter: 9

分析: 7为什么会在8的后面?7不是微任务吗,不应该先执行吗?因为Future的结果处理会在Future执行完毕立即执行。可以看做是一个任务,所以6执行完了会立即执行8,相当于一个整体

Dart中的多线程

Dart是单线程语言,是以消息循环机制来运行的,但并不代表它不能并行执行代码。因为它拥有Isolate,一般把耗时计算放在多线程中。

Isolate

可以看成是一个小的进程,它拥有独立的内存空间,不同Isolate之间通过消息进行通信 ,它拥有自己的事件循环及队列(MicroTask 和 Event)。

  • Isolate的使用
    • 1、创建子线程任务:Isolate.spawn(arg1,arg2)

      • arg1: 需要在子线程执行的函数
      • arg2:传递给函数的参数
      • 这样就在另一个线程中执行arg1的代码了。
    • 2、端口通讯

      • ReceivePort port = ReceivePort()//构造一个端口。
      • port.listen(arg)//注册一个回调监听
      • arg为回调函数。参数为消息内容
      • 在子线程中.通过port.send() 发送消息
    • 3、关闭端口,销毁Isolate

      • 注意端口使用完毕需要调用port.close()函数关闭
      • Isolate使用完毕,需要调用Isolate.kill()函数销毁 示例
 int a = 10;
 void IsolateDemo() async {
  print('外部代码1');
  //创建一个port
  ReceivePort port = ReceivePort();
  //创建isolate
  Isolate iso = await Isolate.spawn(func, port.sendPort);
  //监听数据变化
  port.listen((message) {
    a = message;
    print('接受到了$a');
    port.close();
    iso.kill();
  });

  print('a = $a');
  print('外部代码2');
}
func(SendPort send) {
  sleep(Duration(seconds: 1));
  send.send(100);
  // print('第一个来了:a = $a');
}
  • compute
    • 由于dart中的Isolate比较复杂,数据传输比较麻烦,因此flutter在foundation库中封装了一个轻量级compute操作
  • 使用:compute(func,count)
    • func:子线程函数!func如果有返回值会直接被compute函数返回出去!
    • count: 给函数的参数 示例:
int a = 10;
void computeTest() async {
  print('外部代码1');
  int b = await compute(func2, 10);
  print('外部代码2 :b = $b  a = $a');
}

int func2(int count) {
  a = 999;
  sleep(Duration(seconds: 2));
  print('第二个来了');
  return 10000;
}

总结

  • 执行优先级:Main > MicroTask > EventQueue
  • Dart中的异步是可以和多线程结合使用的。
  • 如果Future中返回子线程的返回值,那么Future的处理是异步
  • 如果Future中没有返回子线程的返回值,那么Future的处理是同步
  • Future的结果处理会在Future执行完毕立即执行。可以看做是一个任务

Mixin

首先Dart不能多重继承的,比如A类不能同时继承父类B和父类C,Mixin是关键字,字面意思是混入,可以解决多重继承的问题,但是一定要注意mixin定义的类不能有构造函数,否则编译会出错。下面举个例子来理解就容易多了,比如有类小明A、小王B、小马C,它们的父类都是Person,具有speak()方法,但是A、B、C都各自有不同的特点

  • A:会打球,会喝酒
  • B:会跳舞、会打球
  • C:会喝酒、会跳舞 如果把会打球、会喝酒、会跳舞都定义在Person类中是不合适的,因为不是每个人都会,但如果A、B、C三个类都各自定义自己,那如果以后A学会了跳舞还得重新定义,这时候就需要使用mixin了,使用with关键字混入,如下所示
class Person{
  speak(){
     print("能说会道")
  }
}
//class定义或者mixin关键字,但是千万不能实现构造函数
class PlayBall(){
  print("会打球")
}
mixin Dance(){
  print("会跳舞")
}
mixin Drink(){
  print("会喝酒")
}

//A继承person 同时混入PlayBall和Drink
class A extends Person with PlayBall,Drink{
  PlayBall();
  Drink();
}

Flutter特点

  • Flutter是Google开源的构建用户界面的跨平台工具包,万物皆Widget
  • Dart中级联操作符“..”,调用后返回相当于this,而“.”返回的是方法的返回值
  • Dart中没有public和private等关键字,默认就是公开的,使用_下划线表示私有变量
  • 不依赖原生UI,具有独立的渲染引擎。这也是flutter项目包体积大的原因,因为渲染引擎被封装进应用里了。这点跟RN有很大不同,RN是桥接的原生UI,如果原生UI发生改变,那RN是必须要更新的。
  • 为什么Flutter中大量final修饰的属性,const修饰的构造方法?因为Flutter渲染逻辑是增量渲染,widget结构是树桩结构,想改变屏幕内容就直接改变Widget对象,常量对象的创建效率更高
  • 使用ErrorWidget.builder自定义一个widget统一管理错误页面
  • Flutter通过PlatformChannel与原生进行通讯,分三种
    • BasicMessageChannel:用于传递字符串和半结构化的信息
    • MethodChannel:用于传递方法调用
    • EventChannel:用于数据流的通讯