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

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

在原生Android开发的世界, 创建模版项目后就会得到一个MainActivity和activity_main.xml.
我们习惯了在MainActivity里去绑定ButtonText再设置各种属性. 他们都是内存中看得见,摸得着的View.
但是到了Flutter中, 得到的了却是main()函数和一堆StatelessWidgetStatefulWidget.
不仅仅是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 级别的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 建议替换原本的 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 组件隐式地持有它。

1
2
3
4
5
6
7
8
9
@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% 的组件去负担生命周期回调,而是根据业务逻辑,在需要的地方精准地拉出一根“信号线”。

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

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

https://chaosgoo.com/2024/01/02/the-death-of-onresume-declarative-ui-lifecycle/

作者

Chaos Goo

发布于

2024-01-02

更新于

2024-01-02

许可协议