Skip to content

36 · 状态恢复与进程重建(State Restoration) ​

一句话定位:移动端的"状态恢复"对应前端的刷新/切后台后页面状态丢失问题,但移动端更狠——系统会在后台杀掉整个进程。本篇讲 Flutter 怎么在进程重建后恢复用户看到的位置、输入与滚动状态。

开篇速览 ​

问题答案
前端对应物浏览器刷新丢状态、SPA 路由状态持久化、localStorage 恢复用户偏好
核心概念RestorationMixin + RestorationScope + 可恢复属性(RestorableProperty)
最常见事故App 切后台被杀,回来时白屏/回到首页,用户要重新走流程
一句话红线默认情况下 Flutter 不做任何状态恢复;不显式接入,进程被杀就丢状态

代码来源说明:HaierEnergy 当前 lib/ 未系统接入 RestorationMixin,仅个别页面处理了生命周期回调。 本篇按"通用方案 + 项目接入建议"撰写。


一、先搞清楚"状态为什么丢" ​

移动端与 Web 最大的不同:系统为了省内存会杀掉后台进程。

事件前端(Web)Flutter(移动端)
页面刷新全部内存状态丢失类似,但可恢复框架兜底
切后台被杀少见(一般 Tab 还活着)非常常见,尤其 Android 低内存
返回上一页路由栈还在路由栈由框架维护,进程没了也全丢
配置变更(旋转/深色)无关Activity 重建,也要恢复

心智模型:进程被杀 = 一次"硬刷新"。你写的 State 全部消失,SharedPreferences 里的持久化数据还在。


二、Flutter 的恢复三件套 ​

2.1 RestorationScope 注册表 ​

每个可恢复状态都要挂在一个 RestorationScope 下,Flutter 会把恢复数据序列化存起来:

dart
RestorationScope(
  restorationId: 'device_detail',
  child: const DeviceDetailPage(),
)

2.2 页面级 RestorationMixin ​

dart
class _DeviceDetailPageState extends State<DeviceDetailPage>
    with RestorationMixin {
  @override
  String? get restorationId => 'device_detail';

  // 用 Restorable 属性替代普通字段,才能参与恢复
  final RestorableInt _selectedTab = RestorableInt(0);
  final RestorableStringN _deviceId = RestorableStringN(null);

  @override
  void restoreState(RestorationBucket? oldBucket, bool initialRestore) {
    registerForRestoration(_selectedTab, 'selected_tab');
    registerForRestoration(_deviceId, 'device_id');
  }

  @override
  void dispose() {
    _selectedTab.dispose();
    _deviceId.dispose();
    super.dispose();
  }
}

2.3 RestorableProperty 家族 ​

类型存的量典型用途
RestorableInt / RestorableDouble数值当前 Tab、滚动位置
RestorableString / RestorableStringN字符串搜索词、选中设备 ID
RestorableBool布尔开关态
RestorableTextEditingController输入框输入内容(最常被要求恢复)
RestorableScrollOffset滚动列表回到上次位置
RestorableRouteFuture路由恢复到"正在打开的子页面"

三、和 Riverpod 的关系(项目关键) ​

HaierEnergy 用 Riverpod 管理状态。Riverpod 的 Provider 状态默认存在内存里,进程被杀就没了,它不负责恢复。正确的分层:

状态类型放哪恢复方式
纯内存瞬时态(下拉菜单开合)Riverpod / 局部 State不需要恢复
用户可见关键态(Tab、输入、滚动)RestorationMixin走 RestorationScope
必须跨会话保留的数据(登录态、用户配置)本地存储(见 18 篇)启动时重新加载
网络数据Provider启动后重新拉取(配合缓存)

推荐组合:

  1. 登录态/用户信息 → SharedPreferences / 安全存储,启动时先读再请求刷新;
  2. 页面内"正在干嘛" → RestorationMixin 恢复;
  3. 网络列表 → 走 Provider 重新请求,可用 12 篇 的分页缓存机制。

四、生命周期钩子:didChangeAppLifecycleState ​

进程被杀前你还有机会"善后"。用 WidgetsBindingObserver 监听切后台,把关键进度主动持久化:

dart
class AppLifecycleObserver with WidgetsBindingObserver {
  @override
  void didChangeAppLifecycleState(AppLifecycleState state) {
    if (state == AppLifecycleState.paused ||
        state == AppLifecycleState.detached) {
      // 切后台:把用户草稿、当前步骤写入本地存储
      draftRepository.savePendingDraft();
    }
  }
}

经验:状态恢复不要只依赖框架的自动序列化,对"写了一半的表单"这类高价值数据,切后台时主动落盘一次最可靠。


五、哪些页面值得恢复(优先级) ​

页面类型是否值得原因
长表单/多步流程⭐⭐⭐用户可能填到一半被杀
详情页/设备页⭐⭐⭐用户要回到"上次看的那台设备"
带 Tab 的列表页⭐⭐恢复选中 Tab + 滚动位置体验提升明显
首屏/纯静态页⭐恢复成本 > 收益

六、测试恢复逻辑 ​

dart
testWidgets('进程重建后恢复选中 Tab', (tester) async {
  // 模拟保存恢复数据
  final bucket = RestorationBucket.empty(owner: RootRestorationScope.of(tester.element(find.byType(MyApp))));

  // 重新 pump 应用并恢复
  await tester.pumpWidget(const MyApp(restorationId: 'root'));
  await tester.pump();

  expect(find.byKey(const Key('tab_2_selected')), findsOneWidget);
});

恢复相关的 bug 很隐蔽,建议把"杀进程 → 重启 → 回到原位置"作为发布前冒烟用例。


动手练习 ​

  1. 给项目里的设备详情页加 RestorationMixin,恢复"选中的 Tab + 设备 ID";
  2. 给一个多步表单加 RestorableTextEditingController,验证输入内容在重建后还在;
  3. 用 didChangeAppLifecycleState 在切后台时把草稿写入本地,重启后提示"继续上次编辑"。

自检清单 ​

  • [ ] 能说清"进程被杀 = 硬刷新",与前端刷新丢状态的区别
  • [ ] 会用 RestorationMixin + RestorableProperty 恢复页面关键态
  • [ ] 知道 Riverpod 状态与 Restoration 的分工边界
  • [ ] 会评估"哪些页面值得恢复"
  • [ ] 知道切后台主动持久化的兜底手段

常见误区 ​

  • "Riverpod 能帮我恢复":Provider 状态只在内存,进程被杀就没了;
  • "加了 RestorationMixin 就全恢复了":普通字段不会恢复,只有 RestorableProperty 会;
  • "滚动位置无所谓":长列表用户回白屏的挫败感极强,滚动恢复性价比最高;
  • "只做 Android":iOS 同样会杀进程,双端都要处理。

相关文档 ​