返回

声明式 UI 架构下的生命周期演进:从内建属性到显式订阅

发布于:  at  08:01 上午
⏱️ 约 2859 字 • 预计阅读 8 分钟

Read in English

为什么声明式UI不再为组件内建onResume?对比原生View体系与声明式架构的差异,理解生命周期从"宿主驱动"向"按需订阅"的转变,以及如何在Flutter和Compose中实现生命周期监听。

消失的“实体”: 从View到Composable/Widget

在原生Android开发的世界, 创建模版项目后就会得到一个MainActivity和activity_main.xml. 我们习惯了在MainActivity里去绑定Button和Text再设置各种属性. 他们都是内存中看得见,摸得着的View. 但是到了Flutter中, 得到的了却是main()函数和一堆StatelessWidget和StatefulWidget. 不仅仅是Flutter, 连Jetpack Compose也在向这种’声明式UI’靠拢. Compose中同样找不到onResume(入口Activity除外),取而代之的而是层层嵌套的Composable函数

这种转变本质上是:我们不再持有 UI 的“句柄”,我们只持有“数据”。

正因为我们持有的只是数据,而数据本身是没有‘前后台’概念的(一个 String 字符串哪里知道自己是否在前台?)。所以,在声明式 UI 中寻找 onResume 本身就是一个伪命题。 只有当数据需要根据系统状态(如用户回来了)进行刷新时,我们才去主动询问系统。

生命周期演进图

onResume 不再属于 UI 组件

View体系的“重形约束”

在原生 Android 中,Activity 是一个重型容器,它直接占据着系统的窗口资源和输入焦点。这种设计决定了开发者必须拥有极强的“生存意识”:

  • 前后台切换: 我们必须精确掌握 onPause 和 onResume,以便在失去焦点时停止动画或暂停视频。
  • 极限生存: 在电池优化策略或**“不保留活动”**的极限测试下,Activity 随时会被销毁,我们不得不依赖 onSaveInstanceState 艰难地维持状态。
  • 任务流转: 耗时后台任务必须挪到专门的 Foreground/Background Service 中,否则随时会被系统“祭天”。

声明式 UI 的“逻辑重构”

但在 Flutter 和 Compose 的世界里,这种“资源焦虑”被屏蔽了。我们面对的是轻量级的函数和配置。

  • UI 即快照: 界面不再是长存的实体,而是数据的瞬时表达。销毁一个 Widget 就像扔掉一张草稿纸,成本极低。
  • 底层重构: 这种变化不仅仅是语法上的,更是底层生存逻辑的重构。你不再需要盯着每一个 View 对象的死活,只需要守护好背后的 State。

消失的本质:为什么它们敢“干掉” onResume?

在原生 Android 中,View 是一个具有**稳定身份(Stable Identity)**的对象。你通过 findViewById 拿到的 Button,在它被销毁前,其内存地址是不变的,因此它可以安全地持有状态与生命周期回调。

而在 Flutter 中,Widget 仅是不可变的配置描述(Immutable Configuration)。 它在每一帧都可能被重新创建。 你无法给一个“瞬时快照”绑定生命周期,因为这个对象本身随时会消失。 真正具有生命周期的不是 Widget,而是底层的 State 对象,因为它在 Widget 重建时依然保持稳定。

只有当业务需要时,才去“订阅”环境

其实 Flutter 并非没有生命周期,只是它不再作为 UI 组件的内建能力。 我们需要区分三个维度的生命周期:

  • App 级别(WidgetsBinding):整个应用进程的前后台信号。
  • 页面级别(RouteAware):当前页面是否处于导航栈顶的可见状态。
  • 组件级别(Composition):Widget 进入或退出 UI 树的过程。

将这些信号与 UI 描述彻底解耦,正是 Flutter 极致灵活的原因。

Flutter 方案:监听 AppLifecycleState

在 Flutter 中,需要通过 WidgetsBindingObserver 来扮演那个“哨兵”的角色。

WidgetsBindingObserver 的订阅是 State 级别的,而不是 Widget 级别的。

// 建议替换原本的 Flutter 代码块
class MyState extends State<MyWidget> with WidgetsBindingObserver {

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

  @override
  void didChangeAppLifecycleState(AppLifecycleState state) {
    // 严谨判断:只有 App 回到前台,且当前页面正处于栈顶可见时,才触发逻辑
    if (state == AppLifecycleState.resumed && ModalRoute.of(context)?.isCurrent == true) {
      _refreshData();
    }
  }

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

Compose 方案:通过副作用显式接入宿主生命周期

Compose 并没有“移除” onResume,而是拒绝让 UI 组件隐式地持有它。

@Composable
fun OnResumeEffect(onResume: () -> Unit) {
    val lifecycleOwner = LocalLifecycleOwner.current
    LaunchedEffect(lifecycleOwner) {
        lifecycleOwner.lifecycle.repeatOnLifecycle(Lifecycle.State.RESUMED) {
            onResume()
        }
    }
}

结语:从“被动接受”到“主动订阅”

从原生 Android 转向 Flutter 或 Compose,最难的不是学习新的语法,而是思维权力的移交。

在传统的 View 体系中,生命周期是系统强行塞给我们的“全家桶”,我们作为开发者,更多是在被动地接受 Activity 的调度。而声明式 UI 的出现,将组件彻底从繁重的系统环境依赖中解放出来。

这种“消亡”实际上是一种进化:

  • 它让我们更关注数据本身:UI 不再是长存的“房子”,而是随数据流动的“快照”。

  • 它赋予了我们订阅的自由:不再为了那 1% 的业务需求让 100% 的组件去负担生命周期回调,而是根据业务逻辑,在需要的地方精准地拉出一根“信号线”。

生命周期没有消失,只是从“隐式回调”进化为了“显式订阅”。


在以下平台分享此文章:

评论


上一篇
定制控制器, 将米家1代屏幕挂灯接入HomeKit
下一篇
联想拯救者 R720 键盘矩阵分析与改造