Flutter的生命周期分为两个部分,一个是Flutter本身的组件的生命周期,一个是平台相关的生命周期。
Stateful 组件的生命周期
StatefulWidget 组件的生命周期时非常重要的知识点,就像 Android 中 Activity 的生命周期一样,不仅在以后的工作中经常用到,面试也会经常被问到。
在 Flutter 中一切皆 「组件」,而组件又分为 「StatefulWidget(有状态)」 和 **StatelessWidget(无状态)**组件 ,他们之间的区别是 StatelessWidget 组件发生变化时必须重新创建新的实例,而 StatefulWidget 组件则可以直接改变当前组件的状态而无需重新创建新的实例。
❝注意:使用的 Flutter 版本 和 Dart 版本如下: Flutter 1.22.4 • channel stable • https://github.com/flutter/flutter.git Framework • revision 1aafb3a8b9 (6 weeks ago) • 2020-11-13 09:59:28 -0800 Engine • revision 2c956a31c0 Tools • Dart 2.10.4 不同的版本 StatefulWidget 组件的生命周期会有差异。 ❞
下面的 StatefulWidget 和 State 结构图是StatefulWidget 组件生命周期的概览,不同版本的差异也可以对比此结构图。
生命周期流程图:
下面详细介绍 StatefulWidget 组件的生命周期。
生命周期一:createState
下面是一个非常简单的 StatefulWidget 组件:
代码语言:javascript复制class StatefulWidgetDemo extends StatefulWidget {
@override
_StatefulWidgetDemoState createState() => _StatefulWidgetDemoState();
}
class _StatefulWidgetDemoState extends State<StatefulWidgetDemo> {
@override
Widget build(BuildContext context) {
return Container();
}
}
当我们构建一个 StatefulWidget 组件时,首先执行其「构造函数」(上面的代码没有显示的构造函数,但有默认的无参构造函数),然后执行 「createState」 函数。但构造函数并不是生命周期的一部分。
当 StatefulWidget 组件插入到组件树中时 「createState」 函数由 「Framework」 调用,此函数在树中给定的位置为此组件创建 「State」,如果在组件树的不同位置都插入了此组件,即创建了多个此组件,如下:
代码语言:javascript复制Row(children: [
MyStatefulWidget(),
MyStatefulWidget(),
MyStatefulWidget(),
],)
那么系统会为每一个组件创建一个单独的 「State」,当组件从组件树中移除,然后重新插入到组件树中时, 「createState」 函数将会被调用创建一个新的 「State」。
「createState」 函数执行完毕后表示当前组件已经在组件树中,此时有一个非常重要的属性 「mounted」 被 「Framework」 设置为 「true」。
生命周期二:initState
「initState」 函数在组件被插入树中时被 Framework 调用(在 「createState」 之后),此函数只会被调用一次,子类通常会重写此方法,在其中进行初始化操作,比如加载网络数据,重写此方法时一定要调用 「super.initState()」,如下:
代码语言:javascript复制@override
void initState() {
super.initState();
//初始化...
}
如果此组件需要订阅通知,比如 「ChangeNotifier」 或者 「Stream」,则需要在不同的生命周期内正确处理订阅和取消订阅通知。
- 在 「initState」 中订阅通知。
- 在 「didUpdateWidget」 中,如果需要替换旧组件,则在旧对象中取消订阅,并在新对象中订阅通知。
- 并在 「dispose」 中取消订阅。
另外在此函数中不能调用 「BuildContext.dependOnInheritedWidgetOfExactType」,典型的错误写法如下:
代码语言:javascript复制@override
void initState() {
super.initState();
IconTheme iconTheme = context.dependOnInheritedWidgetOfExactType<IconTheme>();
}
异常信息如下:
解决方案:
代码语言:javascript复制@override
void didChangeDependencies() {
super.didChangeDependencies();
context.dependOnInheritedWidgetOfExactType<IconTheme>();
}
上面的用法作为初学者使用的比较少,但下面的错误代码大部分应该都写过:
代码语言:javascript复制@override
void initState() {
super.initState();
showDialog(context: context,builder: (context){
return AlertDialog();
});
}
异常信息如下:
解决方案:
代码语言:javascript复制@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
showDialog(context: context,builder: (context){
return AlertDialog(title: Text('AlertDialog'),);
});
});
}
❝注意:弹出 AlertDialog 在 didChangeDependencies 中调用也会出现异常,但和上面的异常不是同一个。 ❞
生命周期三:didChangeDependencies
didChangeDependencies 方法在 initState 之后由 Framework 立即调用。另外,当此 「State」 对象的依赖项更改时被调用,比如其所依赖的 「InheritedWidget」 发生变化时, Framework 会调用此方法通知组件发生变化。
此方法是生命周期中第一个可以使用 「BuildContext.dependOnInheritedWidgetOfExactType」 的方法,此方法很少会被重写,因为 Framework 会在依赖发生变化时调用 「build」,需要重写此方法的场景是:依赖发生变化时需要做一些耗时任务,比如网络请求数据。
didChangeDependencies 方法调用后,组件的状态变为 「dirty」,立即调用 build 方法。
生命周期四:build
此方法是我们最熟悉的,在方法中创建各种组件,绘制到屏幕上。Framework会在多种情况下调用此方法:
- 调用 「initState」 方法后。
- 调用 「didUpdateWidget」 方法后。
- 收到对 「setState」 的调用后。
- 此 「State」 对象的依存关系发生更改后(例如,依赖的 「InheritedWidget」 发生了更改)。
- 调用 「deactivate」 之后,然后将 「State」 对象重新插入树的另一个位置。
此方法可以在每一帧中调用,此方法中应该只包含构建组件的代码,不应该包含其他额外的功能,尤其是耗时任务。
生命周期五:didUpdateWidget
当组件的 「configuration」 发生变化时调用此函数,当父组件使用相同的 「runtimeType」 和 「Widget.key」 重新构建一个新的组件时,Framework 将更新此 「State」 对象的组件属性以引用新的组件,然后使用先前的组件作为参数调用此方法。
代码语言:javascript复制@override
void didUpdateWidget(covariant StatefulLifecycle oldWidget) {
super.didUpdateWidget(oldWidget);
print('didUpdateWidget');
}
此方法中通常会用当前组件与前组件进行对比。Framework 调用完此方法后,会将组件设置为 「dirty」 状态,然后调用 「build」 方法,因此无需在此方法中调用 「setState」 方法。
生命周期六:deactivate
当框架从树中移除此 State 对象时将会调用此方法,在某些情况下,框架将重新插入 State 对象到树的其他位置(例如,如果包含该树的子树 State 对象从树中的一个位置移植到另一位置),框架将会调用 build 方法来提供 State 对象适应其在树中的新位置。
生命周期七:dispose
当框架从树中永久移除此 State 对象时将会调用此方法,与 「deactivate」 的区别是,「deactivate」 还可以重新插入到树中,而 「dispose」 表示此 State 对象永远不会在 「build」。调用完 「dispose」后,「mounted」 属性被设置为 false,也代表组件生命周期的结束,此时再调用 「setState」 方法将会抛出异常。
子类重写此方法,释放相关资源,比如动画等。
非常重要的几个概念
下面介绍几个非常重要的概念和方法,这些并不是生命周期的一部分,但是生命周期过程中的产物,与生命周期关系非常紧密。
mounted
「mounted」 是 State 对象中的一个属性,此属性表示当前组件是否在树中,在创建 「State」 之后,调用 「initState」 之前,Framework 会将 「State」 和 「BuildContext」 进行关联,当 Framework 调用 「dispose」 时,mounted 被设置为 false,表示当前组件已经不在树中。
「createState」 函数执行完毕后表示当前组件已经在组件树中,属性 「mounted」 被 「Framework」 设置为 「true」,平时写代码时或者看其他开源代码时经常看到如下代码:
代码语言:javascript复制if(mounted){
setState(() {
...
});
}
❝强烈建议:在调用 「setState」 时加上 mounted 判断。 ❞
为什么要加上如此判断?因为如果当前组件未插入到树中或者已经从树中移除时,调用 「setState」 会抛出异常,加上 「mounted」 判断,则表示当前组件在树中。
dirty 和 clean
「dirty」 表示组件当前的状态为 「脏状态」,下一帧时将会执行 「build」 函数,调用 「setState」 方法或者 执行 「didUpdateWidget」 方法后,组件的状态为 「dirty」。
「clean」 与 「dirty」 相对应,「clean」 表示组件当前的状态为 「干净状态」,「clean」 状态下组件不会执行 「build」 函数。
setState
「setState」 方法是开发者经常调用的方法,此方法调用后,组件的状态变为 「dirty」,当有数据要更新时,调用此方法。
reassemble
「reassemble」 用于开发,比如 「hot reload」 ,在 release 版本中不会回调此方法。
与平台相关的生命周期
此篇文章所说的生命周期与 StatefulWidget 组件的生命周期是不同的,这里平台相关的生命周期指的是特定平台相关操作所产生的生命周期,比如 Android 中 App 退到后台后的onPause等。
有人下场景,App正在播放视频,此时回到手机桌面或者切换到其他App,那么此时视频应该暂停播放,Flutter 中使用 「AppLifecycleState」 实现:
代码语言:javascript复制class AppLifecycle extends StatefulWidget {
@override
_AppLifecycleState createState() => _AppLifecycleState();
}
class _AppLifecycleState extends State<AppLifecycle>
with WidgetsBindingObserver {
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
if (state == AppLifecycleState.resumed) {
//TODO
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('App 生命周期'),
),
body: Center(
child: Text(''),
),
);
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
}
重点是重写 「didChangeAppLifecycleState」 方法,「AppLifecycleState」 中的状态包括:resumed、inactive、paused、detached。
「didChangeAppLifecycleState」 方法的回调来源于系统的通知(notifications),正常情况下,App是能正常接收到这些通知,但有的情况下是无法接收到通知的,比如用户强制关机、手机没有电自动关机等。
下面对其状态详细说明:
- 「resumed」:应用程序可见且响应用户输入。
- 「inactive」:应用程序处于非激活状态,无法响应用户输入。在iOS上,打电话、响应TouchID请求、进入应用程序切换器或控制中心都处于此状态。在Android上,分屏应用,打电话,弹出系统对话框或其他窗口等。
- 「pause」:应用程序不可见且无法响应用户输入,运行在后台。处于此状态时,引擎将不会调用 「Window.onBeginFrame」 和 「Window.onDrawFrame」。
- 「detached」:应用程序仍寄存在Flutter引擎上,但与平台 View 分离。处于此状态的时机:引擎首次加载到附加到一个平台 View的过程中,或者由于执行 Navigator pop ,view 被销毁。
下面是关于生命周期经常遇到的问题:
有2个页面A和B,在B页面点击返回键返回到A,didChangeAppLifecycleState 不回调
其实这个问题大部分人是想要实现类似于Android 中 「onResume」 中的功能,用 didChangeAppLifecycleState 是无法实现此功能的,didChangeAppLifecycleState 是对应于整个应用程序的,而不是 Flutter 中 不同的路由(页面)。
从A->B,在从B返回A,A重新加载数据使用如下方法:
A页面代码:
代码语言:javascript复制class A extends StatelessWidget {
@override
Widget build(BuildContext context) {
return RaisedButton(onPressed: ()async{
var result = await Navigator.of(context).push(MaterialPageRoute(builder: (context){
return B();
}));
//从B返回到A时,执行下面的代码
//TODO 加载数据
});
}
}
B页面代码:
代码语言:javascript复制class B extends StatelessWidget {
@override
Widget build(BuildContext context) {
return RaisedButton(onPressed: (){
Navigator.of(context).pop('返回的参数');
});
}
}