【Flutter】还在用 visibility_detector 监听页面生命周期?看我骚操作!

自定义混入封装实现 StatelessWidget 与 StatefulWidget 的页面生命周期监听

前言

浏览此文之前我希望读者能知道一些Flutter的基础知识,例如 StatelessWidget 与 StatefulWidget 区别,StatefulWidget 的生命周期,App 的生命周期 AppLifecycleState ,能区分页面生命周期与App生周期的不同。

作为一个移动开发者,不管是 Android 还是 iOS 都有完善的页面生命周期管理,我第一次接触 Flutter 我都惊呆了,如果一个倒计时我想在页面显示的时候展示倒计时,在页面不可见的时候暂停倒计时,非常简单的一个功能,我简直想死。

【Flutter】还在用 visibility_detector 监听页面生命周期?看我骚操作!

随便网络上一搜索其实很多Flutter开发者也发出了疑问,而网上的大多数解决方案都是推荐使用 visibility_detector 来做,我承认 visibility_detector 很优秀,特别是在一些埋点的特殊场景比原生都好用太多,大神们也是脑洞大开用它来进行生命周期的监听,我只能说…妙啊!

虽然不是那么“优雅”,虽然我们可以用封装Widget的方案来实现,但是总的来说还是破坏了控件树。

那么本文我们就探讨一种新的方案,使用原生路由监听方式实现 StatefulWidget 的页面生命周期监听,基于 Get 的生命周期管理实现 StatelessWidget 的生命周期监听。

一、实现代码预览

使用 visibility_detector 还是蛮简单的啊,用你的新方案会不会比较麻烦?

在开始之前我们先对比方案前的用法与新方案的用法。

使用 visibility_detector 来模拟页面生命周期管理的核心思想是:当一个widget变为可见时,我们可以认为它已经“resume”,当它完全不可见时,则可以认为它已经“stop”。

visibility_detector 最终实现:

class MyHomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Visibility Detector Demo'),
      ),
      body: Center(
        child: VisibilityDetector(
          key: Key('unique-key'),
          onVisibilityChanged: (VisibilityInfo info) {
            if (info.visibleFraction == 0)
              print('Widget is not visible');
            else if (info.visibleFraction == 1)
              print('Widget is fully visible');
            else
              print('Widget is partially visible');
          },
          child: Container(
            height: 200,
            color: Colors.blue,
            child: Center(
              child: Text('Hello World!'),
            ),
          ),
        ),
      ),
    );
  }

使用 visibility_detector 我们会包裹根视图,让整个用户完全展示出来就算可见了,否则是不可见。

StatefulWidget 最终实现效果,只需要 with StateLifecycle 就能实现 :

class WorkTrainingPage extends StatefulWidget {
  const WorkTrainingPage({Key? key}) : super(key: key);

  @override
  State<WorkTrainingPage> createState() => _WorkTrainingPageState();
}

class _WorkTrainingPageState extends State<WorkTrainingPage> with StateLifecycle{

  @override
  void onStart() {
    Log.d("${widget.toString()} ====> onStart");
  }

  @override
  void onStop() {
    Log.d("${widget.toString()} ====> onStop");
  }

  @override
  void onResume() {
    Log.d("${widget.toString()} ====> onResume");
    SmartDialog.showToast("${widget.toString()} ====> onResume");
  }

  @override
  void onPause() {
    Log.d("${widget.toString()} ====> onPause");
  }
  ...

StatelessWidget 的最终实现,只需要 with StateLessLifecycleMixin 就能实现:

class NotificationEnablePage extends StatelessWidget with StateLessLifecycleMixin {

  NotificationEnablePage({super.key});

  @override
  void onStart() {
    Log.d("${toString()} ====> onStart");
  }

  @override
  void onStop() {
    Log.d("${toString()} ====> onStop");
  }

  @override
  void onResume() {
    Log.d("${toString()} ====> onResume");
    SmartDialog.showToast("${toString()} ====> onResume");
  }

  @override
  void onPause() {
    Log.d("${toString()} ====> onPause");
  }

  ...

我觉得生命周期的监听就应该这样,因为不是每一个页面都需要处理生命周期,可见不可见去做对应的控件的操作,大部分的页面是用不到生命周期那么我们就可以不需要混入对应的状态,如果你有生命周期处理的特殊需要,那么就在那个页面混入此状态即可。

毕竟这些监听还是会多少占用内存的,为了极限的性能着想就不适合用继承或扩展的方式,用混入的方式就能很好的实现这个特点。

二、StatefulWidget 的混入实现

由于我们的 StatefulWidget 本身就有一些基础的生命周期,例如常见的 initState didChangeDependencies dispose 等。

我们可以直接基于 RouteAware 混入状态和 RouteObserver 来进行监听。

首先我们在顶层定义一个

final RouteObserver routeObserver = RouteObserver();

记得在main.dart中配置:

MaterialApp(
  navigatorObservers: [routeObserver],
  // ... other properties
)

不管是原生的 App 还是 Get的 App 都可以配置类似的 Observer 。

接下来就是完整的代码:

/*
 * StatefulWidget 的生命周期实现基于原生实现
 */
mixin StateLifecycle<T extends StatefulWidget> on State<T> implements RouteAware, WidgetsBindingObserver, IStateLifecycle {

  @override
  void initState() {
    super.initState();
    WidgetsBinding.instance.addObserver(this);
  }

  @override
  void didChangeDependencies() {
    super.didChangeDependencies();
    final route = ModalRoute.of(context);
    if (route != null && route is PageRoute) {
      routeObserver.subscribe(this, route);
    }
  }

  @override
  void dispose() {
    routeObserver.unsubscribe(this);
    WidgetsBinding.instance.removeObserver(this);
    super.dispose();
  }

  @override
  void didPop() {
    onStop();
  }

  @override
  void didPopNext() {
    onResume();
  }

  @override
  void didPush() {
    onStart();
  }

  @override
  void didPushNext() {
    onPause();
  }

  @override
  void didChangeAppLifecycleState(AppLifecycleState state) {
    if (state == AppLifecycleState.resumed) {
      onResume();
    } else if (state == AppLifecycleState.paused) {
      onPause();
    }
  }

   ...
}

// 自定义生命周期回调接口
abstract class IStateLifecycle {
  void onStart(); //启动页面
  void onStop(); //结束页面
  void onResume(); //重新可见
  void onPause(); //被覆盖隐藏
}

混入的相关使用我已经写了几篇文章了,不用我再注释和说明了吧。

需要注意的是这些生命周期定义的看着与 Android 类似,其实有差别 onStart onStop 只是启动和销毁,而 onResume onPause 则只会在重新可见和重新隐藏的时候调用,有点类似 Android 的 restart 。

在我们使用的时候就很简单,直接在 State 上混入即可:

class SettingPage extends BaseStatefulPage<SettingController> {
  SettingPage({super.key});

  @override
  State<SettingPage> createState() => _SettingPageState();

  @override
  SettingController createRawController() {
    return SettingController();
  }
}

class _SettingPageState extends BaseState<SettingPage, SettingController> with StateLifecycle {

  @override
  void onStart() {
    Log.d("${widget.toString()} ====> onStart");
  }

  @override
  void onStop() {
    Log.d("${widget.toString()} ====> onStop");
  }

  @override
  void onResume() {
    Log.d("${widget.toString()} ====> onResume");
    SmartDialog.showToast("${widget.toString()} ====> onResume");
  }

  @override
  void onPause() {
    Log.d("${widget.toString()} ====> onPause");
  }

   ...

不管是用继承的基类还是用原生的方式都是一样的效果,并不影响使用。

三、StatelessWidget 的混入实现

StatefulWidget 的 State 我们可以在对应的生命周期中创建监听,在销毁的时候移除监听,那么我们在无状态的 StatelessWidget 中怎么操作?

不知道大家有没想过,其实在 Get 框架中就算是 StatelessWidget 的 Controller 它一样可以在移除的时候自动销毁,就是说 Get 内部已经有一套生命周期处理了,当然如果大家对原理感兴趣可以搜索相关文章,这不是本文的重点。

那我们就可以创建一个类似的类,放入依赖注入池中也就能监听到 StatelessWidget 的生命周期了,也就能创建监听与移除监听了,思路很清晰,代码很复杂,直接上代码:

/*
 * StateLessWidget 的生命周期实现基于 Getx 框架扩展实现
 */
mixin StateLessLifecycleMixin on StatelessWidget implements RouteAware, WidgetsBindingObserver, IStateLifecycle {
  bool _lifecyclePerformed = false;

  void _ensureLifecycle(BuildContext context) {
    if (!_lifecyclePerformed) {
      final stateLessLifecycle = Get.put(StateLessLifecycle(), tag: key?.toString());
      stateLessLifecycle.setLifecycleCallback(MyLifecycleCallback(
        onReadyAction: () => _onReady(context),
        onCloseAction: _onClose,
      ));
      _lifecyclePerformed = true;
    }
  }

  void _onReady(BuildContext context) {
    final route = ModalRoute.of(context);
    if (route != null && route is PageRoute) {
      routeObserver.subscribe(this, route);  // 订阅至RouteObserver
    }
    WidgetsBinding.instance.addObserver(this); // 添加至WidgetsBinding观察者
  }

  void _onClose() {
    routeObserver.unsubscribe(this); // 取消订阅RouteObserver
    WidgetsBinding.instance.removeObserver(this); // 移除WidgetsBinding观察者
  }

  @protected
  Widget buildWidget(BuildContext context);

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

  @override
  void didChangeAppLifecycleState(AppLifecycleState state) {
    if (state == AppLifecycleState.resumed) {
      onResume();
    } else if (state == AppLifecycleState.paused) {
      onPause();
    }
  }

  @override
  void didPop() {
    onStop();
  }

  @override
  void didPopNext() {
    onResume();
  }

  @override
  void didPush() {
    onStart();
  }

  @override
  void didPushNext() {
    onPause();
  }

   ...
}

/*
 * 继承自 DisposableInterface 实现 Getx 的生命周期
 */
class StateLessLifecycle extends DisposableInterface {
  @override
  void onReady() {
    super.onReady();
    callback?.onReadyCallback();
  }

  @override
  void onClose() {
    callback?.onCloseCallback();
    super.onClose();
  }

  ILifecycleCallback? callback;

  void setLifecycleCallback(ILifecycleCallback callback) {
    this.callback = callback;
  }
}

// 自定义生命周期回调接口
abstract class ILifecycleCallback {
  void onReadyCallback();

  void onCloseCallback();
}

class MyLifecycleCallback implements ILifecycleCallback {
  final VoidCallback onReadyAction;
  final VoidCallback onCloseAction;

  MyLifecycleCallback({required this.onReadyAction, required this.onCloseAction});

  @override
  void onReadyCallback() {
    onReadyAction();
  }

  @override
  void onCloseCallback() {
    onCloseAction();
  }
}

需要注意的是由于没有直接 initState 这种生命周期,所以在 StatelessWidget 的混入中我们需要加入一个Flag来避免重复创建,其他流程在我们实现了 StatelessWidget 的生命周期之后就可以类似 StatefulWidget 一般的实现了。

需要注意的是在 StatelessWidget 中监听生命周期我们会额外的注入一个对象到依赖注入池,内存开销会稍微大于 StatefulWidget,但是中的来说区别不大,并且只有在混入的生命周期状态的情况下才生效,如果不混入此状态,则不会有任何监听和内存开销,当然如果你在意的话,可以把需要监听的页面替换为 StatefulWidget 来实现。

四、测试效果与日志

回桌面返回或息屏返回:

【Flutter】还在用 visibility_detector 监听页面生命周期?看我骚操作!

跳转页面与返回:

【Flutter】还在用 visibility_detector 监听页面生命周期?看我骚操作!

对于的 Log 如下所示,进入设置页面:

【Flutter】还在用 visibility_detector 监听页面生命周期?看我骚操作!

进入消息设置页面:

【Flutter】还在用 visibility_detector 监听页面生命周期?看我骚操作!

进入深色模式设置页面:

【Flutter】还在用 visibility_detector 监听页面生命周期?看我骚操作!

从其他页面返回深色模式设置页面:

【Flutter】还在用 visibility_detector 监听页面生命周期?看我骚操作!

返回到消息设置页面:

【Flutter】还在用 visibility_detector 监听页面生命周期?看我骚操作!

返回到设置页面:

【Flutter】还在用 visibility_detector 监听页面生命周期?看我骚操作!

那么多页面跳转会怎么样,如果你对此感兴趣可以查看我前文对于单页面多实例跳转的继承封装,并设置启动模式,结合此生命周期的回调也是一样的效果。

后记

其实比较新的 3.13 版本中加入全新生命周期 AppLifecycleListener 我没有基于这个实现,为了尽可能的兼容更多的版本还是使用老版本的 WidgetsBindingObserver 实现的。

关于 visibility_detector ,其实 visibility_detector 大行其道的今天,大家都这么用没什么不好的,我这种也勉强算是一种新的解题思路吧,相比通过控件的可见性判断来说是不是更显得“优雅”一些呢…[狗头]

当然了我没有说 visibility_detector 不行的意思,其实我自己的项目也是用的这种方案,不过后期我会慢慢过渡到新的方案中,如有什么问题也会同步更新。

那么本期内容就到这里,如讲的不到位或错漏的地方,希望同学们可以评论区指出,如果有更多更好更方便的方式也欢迎大家评论区交流。

本文的代码已经全部贴出,部分没贴出的代码可以在前文中找到,也可以到我的 Flutter Demo 查看源码【传送门】

如果感觉本文对你有一点点的启发,还望你能点赞支持一下,你的支持是我最大的动力啦!

Ok,这一期就此完结。

【Flutter】还在用 visibility_detector 监听页面生命周期?看我骚操作!

原文链接:https://juejin.cn/post/7342330487078387751 作者:Newki

(0)
上一篇 2024年3月5日 上午10:17
下一篇 2024年3月5日 上午10:28

相关推荐

发表回复

登录后才能评论