鸿蒙Flutter实战:MultiProvider多状态管理架构实践

前言

Flutter 的状态管理方案琳琅满目------BLoC、Riverpod、Redux、GetX......但对于一个中小型应用,Provider 的"刚好够用"哲学往往是最务实的选择。它没有 BLoC 的样板代码地狱,也没有 Riverpod 的概念曲线,5 分钟就能上手。

鸿蒙 Flutter 备忘录应用使用 MultiProvider + 4 个 ChangeNotifier 管理整个应用的状态。本文拆解这个架构的设计思路、常见陷阱和最佳实践。

项目仓库:todo_flutter_harmony

为什么选 Provider

方案 学习成本 代码量 鸿蒙兼容性 适合场景
Provider ✅ 纯 Dart 中小应用
BLoC ✅ 纯 Dart 大型应用/团队
Riverpod ✅ 纯 Dart 中大型应用
GetX ⚠️ 框架耦合 快速原型
Redux ✅ 纯 Dart 复杂状态

对于这个备忘录应用(4 个数据实体,10 个页面),Provider 是最合适的------学习成本低,鸿蒙天然兼容,且社区庞大。

应用入口:MultiProvider 注册

dart 复制代码
// main.dart
void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MultiProvider(
      providers: [
        ChangeNotifierProvider(create: (_) => MemoProvider()),
        ChangeNotifierProvider(create: (_) => TodoProvider()),
        ChangeNotifierProvider(create: (_) => DiaryProvider()),
        ChangeNotifierProvider(create: (_) => CategoryProvider()),
      ],
      child: const App(),
    );
  }
}

MultiProvider 的嵌套顺序不影响功能------这 4 个 Provider 之间没有依赖关系,它们各自独立。

如果 Provider 之间有依赖(比如 MemoProvider 需要 CategoryProvider 的数据),可以使用 ProxyProvider

dart 复制代码
ChangeNotifierProxyProvider<CategoryProvider, MemoProvider>(
  create: (_) => MemoProvider(),
  update: (_, categoryProvider, memoProvider) {
    memoProvider!.updateCategoryProvider(categoryProvider);
    return memoProvider;
  },
)

但在本应用中,每个 Provider 都直接访问 DatabaseHelper 单例,没有相互依赖。

MemoProvider 完整实现

dart 复制代码
class MemoProvider extends ChangeNotifier {
  final DatabaseHelper _db = DatabaseHelper.instance;
  List<Memo> _allMemos = [];
  bool _isSelectionMode = false;
  final Set<int> _selectedIds = {};
  int? _categoryFilter;
  String _searchQuery = '';

  // ====== Getters ======
  List<Memo> get allMemos => List.unmodifiable(_allMemos);
  bool get isSelectionMode => _isSelectionMode;
  Set<int> get selectedIds => Set.unmodifiable(_selectedIds);
  int get selectedCount => _selectedIds.length;

  List<Memo> get filteredMemos {
    var result = List<Memo>.from(_allMemos);

    if (_categoryFilter != null) {
      result = result.where((m) => m.categoryId == _categoryFilter).toList();
    }

    if (_searchQuery.isNotEmpty) {
      final q = _searchQuery.toLowerCase();
      result = result.where((m) =>
        m.title.toLowerCase().contains(q) ||
        m.content.toLowerCase().contains(q)
      ).toList();
    }

    result.sort((a, b) {
      if (a.isPinned != b.isPinned) return a.isPinned ? -1 : 1;
      return b.createdAt.compareTo(a.createdAt);
    });

    return result;
  }

  // ====== 数据加载 ======
  Future<void> loadMemos() async {
    _allMemos = await _db.getAllMemos();
    notifyListeners();
  }

  // ====== CRUD ======
  Future<void> addMemo(Memo memo) async {
    await _db.insertMemo(memo);
    await loadMemos();
  }

  Future<void> updateMemo(Memo memo) async {
    await _db.updateMemo(memo);
    await loadMemos();
  }

  Future<void> deleteMemo(int id) async {
    await _db.deleteMemo(id);
    await loadMemos();
  }

  // ... 选择模式、搜索、筛选等其他方法
}

三种 Consumer 模式的选用

context.watch:监听变化

dart 复制代码
Consumer<MemoProvider>(
  builder: (context, provider, _) {
    return Text('共 ${provider.allMemos.length} 条备忘录');
  },
)

等价写法:

dart 复制代码
// 在 build 方法中
final provider = context.watch<MemoProvider>();
return Text('共 ${provider.allMemos.length} 条备忘录');

context.watch 会注册一个 listener,当 Provider 调用 notifyListeners() 时,widget 自动重建。

context.read:一次性读取

dart 复制代码
FloatingActionButton(
  onPressed: () {
    // 读取但不监听------适合事件处理
    context.read<MemoProvider>().addMemo(newMemo);
  },
  child: const Icon(Icons.add),
)

context.read 不注册 listener ------只在当前时刻读取 Provider,不会导致 widget 重建。适合在 onPressedonTap 等事件回调中使用。

context.select:精确订阅

dart 复制代码
// 只监听 memo 数量变化,不关心内容变化
final count = context.select<MemoProvider, int>(
  (provider) => provider.allMemos.length,
);

context.select 只在 selector 返回值变化时重建。当 Provider 通知了变化但 memo 数量没变时,这个 widget 不重建------避免不必要的渲染。

IndexedStack + 页面数据加载

应用使用 IndexedStack 保持 4 个 tab 的页面状态:

dart 复制代码
class HomePage extends StatefulWidget {
  @override
  State<HomePage> createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  int _currentIndex = 0;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: IndexedStack(
        index: _currentIndex,
        children: const [
          MemoListPage(),
          TodoListPage(),
          DiaryListPage(),
          StatsPage(),
        ],
      ),
      bottomNavigationBar: NavigationBar(
        selectedIndex: _currentIndex,
        onDestinationSelected: (index) {
          setState(() => _currentIndex = index);
        },
        destinations: const [
          NavigationDestination(icon: Icon(Icons.note_alt_outlined), label: '备忘录'),
          NavigationDestination(icon: Icon(Icons.checklist_outlined), label: '待办'),
          NavigationDestination(icon: Icon(Icons.book_outlined), label: '日记'),
          NavigationDestination(icon: Icon(Icons.bar_chart), label: '统计'),
        ],
      ),
    );
  }
}

数据在各页面的 initState 中通过 addPostFrameCallback 加载:

dart 复制代码
@override
void initState() {
  super.initState();
  WidgetsBinding.instance.addPostFrameCallback((_) {
    context.read<MemoProvider>().loadMemos();
  });
}

addPostFrameCallback 确保首帧已渲染后再触发数据加载,避免在 build 阶段调用 Provider 方法(Flutter 禁止在 build 中修改状态)。

await 后的 mounted 检查

dart 复制代码
Future<void> _deleteMemo(int id) 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('删除')),
      ],
    ),
  );

  // 关键:await 后检查 mounted
  if (!mounted) return;
  if (confirmed != true) return;

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

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

showDialog 返回一个 Future,在 await 期间用户可能按了系统返回键导航离开,此时 mounted == false。之后任何调用 setStatecontext 的操作都会抛出异常。每个 await 后检查 mounted 是最基本的防御性编程。

鸿蒙兼容性

Provider 是纯 Dart 包,不包含任何原生代码。它仅依赖 Flutter 的 InheritedWidget 机制和 Dart 的 ChangeNotifier 模式,在 Android、iOS、鸿蒙 OHOS 上行为完全一致。

总结

Provider 状态管理架构的关键决策:

  1. MultiProvider 注册 4 个 ChangeNotifier:各司其职,无相互依赖
  2. context.watch / read / select 三模式:按场景选择监听策略
  3. addPostFrameCallback 触发初始加载:避免在 build 中修改状态
  4. 每个 await 后检查 mounted:防止异步回调中的 widget 已 dispose 崩溃

完整项目代码见:todo_flutter_harmony

相关推荐
不羁的木木10 小时前
HarmonyOS防窥保护实战:3步接入Device Security Kit保护用户隐私
华为·harmonyos
我是一只码蚁10 小时前
记一次苍穹外卖项目 Maven 编译报错的排查与解决全过程
java·经验分享·笔记·后端·架构·maven
深念Y10 小时前
DeepSeek/MiMo 推理链缓存代理:从内存到 SQLite 的两级缓存架构实战
数据库·缓存·架构·sqlite·内存·优化·分层
中国云报10 小时前
百年名校焕新光智底座,华为“领航”光智共融
华为
heimeiyingwang10 小时前
【架构实战】分布式ID生成方案:雪花算法与业务ID设计
分布式·算法·架构
光泽雨10 小时前
ADO.NET 进阶知识与实战坑位深度解析
性能优化·架构·.net
嗝o゚11 小时前
CANN hixl 单边通信库——PD 分离架构下的跨设备通信优化实践
架构·cann·hixl