前言
最近学习了SwiftUI
和Flutter
,说实话两个学下来感觉太像了,然后心累了。用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:有状态,包含两个对象Widget
和State
初始化阶段:
CreateState
:State的构造方法,一旦执行这个回调,Mounted=true
initState
:这个回调里一般会做一些数据初始化
的工作
组件创建阶段:
didChangeDependencies
:依赖的InheritedWidget
发生变化之后或者组件依赖的全局 state 发生了变化时,也会调用 build,例如系统语言等、主题色等。build
:绘制组件,当调用setState方法,会重新调用build进行渲染didupdateWidget
:这个函数基本上是伴随着build调用而调用的
组件销毁阶段:
deactivate
:State 被暂时从视图树中移除时会调用这个方法,相当于安装中的onpause
回调dispose
:当Widget彻底销毁的时候
整个生命周期过程如下图所示:
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还是原来的。
总结
了解了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.catchErro
r回调只处理原始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
:用于数据流的通讯