鸿蒙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

相关推荐
Magic-ZYJ13 分钟前
隐私优先 HarmonyOS 应用怎么设计:无账号、无后台、无统计 SDK
华为·harmonyos·鸿蒙·独立开发者·心晴手记
独守一片天15 分钟前
鸿蒙车机手机协同服务生态
华为·智能手机·harmonyos
ZJU_统一阿萨姆17 分钟前
【推理优化进阶】通信关键路径:NCCL、RDMA 与计算通信重叠
开发语言·人工智能·语言模型·架构·系统架构
m0_5873830032 分钟前
智慧场馆解决方案实战指南:从系统架构到落地部署全解析
java·spring boot·架构·系统架构
2501_919749031 小时前
华为鸿蒙测手速APP—小羊手速
华为·harmonyos·鸿蒙
ZYJCSZKJ1 小时前
AI数字人直播系统的技术架构与多方言交互实现方案——基于广西区域商业场景的工程实践
人工智能·架构·交互
Keystone_Onion1 小时前
跨境电商架构演进:基于美国海外仓的中大件分布式仓网路由优化方案
分布式·架构
无凭2 小时前
DeepSeek Harness 为什么需要 Cordis:从 Everything is Plugin 开始看
人工智能·架构
智购科技自动贩卖机2 小时前
自动售货机硬件主控方案选型实战:单片机、树莓派、ESP32怎么选?
人工智能·单片机·嵌入式硬件·物联网·网络协议·yolo·架构
Erishen2 小时前
🤖 用 AutoGen 搭 PSE 三角色闭环:可重试、可追溯、可审计的多智能体框架
架构·开源·agent