Flutter MVVM实战:用Provider和Riverpod分别重构一个Todo App,聊聊我的选择
2026/6/14 23:39:56 网站建设 项目流程

Flutter MVVM实战:Provider与Riverpod的Todo App重构对比

当Flutter开发者面临状态管理方案选择时,Provider和Riverpod总是出现在候选名单的前列。我在最近一个Todo应用的重构过程中,分别用这两种方案实现了完整的MVVM架构。本文将分享从项目初始化到功能完成的完整对比体验,包括代码组织差异、响应式机制实现、测试便利性等实战细节。

1. 项目背景与架构设计

Todo应用作为经典案例,包含了状态管理的典型需求:列表展示、增删改查操作和状态联动。MVVM架构将业务逻辑与UI解耦的特性,正好匹配这类交互密集型应用的需求。

基础架构组件对比

组件Provider实现方案Riverpod实现方案
Model层纯Dart类纯Dart类(与Provider方案一致)
ViewModel层ChangeNotifier的子类StateNotifier的子类
状态通知机制notifyListeners()调用state属性更新
依赖注入MultiProvider层级结构ProviderScope全局容器

在项目初始化阶段,Riverpod的setup明显更简洁。不需要在Widget树中层层包裹Provider,只需在根节点使用单个ProviderScope:

void main() { runApp(ProviderScope(child: MyApp())); }

而Provider方案则需要维护复杂的Provider树:

void main() { runApp( MultiProvider( providers: [ ChangeNotifierProvider(create: (_) => TodoViewModel()), ], child: MyApp(), ), ); }

2. ViewModel实现细节对比

ViewModel作为MVVM架构的核心枢纽,两种方案的实现差异直接影响代码结构和可维护性。

2.1 Provider方案实现

基于ChangeNotifier的ViewModel需要手动管理状态通知:

class TodoViewModel extends ChangeNotifier { final List<TodoItem> _todos = []; List<TodoItem> get todos => _todos; void addTodo(String title) { _todos.add(TodoItem(title: title)); notifyListeners(); // 必须显式调用 } // 其他操作方法... }

典型痛点

  • 容易忘记调用notifyListeners()
  • 状态变更逻辑分散在各个方法中
  • 需要手动维护不可变状态

2.2 Riverpod方案实现

StateNotifier提供了更结构化的状态管理:

class TodoViewModel extends StateNotifier<List<TodoItem>> { TodoViewModel() : super([]); void addTodo(String title) { state = [...state, TodoItem(title: title)]; // 自动触发更新 } // 其他操作方法... }

优势对比

  • 状态变更通过state赋值自动通知
  • 强制使用不可变状态模式
  • 所有状态变更集中处理

实际开发中发现,Riverpod的状态不可变性要求虽然增加了初期学习成本,但显著减少了由意外状态修改引发的bug。

3. 视图层集成对比

两种方案在UI层的集成方式体现了不同的设计哲学。

3.1 组件状态订阅

Provider使用Consumer或context.watch:

Consumer<TodoViewModel>( builder: (context, viewModel, child) { return ListView.builder( itemCount: viewModel.todos.length, itemBuilder: (context, index) => TodoItem( item: viewModel.todos[index] ), ); }, )

Riverpod则通过watch函数实现:

final viewModel = ref.watch(todoViewModelProvider); return ListView.builder( itemCount: viewModel.length, itemBuilder: (context, index) => TodoItem( item: viewModel[index] ), );

3.2 操作触发方式

Provider需要通过context读取实例:

onPressed: () { context.read<TodoViewModel>().addTodo('New item'); }

Riverpod则通过provider.notifier:

onPressed: () { ref.read(todoViewModelProvider.notifier).addTodo('New item'); }

性能考量

  • Riverpod的编译时安全机制可以避免常见的provider查找错误
  • Provider的context依赖在大型项目可能导致widget重建范围过大

4. 测试与可维护性对比

在编写单元测试时,两种方案的差异更加明显。

4.1 Provider测试方案

test('should add todo item', () { final viewModel = TodoViewModel(); viewModel.addTodo('Test item'); expect(viewModel.todos.length, 1); });

需要额外处理ChangeNotifier的监听机制。

4.2 Riverpod测试方案

test('should add todo item', () { final container = ProviderContainer(); addTearDown(container.dispose); container.read(todoViewModelProvider.notifier).addTodo('Test item'); expect(container.read(todoViewModelProvider).length, 1); });

测试优势

  • 完整的依赖注入环境
  • 可以模拟整个provider层次
  • 状态变更更易断言

在实现撤销/重做功能时,Riverpod的不可变状态模式展现出更大优势。只需维护状态历史列表即可:

class TodoViewModel extends StateNotifier<List<TodoItem>> { final List<List<TodoItem>> _history = []; void addTodo(String title) { _history.add(state); state = [...state, TodoItem(title: title)]; } void undo() { if (_history.isNotEmpty) { state = _history.removeLast(); } } }

5. 开发体验与决策建议

经过完整项目实践,两种方案各有适用场景:

选择Provider当

  • 项目已使用Provider生态
  • 需要快速上手简单项目
  • 团队熟悉Flutter传统模式

选择Riverpod当

  • 项目需要长期维护扩展
  • 重视编译时安全
  • 需要更灵活的状态组合
  • 考虑未来迁移到Flutter 3.0+

实际开发中,Riverpod的以下特性显著提升了效率:

  • 自动dispose管理
  • 家族参数支持
  • 更细粒度的rebuild控制
  • 与freezed的结合使用
// 配合freezed实现极致类型安全 @freezed class TodoState with _$TodoState { factory TodoState({ required List<TodoItem> items, required FilterType filter, }) = _TodoState; } final todoViewModelProvider = StateNotifierProvider.autoDispose< TodoViewModel, TodoState >((ref) => TodoViewModel());

对于新启动的中大型项目,Riverpod的综合优势已经非常明显。但在小规模原型开发中,Provider的简洁性仍然具有吸引力。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询