Flutter 小技巧之 3.13 全新生命周期 AppLifecycleListener

Flutter 3.13 在 Framework 里添加了 AppLifecycleListener 用于监听应用生命周期变化,并响应退出应用的请求等支持,那它有什么特殊之处?和老的相比又有什么不同?

简单说,在 Flutter 3.13 之前,我们一般都是用 WidgetsBindingObserverdidChangeAppLifecycleState 来实现生命周期的监听,只是 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 可以更方便去判断和记录整个生命周期的链条变化,因为它已经帮你封装好回调方法,例如:

  • inactiveresumed 调用的是 onResume
  • detachedresumed 调用的是 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.onBeginFramePlatformDispatcher.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 端只发送了 inactivepause 两个状态,但是收到 pause 时,在 _generateStateTransitions 方法里,会根据 pauseAppLifecycleState 里的位置(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 端。

results matching ""

    No results matching ""