Flutter 小技巧之 3.13 全新生命周期 AppLifecycleListener
Flutter 3.13 在 Framework 里添加了 AppLifecycleListener
用于监听应用生命周期变化,并响应退出应用的请求等支持,那它有什么特殊之处?和老的相比又有什么不同?
简单说,在 Flutter 3.13 之前,我们一般都是用 WidgetsBindingObserver
的 didChangeAppLifecycleState
来实现生命周期的监听,只是 didChangeAppLifecycleState
方法比较「粗暴」,直接返回 AppLifecycleState
让用户自己处理,使用的时候需要把整个 WidgetsBindingObserver
通过 mixin
引入。
而 AppLifecycleListener
则是在 WidgetsBindingObserver.didChangeAppLifecycleState
的基础上进行了封装,再配合当前 lifecycleState
形成更完整的生命周期链条,对于开发者来说就是使用更方便,并且 API 相应更直观。
首先 AppLifecycleListener
是一个完整的类,所以使用它无需使用 mixin
,你只需要在使用的地方创建一个 AppLifecycleListener
对象即可。
late final AppLifecycleListener _listener;
late AppLifecycleState? _state;
@override
void initState() {
super.initState();
_state = SchedulerBinding.instance.lifecycleState;
_listener = AppLifecycleListener(
onShow: () => _handleTransition('show'),
onResume: () => _handleTransition('resume'),
onHide: () => _handleTransition('hide'),
onInactive: () => _handleTransition('inactive'),
onPause: () => _handleTransition('pause'),
onDetach: () => _handleTransition('detach'),
onRestart: () => _handleTransition('restart'),
// This fires for each state change. Callbacks above fire only for
// specific state transitions.
onStateChange: _handleStateChange,
);
}
void _handleTransition(String name) {
print("########################## main $name");
}
其次,AppLifecycleListener
根据 AppLifecycleState
区分好了所有 Callback 调用,调用编排更加直观。
最后,AppLifecycleListener
可以更方便去判断和记录整个生命周期的链条变化,因为它已经帮你封装好回调方法,例如:
- 从
inactive
到resumed
调用的是onResume
- 从
detached
到resumed
调用的是onStart
现在通过 AppLifecycleListener
的回调,我们可以更方便和直观的感知到整个生命周期变化的链条,并且 3.13 正式版中还引入了一个全新的状态 : 「hidden
」,当然它其实在 Android/iOS 上是不工作的。
因为 hidden
这个概念在移动 App 上并不实际存在,例如它定义在这里只是为了对齐统一所有状态。
虽然在移动 App 平台虽然没有 hidden
这个状态,但是例如你在 Android 平台使用 AppLifecycleListener
,却还是可以收到 hidden
的状态回调,为什么会这样我们后面解释。
首先我们简单看下 AppLifecycleState
的几个状态:
detached
App 可能还存有 Flutter Engine ,但是视图并不存在,例如没有 FlutterView
,Flutter 初始化之前所处的默认状态。
也就是其实没有视图的情况下 Engine 还可以运行,一般来说这个状态仅在 iOS 和 Android 上才有,尽管所有平台上它是开始运行之前的默认状态,一般不严谨要求的情况下,可以简单用于退出 App 的状态监听。
resumed
表示 App 处于具有输入焦点且可见的正在运行的状态。
例如在 iOS 和 macOS 上对应于在前台活动状态。
Android 上无特殊情况对应 onResume
状态,但是其实和 Activity.onWindowFocusChanged
有关系。
例如当存在多 Activity 时:
- 只有 Focus 为 true 的 Activity ,进入
onResume
才会是resumed
- 其他 Focus 为 false 的 Activity,进入
onResume
会是inactive
只要还是看 Activity.onWindowFocusChanged
回调里是否 Foucs,只是默认情况下 Flutter 只有单 Activity ,所以才说无特殊情况对应 onResume
状态。
inactive
App 至少一个视图是可见的,但没有一个视图具 Focus。
- 在非 Web 桌面平台上,这对应于不在前台但仍具有可见窗口的应用。
- 在 Web ,这对应没有焦点的窗口或 tab 里运行的应用。
- 在 iOS 和 macOS 上,对应在前台非活动状态下运行的 Flutter 视图,例如出现电话、生物认证、应用切换、控制中心时。
- 在 Android 上,这对应 Activity.onPause 已经被调用或
onResume
时没有 Focus 的状态。(分屏、被遮挡、画中画)
在 Android 和 iOS 上, inactive 可以认为它们马上会进入 hidden 和 paused 状态。
paused
App 当前对用户不可见,并且不响应用户行为。
当应用程序处于这个状态时,Engine 不会调用 PlatformDispatcher.onBeginFrame
和PlatformDispatcher.onDrawFrame
回调。
仅在 iOS 和 Android 上进入此状态。
hidden
App 的所有视图都被隐藏。
在 iOS 和 Android 上说明马上要进入 paused。
在 PC 上说明最小化或者不再可见的桌面上。
- 在 Web 上说明在不可见的窗口或选项卡中。
所以从上面可以看到,其实不同平台的生命周期还是存在差异的,而 AppLifecycleState
的作用就是屏蔽这些差异,并且由于历史原因,目前 Flutter 的状态名称并不与所平台上的状态名称一一对应,例如 :
在 Android 上,当系统调用 Activity.onPause 时,Flutter 会进入 inactive 状态;但是当 Android 调用 Activity.onStop,Flutter会进入 paused 状态。
当然,如果 App 被任务管理器、crash、kill signal 等场景销毁时,用户是无法收到任何回调通知的。
那么这时候,你再回过头来看 hidden
,就会知道为什么它在 Android 和 iOS 上并没有实际意义,因为它是为了 PC 端(最小化/不可见)而存在,但是如果你通过 AppLifecycleListener
进行监听,你会发现其实是可以收到 hidden
的回调,例如在 Android 和 iOS 上 :
前台到后台: inactive - hide - pause
后台回前台:restart - show - resume
明明在原生 Android 和 iOS 上并没有 hidden
,那为什么 Dart 里又会触发呢?
这是因为 Flutter 在 Framework 为了保证 Dart 层面生命周期的一致性,会对生命周期调用进去「补全」。
例如在退到后台时,native 端只发送了 inactive
和 pause
两个状态,但是收到 pause
时,在 _generateStateTransitions
方法里,会根据 pause
在 AppLifecycleState
里的位置(pause 和 inactive 之间还有 hidden) ,在代码里「手动」加入 hidden
从而触发 onHide
调用。
所以,在 Android 和 iOS 端使用 AppLifecycleState
时,我们一般不要去依赖 onHide
回调,因为本质上它并不适用于移动端的生命周期。
最后,AppLifecycleState
还提供了 onExitRequested
方法,但是它并不支持类似 Android 的 back 返回拦截场景,而是需要通过 ServicesBinding.instance.exitApplication(AppExitType exitType)
触发的退出请求,才可以被 onExitRequested
拦截,前提是调用时传入了 AppExitType.cancelable
。
也就是
ServicesBinding.instance.exitApplication(AppExitType.cancelable);
这样的调用才会触发onExitRequested
,另外目前System.exitApplication
的响应只在 PC 端实现,移动端不支持。
@override
void initState() {
super.initState();
_listener = AppLifecycleListener(
onExitRequested: _handleExitRequest,
);
}
Future<AppExitResponse> _handleExitRequest() async {
var result = await showDialog(
context: context,
builder: (context) => AlertDialog.adaptive(
title: const Text('Exit'),
content: const Text('Exit'),
actions: [
TextButton(
child: const Text('No'),
onPressed: () {
Navigator.of(context).pop(false);
},
),
TextButton(
child: const Text('Yes'),
onPressed: () {
Navigator.of(context).pop(true);
},
),
],
));
final AppExitResponse response =
result ? AppExitResponse.exit : AppExitResponse.cancel;
return response;
}
最后做个总结:
AppLifecycleListener
的好处就是不用 mixin ,并且通过回调可以判断生命周期链条。AppLifecycleState
的状态和命名与原生端并不一定对应。- Flutter 在单页面和多页面下可能会出现不同的状态相应。
- hidden 在 Android 和 iOS 端并不存在,它仅仅是为了统一而手动插入的中间过程。
onExitRequested
只作用于 PC 端。