请添加图片描述

前言

Flutter 开发者迟早会遇到这个红色的错误页面:

setState() called after dispose(): _MemoListPageState#a1b2c(lifecycle state: defunct, not mounted)

翻译成大白话:你在 widget 已经被销毁之后,又试图更新它的状态。这通常发生在异步操作的回调中——用户在你等待网络请求时已经导航离开了,但你的代码还在试图 setState

鸿蒙 Flutter 备忘录应用中,每个异步回调后都有 if (!mounted) return 的防御性检查。本文系统性地梳理这个问题为什么发生、在哪里发生、以及如何避免。

项目仓库:todo_flutter_harmony

为什么 await 后 mounted 可能为 false

Flutter 的 State 对象有生命周期:

createState() → initState() → build() → ... → dispose()

dispose() 被调用后,mounted 变为 false。以下场景都会触发 dispose

  1. 用户按系统返回键:当前页面从导航栈中弹出
  2. Navigator.pop():代码触发的页面关闭
  3. 父 widget 重建且不再包含该子 widget:条件渲染导致 widget 被移除
  4. Tab 切换(不使用 IndexedStack 的情况下):旧 Tab 页面被 dispose

如果在一个 async 函数中 await 了一个 Future,在等待期间上述任意场景发生,mounted 就变成了 false。

Future<void> _saveAndNavigate() async {
  // 假设这个 await 耗 200ms
  await database.insertMemo(memo);

  // 在这 200ms 内,用户可能已经按了返回键
  // 此时 mounted == false

  Navigator.pop(context);  // 如果在 Widget dispose 后调用,会抛异常
}

典型场景一:对话框回调

Future<void> _showDeleteConfirmDialog(int memoId) async {
  final confirmed = await showDialog<bool>(
    context: context,
    builder: (ctx) => AlertDialog(
      title: const Text('确认删除'),
      content: const Text('确定要删除这条备忘录吗?'),
      actions: [
        TextButton(
          onPressed: () => Navigator.pop(ctx, false),
          child: const Text('取消'),
        ),
        TextButton(
          onPressed: () => Navigator.pop(ctx, true),
          child: const Text('删除', style: TextStyle(color: Colors.red)),
        ),
      ],
    ),
  );

  // ⚠️ showDialog 是异步的,用户可能在弹窗显示期间
  //    按系统返回键两次关闭了 page + dialog
  if (!mounted) return;

  if (confirmed != true) return;

  await context.read<MemoProvider>().deleteMemo(memoId);

  // ⚠️ 删除操作也是异步的
  if (!mounted) return;

  ScaffoldMessenger.of(context).showSnackBar(
    const SnackBar(content: Text('已删除')),
  );
}

这里有两个 await 点,每个后面都需要 if (!mounted) return

  1. showDialog 返回后
  2. deleteMemo 完成后(需要 context 来显示 SnackBar)

典型场景二:Navigator 异步返回

Future<void> _navigateToEditPage(Memo memo) async {
  await Navigator.pushNamed(
    context,
    '/memo/edit',
    arguments: memo.id,
  );

  // ⚠️ 用户从编辑页返回后,这个页面可能已经被 dispose 了
  if (!mounted) return;

  // 刷新数据
  context.read<MemoProvider>().loadMemos();
}

这个场景比较微妙——用户正常从编辑页返回通常不会导致当前页面 dispose。但如果在编辑页期间,系统推送了一个通知,用户从通知进入应用的其他页面,当前栈可能会被重建。

典型场景三:FutureBuilder 和 StreamBuilder

FutureBuilder<List<Memo>>(
  future: DatabaseHelper.instance.getAllMemos(),
  builder: (context, snapshot) {
    // ⚠️ 当 Future 完成时,widget 可能已经不再树中
    if (snapshot.connectionState == ConnectionState.done) {
      // 不要在这里调用 Provider 或 Navigator
    }
    return ...;
  },
)

FutureBuilder 的 builder 不需要手动检查 mounted——Flutter 框架内部已经处理了这个情况(当 widget 不在树中时不会调用 builder)。但如果 builder 中有显式的 context 操作(如 Provider.of),仍然可能导致问题。

更好的替代方案:在 initState 中用 addPostFrameCallback 触发数据加载,通过 Provider 响应式更新 UI。

典型场景四:动画完成回调

void _playExitAnimation() {
  _controller.forward().then((_) {
    // ⚠️ 动画期间 widget 可能被 dispose
    if (!mounted) return;
    Navigator.pop(context);
  });
}

AnimationController.forward() 返回一个 TickerFuture,用 .then() 注册回调时,动画可能持续几百毫秒——足够用户导航离开。

Provider 中的安全检查

Provider 的 ChangeNotifier 内部,notifyListeners() 不需要 mounted 检查——因为 ChangeNotifier 不是 widget,没有 lifecycle。但如果 Provider 中操作了 UI 相关的 context,同样需要注意:

class MemoProvider extends ChangeNotifier {
  Future<void> deleteMemoAndNotify(BuildContext? context, int id) async {
    await DatabaseHelper.instance.deleteMemo(id);
    await loadMemos();  // 内部的 notifyListeners() 是安全的

    // context 可能已失效
    if (context != null) {
      // 不推荐:Provider 不应持有 context
      ScaffoldMessenger.of(context).showSnackBar(...);
    }
  }
}

最佳实践:让 UI 层处理 UI 反馈,Provider 只负责数据和状态。

// 在 widget 中
Future<void> _deleteMemo(int id) async {
  await context.read<MemoProvider>().deleteMemo(id);
  if (!mounted) return;
  ScaffoldMessenger.of(context).showSnackBar(
    const SnackBar(content: Text('已删除')),
  );
}

封装一个 MountedGuard

如果每个异步回调都写 if (!mounted) return 觉得繁琐,可以封装一个 helper:

extension MountedGuard on State {
  /// 返回 true 表示安全,false 表示 widget 已 dispose
  bool get isMountedSafe => mounted;

  /// 只在 mounted 时执行回调
  void ifMounted(VoidCallback callback) {
    if (mounted) callback();
  }
}

// 使用
await showDialog(...);
ifMounted(() {
  setState(() => _data = result);
});

不过,这个封装掩藏了检查逻辑,团队成员可能忘记调用。显式写 if (!mounted) return 虽然啰嗦,但因为显眼,反而是一种自我保护——任何一个开发者看到这段代码都知道这里有个异步安全点。

Lint 规则

analysis_options.yaml 中添加:

linter:
  rules:
    - use_build_context_synchronously

这个 lint 规则会在 await 之后直接使用 context 时报 warning,强制开发者在 await 和 context 使用之间插入 mounted 检查。

鸿蒙兼容性

mounted 属性是 Flutter State 类的内置属性,在 Flutter 框架层实现,与平台无关。在鸿蒙 OHOS 上行为与 Android/iOS 完全一致。

总结

异步回调中的 mounted 检查是 Flutter 开发中成本最低、收益最高的防御性编程实践:

  1. 每个 await 后都检查 if (!mounted) return
  2. 特别关注 showDialogNavigator.push、动画完成回调这三个场景
  3. Provider 不持有 context,UI 反馈由 widget 层负责
  4. use_build_context_synchronously lint 规则强制检查

这条规则在鸿蒙 Flutter 备忘录应用的每个页面中都有体现,是整个应用稳定性的基石。

完整项目代码见:todo_flutter_harmony

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐