Flutter 没有完整的生命周期?

·  阅读 528

1.这是什么

1.1 简介

GitHub:flutter_lifecycle

本文将要介绍的是一个在 flutter 上实现的生命周期系统,通过本库代码能够实现 StatefulWidget 具备一套完整的生命周期。并且在任何你想要的地方都能够轻松感知生命周期。

生命周期 这个词对于移动端开发来说是非常熟悉的,我们常常在不同的生命周期中处理不同的业务逻辑,例如:在 onCreate 中初始化数据或注册监听,在 onDestory 中释放资源或解绑注册。

所以在使用 flutter 开发移动应用时,我们期望有一套完整的生命周期系统来应对复杂的业务场景

其实 在 flutter 中 StatefulWidget 是具备生命周期的,例如:initState(),dispose()但是不够完整,对于一些特殊的场景没有提供对应的生命周期,例如:切换前后台,页面跳转,页面返回,等等这些移动应用常见的场景都没有对应的回调可以监听

于是我借鉴原生平台 Android/iOS 的生命周期思想,实现了在 Flutter 上的一套生命周期系统

/// 生命周期状态
enum LifecycleState {
  /// 描述:初始化状态
  /// 频率:单次调用
  /// 说明:在 StatefulWidget 创建的初始化阶段触发
  onInit,

  /// 描述:创建完成状态
  /// 频率:单次调用
  /// 说明:StatefulWidget 创建完成,第一帧渲染完成触发
  onCreate,

  /// 描述:开始执行
  /// 频率:(可能)多次调用
  /// 说明:StatefulWidget 开始或重新 可见(暂时不可交互);例如:首次进入页面时/非全屏界面消失时;与 #onStop 成对
  onStart,

  /// 描述:开始交互
  /// 频率:(可能)多次调用
  /// 说明:StatefulWidget 开始或重新 可交互;例如:首次进入页面可交互时/非全屏界面消失时;与 #onPause 成对
  onResume,

  /// 描述:挂起/暂停执行
  /// 频率:(可能)多次调用
  /// 说明:StatefulWidget 可见但不可交互;在 StatefulWidget 失去焦点/进入后台/被系统或自定义的 非 全屏弹窗遮挡时调用;与 #onResume 成对
  onPause,

  /// 描述:停止执行
  /// 频率:(可能)多次调用
  /// 说明:StatefulWidget 不可见 & 不可交互;在 StatefulWidget 完全离开用户视野/进入后台/被系统或自定义的全屏弹窗遮挡时调用;与 #onStart 成对
  onStop,

  /// 描述:销毁
  /// 频率:单次调用
  /// 说明:StatefulWidget 销毁/退出程序时调用
  onDestroy;
}
1.2 图解

lifecycle.png

对于做过移动开发的同学一看这个解释以及这张图,应该是能够完全理解了,特别是Android开发者,简直无痛接受。这里针对其他平台开发者再啰嗦两句

1.3 详解

  • onInit
  • onCreate
  • onDestroy

这三个状态很好理解,就是初始化,创建完成和销毁。 比较难理解的是另外四个,这里通过场景描述简单解释一下

onStart 指页面开始执行的状态,这时候认为界面可见,但是暂时不可交互。

一般来说,这个状态非常短暂,紧接着就会执行 onResume

什么时候触发?1.首次进入界面(onCreate之后);2.从桌面(后台)回到前台的时候;3.从下一个页面返回到当前页面的时候

onResume 指页面开始可以交互的状态,这个时候界面可见,并且可以正常交互。这个生命周期之后进入正常运行阶段。

什么时候触发?1.首次进入界面(onStart之后);2.从任务栏(后台)回到前台的时候; 3.从下一个页面返回到当前页面的时候;4.非全屏弹窗移除时

onStartonResume 有什么区别? 正常情况下这两个生命周期按顺序(都会)执行;特别情况:依据是否进入过不可见的状态。如果从不可见恢复,则从 onStart 开始执行;如果从不可交互恢复,则从 onResume 开始执行

onPause 指界面从正常运行状态,变为不可交互的状态。这时候认为界面可见,但是不可交互。

什么是可见,但是不可交互?最简单的理解就是:当界面被一个非全屏的透明弹窗遮挡了,用户可以看到这个界面,但是无法触摸交互。

什么时候触发?1.非全屏弹窗覆盖到当前界面上;2.全屏的弹窗完全覆盖到当前界面上;3.进入后台(进入到任务栏);4.进入到后台(桌面)

onStop 指界面不再可见的状态

什么是不可见?一个全屏的界面覆盖在当前界面上;当程序完全进入后台

什么时候触发?1.全屏的弹窗完全覆盖到当前界面上;2.进入后台(回到桌面,进入到其他程序)

onPauseonStop 有什么区别? 正常情况下这两个生命周期按顺序(都会)执行;特别情况:依据是否进入不可见的状态。如果可见但不可交互,则只进入到 onPause 状态;如果完全不可见,则进入到 onStop 状态

2.如何使用

通过前面的讲解,现在对生命周期各个状态应该是非常熟悉,也了解了对应的触发场景。开发者可以按照自己的业务需求在对应的生命周期中编写逻辑

接下来简单了解一下如何使用

2.1 添加依赖库

在 pubspec.yaml 文件中添加 flutter_lifecycle_aware 依赖

dependencies:

  # flutter生命周期
  flutter_lifecycle_aware: ^0.0.2

2.2 创建观察者

在任何你想要监听 StatefulWidget 生命周期的地方继承 LifecycleObserver 观察者,实现 onLifecycleChanged 方法

///需要监听StatefulWidget生命周期的地方
class AViewModel extends LifecycleObserver {
  ///需要释放的资源
  ScrollController controller = ScrollController();

  ///初始化数据
  void initData() {}

  ///销毁/释放资源
  void destroy() {
    controller.dispose();
  }

  ///生命周期回调监听
  @override
  void onLifecycleChanged(LifecycleOwner owner, LifecycleState state) {
    if (state == LifecycleState.onCreate) {
      initData();
    } else if (state == LifecycleState.onDestroy) {
      destroy();
    }
  }
}

2.3 使用 Lifecycle 并且绑定观察者对象

在 StatefulWidget 中混入 Lifecycle ,绑定 LifecycleObserver 实现生命周期感知

///StatefulWidget中混入Lifecycle然后绑定LifecycleObserver
class _MyPageState extends State<MyPage> with Lifecycle {
  @override
  void initState() {
    super.initState();

    ///绑定LifecycleObserver
    getLifecycle().addObserver(AViewModel());
  }

  @override
  Widget build(BuildContext context) {
    return Container();
  }
}

2.4 辅助配置

在 MaterialApp > navigatorObservers 中添加辅助配置 LifecycleRouteObserver.routeObserver

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(

      /// 生命周期辅助设置
      navigatorObservers: [LifecycleRouteObserver.routeObserver],
      home: const MyPage(),
    );
  }
}

我们看到使用起来非常简单,直接继承或者混入 Lifecycle 就可以使得 StatefulWidget 组件具备生命周期状态。 LifecycleObserver 是实现生命周期感知,将被观察者注册到 Lifecycle 中,就能同步获取到 Lifecycle 的生命周期变化,我们称之为生命周期感知

以上例子是依据生命周期释放资源的场景,开发者根据需要自行拓展,例如实现一个生命周期感知的网络请求工具类,实现网络请求在特定状态自动取消,避免内存泄漏,避免业务逻辑出错。

3.怎么实现的

这个库听起来好像有点复杂,又什么生命周期系统,有什么生命周期感知。那么它到底是如何实现的呢?

3.1 基本条件的实现

3.1.1 使用 State 提供的一些生命周期

StatefulWidget 的 State 具备一套从创建到销毁的生命周期。

  • createState:创建State
  • initState:初始化State,整个生命周期中的初始化阶段调用,只会调用一次
  • didChangeDependencies:当 State 对象依赖发生变动时调用。initState 之后会调用一次,其他情况是 StatefulWidget 依赖的 InheritedWidget 中 updateShouldNotify 的返回true时调用
  • build:构建页面
  • addPostFrameCallback:WidgetsBinding 首次绘制完成回调,只调用一次
  • didUpdateWidget:当 Widget 更新时调用。实际上每次更新状态时,Flutter 都是创建一个新的 Widget
  • deactivate:从 Widget Tree 中移除 State 对象时会调用,理解为停止工作
  • dispose:Widget 被销毁时调用,整个生命周期只会执行一次

依据 State 提供的生命周期,可以看到我们可以直接使用其中的一部分,

1.将 initState 对应到 onInit

  @override
  void initState() {
    ///onInit
    _dispatchLifecycleState(LifecycleState.onInit);
  }

2.将 addPostFrameCallback 对应到 onCreateonStartonResume 依据 addPostFrameCallback 是在build之后完成了首次绘制,并且只调用一次,我们将此状态,对应到 创建/开始/可交互 这3个生命周期。

  @override
  void initState() {
    ///首次绘制完成,只回调一次
    WidgetsBinding.instance.addPostFrameCallback((Duration timeStamp) {
      ///生命周期方法回调:onCreate/onStart/onResume
      _dispatchLifecycleState(LifecycleState.onCreate);
      _dispatchLifecycleState(LifecycleState.onStart);
      _dispatchLifecycleState(LifecycleState.onResume);
    });
  }

3.将 deactivate 对应到 onPauseonStop 在 Widget 从组件树中移除时,触发 挂起/停止 的状态

  @override
  void deactivate() {
    ///onPause/onStop
    _dispatchLifecycleState(LifecycleState.onPause);
    _dispatchLifecycleState(LifecycleState.onStop);
  }

4.将 dispose 对应到 onDestroy

 @override
  void dispose() {
    ///onDestroy
    _dispatchLifecycleState(LifecycleState.onDestroy);
  }

诶~不对啊,这 StatefulWidget 本身提供的生命周期都已经满足了各种状态,那封装个毛?重命名一下就推出一个库?某某皮肤都不敢这么干!

当然不是拉!我们说一个新事物的出现一定是为了解决现有事物的不足,或者解决一些痛点。

现在有这么一个业务场景: 界面A(一级页面)和 界面B(二级详情页面)

  1. 当 界面A 跳转到 界面B 的时候,希望 界面A 有个停止执行的状态
  2. 从 界面B 返回到 界面A 的时候,希望 界面A 有个恢复执行的状态

这时候 StatefulWidget 就无法满足了 从 前进(页面跳转)的角度看,它只能提供 创建 相关的回调,并没有 停止 相关的回调 从 回退(页面返回)的角度看,它只能提供 停止 相关的回调,并没有 恢复 相关的回调

3.1.2 RouteAware 路由感知

这里我们了解一下 flutter 路由感知状态 RouteAware ,它提供了路由时的几个状态

  • didPushNext:当一个新的路由入栈,并且当前路由不再可见
  • didPopNext:当顶部路由出栈,当前路由重新展示
  • didPush:当前路由入栈
  • didPop:当前路由出栈

利用 didPushNext 监听界面跳转,并且知道 一级界面 被挂起执行,不再可见

此时,补充 前进 角度下 停止 相关的状态回调

  /// 调用时期:新的路由添加入栈,当前路由不再可见
  @override
  void didPushNext() {
    /// 新的 widget 已经添加入栈,当前 widget 挂起
    _dispatchLifecycleState(LifecycleState.onPause);
    _dispatchLifecycleState(LifecycleState.onStop);
  }

利用 didPopNext 监听界面返回,并切知道 一级界面 回到重新执行状态

此时,补充 回退 角度下 恢复 相关的状态回调

  /// 调用时期:顶部路由弹出,当前路由显示
  @override
  void didPopNext() {
    /// 回到当前 widget onStart/onResume
    _dispatchLifecycleState(LifecycleState.onStart);
    _dispatchLifecycleState(LifecycleState.onResume);
  }

到目前为止,页面的各种生命周期算是比较完整了,一个页面/多个页面交互的各种生命周期也完整了。

有个问题是,我们依赖路由跳转的感知实现了 一级页面 的 暂停 和 恢复,但是对于切换前后台的时候,这些状态还是无法感知。

3.1.3 AppLifecycleState 监听 App 生命周期监听

Flutter 提供了对 App 的生命周期监听,可以实现切换前后台的回调。

  • resumed:应用程序可见,并且可交互状态
  • inactive:应用程序处于非活动状态
  • paused:应用程序处于用户不可见,不响应用户状态,处于后台运行状态
  • detached:理解为程序销毁状态

inactive 这个状态有点奇怪,按照源码备注理解,应该是进入后台时,程序处于非活动状态时触发,但是实际测试发现,从后台回到前台也会触发这个回调。

这里的切后台有两个概念:

  1. 完全切入后台,当前应用相关界面不可见。例如:回到 Home 桌面;通过任务栏切换到了其他应用
  2. 非完全切后台,当前应用界面可见,但

这里有一个很重要的知识点需要先了解一下!!!

Flutter 的所有程序代码都是运行在一个 Activity/Controller 当中

所以在混合开发的时候,展示一个新的 Activity/Controller 时,Flutter 会认为程序进入后台

再看一下 AppLifecycleState 几个状态的调用时机

一:普通情况

完全切后台情况 1.进入后台:inactive > paused 2.回到前台:inactive > resume

非完全切后台情况 1.进入后台:inactive 2.回到前台:resume

二:展示一个 Activity/Controller

全屏 Activity/Controller 的情况 1.全屏 Activity/Controller 展示:inactive > paused 2.全屏 Activity/Controller 消失:resumed

非全屏 Activity/Controller 的情况 1.非全屏 Activity/Controller 展示:inactive 2.非全屏 Activity/Controller 消失:resumed

三:存在多级界面 前面提到过 Flutter 所有的界面都是运行在一个 Activity/Controller 。

如果所有界面都在实现了生命周期监听的话, 那么当 Flutter 进入后台的时候,所有的界面都会收到程序进入后台的通知,但是我们前面已经实现了通过路由跳转的时候触发 暂停 和 恢复 的状态,如果此时所有的多级界面都再次回调,则会产生错误的多余的状态调用。

这里其实只需要处理最顶层的界面的 停止 和 恢复

AppLifecycleState 的调用时机比较复杂,综上分析之后对应的状态调用应该这么写

  @override
  void didChangeAppLifecycleState(AppLifecycleState state) {
    /// flutter 所有界面都在同一个Activity/Controller中,因此所有页面都能响应切换前后台的回调
    /// 此处逻辑只需要栈顶页面需要响应 挂起/恢复 回调
    bool isCurrent = _modalRoute?.isCurrent ?? false;
    if (isCurrent) {
      switch (state) {
        case AppLifecycleState.resumed: //恢复交互
          /// 如果执行了 AppLifecycleState.paused 则需要从 onStart 开始
          if (_callStartStateWhenForeground) {
            _dispatchLifecycleState(LifecycleState.onStart);
            _callStartStateWhenForeground = false;
          }

          _dispatchLifecycleState(LifecycleState.onResume);

          break;
        case AppLifecycleState.inactive: //挂起

          ///进入后台 和 从桌面回来 都会执行 inactive 回调,添加逻辑,去除从后台回来的多余执行
          if (!_callStartStateWhenForeground) {
            _dispatchLifecycleState(LifecycleState.onPause);
          }

          break;
        case AppLifecycleState.paused: //停止

          ///切后台执行了 AppLifecycleState.paused ,则认为回到前台生命周期从 onStart 开始
          _callStartStateWhenForeground = true;

          _dispatchLifecycleState(LifecycleState.onStop);
          break;
        case AppLifecycleState.detached: //销毁
          _dispatchLifecycleState(LifecycleState.onDestroy);
          break;
      }
    }
  }

到这里算是完整的具备了各种场景下应该触发哪一个生命周期的基本条件。下一步只需要封装一下就可以了

3.2 封装

对于这种场景一般的实现方式是,创建一个抽象类,定义一系列的抽象方法,给开发者重写,然后在对应的回调中做业务逻辑。示例代码

abstract class Lifecycle {
  void onInit();

  void onDestroy();
}

class MyPageState extends State<MyPage> with Lifecycle {

  ///一些列需要释放资源的对象
  ScrollController scrollController = ScrollController();
  PageController pageController = PageController();
  AViewModel aVM = AViewModel();
  BViewModel bVM = BViewModel();
  
  @override
  Widget build(BuildContext context) {
    return Container();
  }

  @override
  void onDestroy() {
    scrollController.dispose();
    pageController.dispose();
    aVM.destroy();
    bVM.destroy();
  }

  @override
  void onInit() {
    aVM.initData();
    bVM.initData();
  }
}

这种业务代码很常见,有问题吗?没有!中规中矩,但是个人觉得不是特别的完美,不想在这个 UI 界面中写一堆释放资源的代码。并且如果开发者忘记编写释放逻辑了怎么办?

于是我看了一下 Android framework 中 lifecycle 的实现方式。它的实现思想很简单,是解偶的思想在里面,通过观察者模式实现管理需要监听生命周期的对象,在 lifecycle(被观察者)生命周期变化的时候通知这些观察者,这些观察者被美称为:具备生命周期感知的对象

嚯~好高大上。一听就觉得很吊!不过看完,个人觉得这种方式确实很不错。所以我借鉴了

观察者

/// 生命周期观察者
/// 通过 onLifecycleChanged 监听 生命周期变化
/// 任何对象可以通过实现此类,并将自身添加到被观察者 LifecycleObservable,实现监听生命周期变化
abstract class LifecycleObserver {
  /// widget 状态改变回调
  void onLifecycleChanged(LifecycleOwner owner, LifecycleState state);
}

被观察者

/// 生命周期被观察者
/// 管理观察者对象:添加观察者,移除观察者,通知观察者
abstract class LifecycleObservable {
  /// 添加观察者
  void addObserver(LifecycleObserver observer);

  /// 移除观察者
  void removeObserver(LifecycleObserver observer);

  /// 通知观察者
  void notify(LifecycleState state);
}

观察者的实现类就使用常规套路,定义一个集合存储观察者对象,在生命周期改变时遍历通知所有观察者

生命周期所有者:内部持有被观察者对象

abstract class LifecycleOwner {
  ///Lifecycle被观察者
  LifecycleObservable getLifecycle();
}

生命周期功能类:内部实现生命周期分发

class Lifecycle extends LifecycleOwner {
  /// 获取被观察者,用来管理观察者
  @override
  LifecycleObservable getLifecycle() {
    return _dispatcher().getLifecycle();
  }
}

绑定观察者

///StatefulWidget 中混入/继承 Lifecycle 然后绑定 LifecycleObserver
class _MyPageState extends State<MyPage> with Lifecycle {
  @override
  void initState() {
    super.initState();

    ///绑定LifecycleObserver:AViewModel
    getLifecycle().addObserver(AViewModel());
  }
}

AViewModel 是一个继承了 LifecycleObserver 的具备生命周期感知的对象。那么现在的业务代码就可以这么写了

///需要监听 StatefulWidget 生命周期的地方
class AViewModel extends LifecycleObserver {
  ///需要释放的资源
  ScrollController controller = ScrollController();

  ///初始化数据
  void initData() {}

  ///销毁/释放资源
  void destroy() {
    controller.dispose();
  }

  ///生命周期回调监听
  @override
  void onLifecycleChanged(LifecycleOwner owner, LifecycleState state) {
    if (state == LifecycleState.onCreate) {
      initData();
    } else if (state == LifecycleState.onDestroy) {
      destroy();
    }
  }
}

现在只需要将 AViewModel 绑定进来就行,然后 AViewModel 就按照你的业务写好对应的逻辑即可

讲完。

详细实现代码可以前往 flutter_lifecycle 查看,觉得有用可以 star 一下,引入到项目中,使用过程中有什么问题可以反馈给我,会及时修改处理

分类:
Android
标签:
分类:
Android
标签:
收藏成功!
已添加到「」, 点击更改