在App的开发过程中,我们通常都需要了解App以及各个页面的生命周期,方便我们在App进入前台时启动一些任务,在进入后台后暂停一些任务。同时,各个页面的生命周期也很重要,每个页面消失时要做一些内存清理、计时器清除、通知清除等操作。所以,本文主要就是学习一下在flutter开发App的时候,如何去怼App以及各个页面的生命周期进行监听和回调。
在Flutter开发中,所有的组件和页面都继承自Widget,所以探索页面的生命周期其实就是Widget的生命周期。和Android的Activity和iOS的Controller一样,在Widget中,也有对应生命周期的一些方法函数。当进行到某一阶段时,会自动回调对应的方法函数。
在 Flutter 中一切皆 组件,而组件又分为 StatefulWidget(有状态) 和 StatelessWidget(无状态)组件 ,他们之间的区别是 StatelessWidget 组件发生变化时必须重新创建新的实例,而 StatefulWidget 组件则可以直接改变当前组件的状态而无需重新创建新的实例。
下面就主要以分析一下StatefulWidget有状态组件的生命周期。 下图是StatefulWidget 和 State 结构图是StatefulWidget 组件生命周期的概览,不同版本的差异也可以对比此结构图。
其生命周期流程图则如下所示,下图中所有方框都是StatefulWidget中可以重写的方法,这些方法在响应的生命周期状态下会被自动回调。
下面是一个非常简单的 StatefulWidget 组件:
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,如果在组件树的不同位置都插入了此组件,即创建了多个此组件,如下:
Row(children: [
MyStatefulWidget(),
MyStatefulWidget(),
MyStatefulWidget(),
],)
那么系统会为每一个组件创建一个单独的 State,当组件从组件树中移除,然后重新插入到组件树中时, createState 函数将会被调用创建一个新的 State。
createState 函数执行完毕后表示当前组件已经在组件树中,此时有一个非常重要的属性 mounted被 Framework 设置为 true。
initState 函数在组件被插入树中时被 Framework 调用(在 createState 之后),此函数只会被调用一次,子类通常会重写此方法,在其中进行初始化操作,比如加载网络数据,重写此方法时一定要调用 super.initState(),如下:
@override
void initState() {
super.initState();
//初始化...
}
如果此组件需要订阅通知,比如 ChangeNotifier 或者 Stream,则需要在不同的生命周期内正确处理订阅和取消订阅通知。
另外在此函数中不能调用 BuildContext.dependOnInheritedWidgetOfExactType,典型的错误写法如下:
@override
void initState() {
super.initState();
IconTheme iconTheme = context.dependOnInheritedWidgetOfExactType<IconTheme>();
}
异常信息如下:
解决方案:
@override
void didChangeDependencies() {
super.didChangeDependencies();
context.dependOnInheritedWidgetOfExactType<IconTheme>();
}
上面的用法作为初学者使用的比较少,但下面的错误代码大部分应该都写过:
@override
void initState() {
super.initState();
showDialog(context: context,builder: (context){
return AlertDialog();
});
}
异常信息如下:
解决方案:
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
showDialog(context: context,builder: (context){
return AlertDialog(title: Text('AlertDialog'),);
});
});
}
注意:弹出 AlertDialog 在 didChangeDependencies 中调用也会出现异常,但和上面的异常不是同一个。
didChangeDependencies 方法在 initState 之后由 Framework 立即调用。另外,当此 State 对象的依赖项更改时被调用,比如其所依赖的 InheritedWidget 发生变化时, Framework 会调用此方法通知组件发生变化。
此方法是生命周期中第一个可以使用 BuildContext.dependOnInheritedWidgetOfExactType 的方法,此方法很少会被重写,因为 Framework 会在依赖发生变化时调用 build,需要重写此方法的场景是:依赖发生变化时需要做一些耗时任务,比如网络请求数据。
didChangeDependencies 方法调用后,组件的状态变为 dirty,立即调用 build 方法。
此方法是我们最熟悉的,在方法中创建各种组件,绘制到屏幕上。 Framework会在多种情况下调用此方法:
此方法可以在每一帧中调用,此方法中应该只包含构建组件的代码,不应该包含其他额外的功能,尤其是耗时任务。
当组件的 configuration 发生变化时调用此函数,当父组件使用相同的 runtimeType 和 Widget.key 重新构建一个新的组件时,Framework 将更新此 State 对象的组件属性以引用新的组件,然后使用先前的组件作为参数调用此方法。
@override
void didUpdateWidget(covariant StatefulLifecycle oldWidget) {
super.didUpdateWidget(oldWidget);
print('didUpdateWidget');
}
此方法中通常会用当前组件与前组件进行对比。Framework 调用完此方法后,会将组件设置为 dirty 状态,然后调用 build 方法,因此无需在此方法中调用 setState 方法。
当框架从树中移除此 State 对象时将会调用此方法,在某些情况下,框架将重新插入 State 对象到树的其他位置(例如,如果包含该树的子树 State 对象从树中的一个位置移植到另一位置),框架将会调用 build 方法来提供 State 对象适应其在树中的新位置。
当框架从树中永久移除此 State 对象时将会调用此方法,与 deactivate 的区别是,deactivate 还可以重新插入到树中,而 dispose 表示此 State 对象永远不会在 build。调用完 dispose后,mounted 属性被设置为 false,也代表组件生命周期的结束,此时再调用 setState 方法将会抛出异常。
子类重写此方法,释放相关资源,比如动画等。
下面介绍几个非常重要的概念和方法,这些并不是生命周期的一部分,但是生命周期过程中的产物,与生命周期关系非常紧密。
mounted 是 State 对象中的一个属性,此属性表示当前组件是否在树中,在创建 State 之后,调用 initState 之前,Framework 会将 State 和 BuildContext 进行关联,当 Framework 调用 dispose时,mounted 被设置为 false,表示当前组件已经不在树中。
createState 函数执行完毕后表示当前组件已经在组件树中,属性 mounted 被 Framework 设置为 true,平时写代码时或者看其他开源代码时经常看到如下代码:
if(mounted){
setState(() {
...
});
}
强烈建议:在调用 setState 时加上 mounted 判断。
为什么要加上如此判断?因为如果当前组件未插入到树中或者已经从树中移除时,调用 setState 会抛出异常,加上 mounted 判断,则表示当前组件在树中。
dirty 表示组件当前的状态为 脏状态,下一帧时将会执行 build 函数,调用 setState 方法或者 执行 didUpdateWidget 方法后,组件的状态为 dirty。
clean 与 dirty 相对应,clean 表示组件当前的状态为 干净状态,clean 状态下组件不会执行 build函数。
setState 方法是开发者经常调用的方法,此方法调用后,组件的状态变为 dirty,当有数据要更新时,调用此方法。
reassemble 用于开发,比如 hot reload ,在 release 版本中不会回调此方法。
App的生命周期与上面所说的StatefulWidget 组件的生命周期是不同的,这里App的生命周期指的是特定平台相关操作所产生的生命周期,比如 Android 中 App 退到后台后的onPause等。App正在播放视频,此时回到手机桌面或者切换到其他App,那么此时视频应该暂停播放。
App的生命周期的监听,在Flutter中需要通过监听器WidgetsBindingObserver监听器中的AppLifecycleState方法来是实现。
class CEAppLifePage extends StatefulWidget {
CEAppLifePage({Key key}) : super(key: key);
@override
_CEAppLifePageState createState() => _CEAppLifePageState();
}
//实现WidgetsBindingObserver观察者
class _CEAppLifePageState extends State<CEAppLifePage> with WidgetsBindingObserver{
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this); //添加观察者
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text("App生命周期"),),
body: Column(children: <Widget>[
Text('test data 1'),
RaisedButton(
child: Text('next page'),
onPressed: (){
// KLEasyLoading.showInfo('生命周期 test');
Navigator.of(context).push(new MaterialPageRoute(builder: (context) => CELoginPage()));
}
),
],),
);
}
// 生命周期变化时回调
// resumed:应用可见并可响应用户操作,app进入前台
// inactive:用户可见,但不可响应用户操作,比如来了个电话,前后台切换的过渡状态
// paused:已经暂停了,用户不可见、不可操作,app进入后台
// suspending:应用被挂起,此状态IOS永远不会回调
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
super.didChangeAppLifecycleState(state);
print("didChangeAppLifecycleState: $state");
}
//当前系统改变了一些访问性活动的回调
@override
void didChangeAccessibilityFeatures() {
super.didChangeAccessibilityFeatures();
print("didChangeAccessibilityFeatures");
}
//低内存回调
@override
void didHaveMemoryPressure() {
super.didHaveMemoryPressure();
print("didHaveMemoryPressure");
}
//用户本地设置变化时调用,如系统语言改变
@override
void didChangeLocales(List<Locale> locale) {
super.didChangeLocales(locale);
print("didChangeLocales");
}
//应用尺寸改变时回调,例如旋转
@override
void didChangeMetrics() {
super.didChangeMetrics();
Size size = WidgetsBinding.instance.window.physicalSize;
print("didChangeMetrics :宽:${size.width} 高:${size.height}");
}
//系统切换主题时回调
@override
void didChangePlatformBrightness() {
super.didChangePlatformBrightness();
print("didChangePlatformBrightness");
}
///文字系数变化
@override
void didChangeTextScaleFactor() {
super.didChangeTextScaleFactor();
print(
"didChangeTextScaleFactor :${WidgetsBinding.instance.window.textScaleFactor}");
}
@override
void dispose() {
super.dispose();
WidgetsBinding.instance.removeObserver(this); //销毁观察者
}
}
重点是重写 didChangeAppLifecycleState 方法,AppLifecycleState 中的状态包括:resumed、inactive、paused、detached。
didChangeAppLifecycleState 方法的回调来源于系统的通知(notifications),正常情况下,App是能正常接收到这些通知,但有的情况下是无法接收到通知的,比如用户强制关机、手机没有电自动关机等。
下面对其状态详细说明:
其实这个问题大部分人是想要实现类似于Android 中 onResume 中的功能,用 didChangeAppLifecycleState 是无法实现此功能的,didChangeAppLifecycleState 是对应于整个应用程序的,而不是 Flutter 中 不同的路由(页面)。
从A->B,在从B返回A,A重新加载数据使用如下方法:
// A页面代码
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页面代码:
class B extends StatelessWidget {
@override
Widget build(BuildContext context) {
return RaisedButton(onPressed: (){
Navigator.of(context).pop('返回的参数');
});
}
}