Flutter + Riverpod +MVI 架构下的现代状态管理

目录

  • [1. Riverpod 简介](#1. Riverpod 简介 "#1-riverpod-%E7%AE%80%E4%BB%8B")
  • [2. 为什么选择 Riverpod](#2. 为什么选择 Riverpod "#2-%E4%B8%BA%E4%BB%80%E4%B9%88%E9%80%89%E6%8B%A9-riverpod")
  • [3. MVI 架构在 Flutter 中的实现](#3. MVI 架构在 Flutter 中的实现 "#3-mvi-%E6%9E%B6%E6%9E%84%E5%9C%A8-flutter-%E4%B8%AD%E7%9A%84%E5%AE%9E%E7%8E%B0")
  • [4. Provider 类型选择指南](#4. Provider 类型选择指南 "#4-provider-%E7%B1%BB%E5%9E%8B%E9%80%89%E6%8B%A9%E6%8C%87%E5%8D%97")
  • [5. 状态管理最佳实践](#5. 状态管理最佳实践 "#5-%E7%8A%B6%E6%80%81%E7%AE%A1%E7%90%86%E6%9C%80%E4%BD%B3%E5%AE%9E%E8%B7%B5")
  • [6. 代码生成工作流](#6. 代码生成工作流 "#6-%E4%BB%A3%E7%A0%81%E7%94%9F%E6%88%90%E5%B7%A5%E4%BD%9C%E6%B5%81")
  • [7. 测试策略](#7. 测试策略 "#7-%E6%B5%8B%E8%AF%95%E7%AD%96%E7%95%A5")
  • [8. 常见问题和解决方案](#8. 常见问题和解决方案 "#8-%E5%B8%B8%E8%A7%81%E9%97%AE%E9%A2%98%E5%92%8C%E8%A7%A3%E5%86%B3%E6%96%B9%E6%A1%88")

1. Riverpod 简介

1.1 什么是 Riverpod?

Riverpod 是 Flutter 生态系统中的一个强大的状态管理库,由 Provider 的作者 Remi Rousselet 开发。它是 Provider 的完全重写版本,解决了 Provider 的诸多限制。

核心特性

  • 编译时安全:在编译时捕获错误,而非运行时
  • 无需 BuildContext:可以在任何地方读取 Provider
  • 支持多个相同类型的 Provider:不受类型限制
  • 代码生成支持 :使用 riverpod_annotation 简化代码
  • 测试友好:轻松模拟和覆盖 Provider
  • 性能优化:精确的依赖追踪,最小化重建

1.2 Riverpod vs Provider vs Bloc

特性 Riverpod Provider Bloc
学习曲线 中等 简单 陡峭
类型安全 ✅ 强 ⚠️ 中等 ✅ 强
样板代码 少(使用代码生成)
测试性 ✅ 优秀 ⚠️ 一般 ✅ 优秀
BuildContext 依赖 ❌ 不需要 ✅ 需要 ❌ 不需要
异步支持 ✅ 原生支持 ⚠️ 需要额外处理 ✅ 原生支持
社区支持 ✅ 活跃 ✅ 活跃 ✅ 活跃

1.3 核心概念

Provider

Provider 是 Riverpod 的基本单元,用于封装一段状态并允许其他部分访问。

dart 复制代码
// 简单的 Provider - 提供不变的值
final nameProvider = Provider<String>((ref) => 'John Doe');

// 使用
final name = ref.watch(nameProvider); // 'John Doe'

Ref

Ref 对象用于与 Provider 交互,有三个主要方法:

  • watch: 监听 Provider 并在其变化时重建
  • read: 读取 Provider 的值但不监听
  • listen: 监听 Provider 但不重建 Widget

ProviderScope

应用的根节点,使 Riverpod 在整个应用中可用。

dart 复制代码
void main() {
  runApp(
    const ProviderScope(
      child: MyApp(),
    ),
  );
}

2. 为什么选择 Riverpod

2.1 第一性原理视角

从 Flutter 的第一性原理出发,状态管理的本质是解决状态归属和状态传递问题

  1. 状态归属:谁拥有这个状态?谁负责更新它?
  2. 状态传递:如何将状态传递给需要它的 Widget?
  3. 状态变更:状态变化时,如何通知依赖它的 Widget?

Riverpod 通过以下方式优雅地解决这些问题:

dart 复制代码
// 1. 状态归属 - Provider 拥有状态
@riverpod
class Counter extends _$Counter {
  @override
  int build() => 0; // 初始状态
  
  void increment() => state++; // 状态更新逻辑
}

// 2. 状态传递 - 无需 BuildContext,全局可访问
final count = ref.watch(counterProvider);

// 3. 状态变更 - 自动通知依赖的 Widget
ref.read(counterProvider.notifier).increment();

2.2 与 MVI 架构的完美契合

MVI(Model-View-Intent)架构强调单向数据流

scss 复制代码
Intent (用户操作) → Model (状态更新) → View (UI 重建)

Riverpod 天然支持这种模式:

dart 复制代码
// Model - 状态定义
@riverpod
class TodoList extends _$TodoList {
  @override
  List<Todo> build() => [];
  
  // Intent - 用户意图处理
  void addTodo(String description) {
    state = [...state, Todo(id: uuid.v4(), description: description)];
  }
}

// View - UI 订阅状态
class TodosView extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final todos = ref.watch(todoListProvider); // 订阅 Model
    
    return ListView.builder(
      itemCount: todos.length,
      itemBuilder: (context, index) => Text(todos[index].description),
    );
  }
}

2.3 实际项目收益

基于我们的实践经验,使用 Riverpod + MVI 架构带来的收益:

  • 开发效率提升 40%:代码生成减少样板代码,清晰的架构加速开发
  • Bug 减少 50%:编译时安全和单向数据流减少状态管理错误
  • 测试覆盖率提升至 80%+:Provider 易于测试和模拟
  • 新成员上手时间缩短至 2 天:统一的模式和清晰的文档

3. MVI 架构在 Flutter 中的实现

3.1 MVI 架构概述

MVI 架构将应用分为三个核心部分:

  1. Model(模型):应用的状态,不可变数据结构
  2. View(视图):UI 展示,订阅 Model 并渲染
  3. Intent(意图):用户操作,触发 Model 更新

3.2 使用 Riverpod 实现 MVI

Model 层:不可变状态

dart 复制代码
// lib/features/todos/domain/models/todo.dart

import 'package:flutter/foundation.dart';

/// Todo 模型 - 不可变数据结构
@immutable
class Todo {
  const Todo({
    required this.id,
    required this.description,
    this.completed = false,
  });

  final String id;
  final String description;
  final bool completed;

  /// 创建副本(不可变对象的状态更新方式)
  Todo copyWith({
    String? id,
    String? description,
    bool? completed,
  }) {
    return Todo(
      id: id ?? this.id,
      description: description ?? this.description,
      completed: completed ?? this.completed,
    );
  }

  @override
  bool operator ==(Object other) =>
      identical(this, other) ||
      other is Todo &&
          runtimeType == other.runtimeType &&
          id == other.id &&
          description == other.description &&
          completed == other.completed;

  @override
  int get hashCode => id.hashCode ^ description.hashCode ^ completed.hashCode;

  @override
  String toString() => 'Todo(id: $id, description: $description, completed: $completed)';
}

Intent 层:使用 Notifier 处理意图

dart 复制代码
// lib/features/todos/presentation/providers/todo_list_provider.dart

import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:uuid/uuid.dart';
import '../../domain/models/todo.dart';

part 'todo_list_provider.g.dart';

const _uuid = Uuid();

/// TodoList Provider - 处理所有与 Todo 列表相关的意图
@riverpod
class TodoList extends _$TodoList {
  /// 初始化状态
  @override
  List<Todo> build() => [
    const Todo(id: 'todo-0', description: '学习 Riverpod'),
    const Todo(id: 'todo-1', description: '实践 MVI 架构'),
    const Todo(id: 'todo-2', description: '编写单元测试'),
  ];

  /// Intent: 添加新 Todo
  void add(String description) {
    state = [
      ...state,
      Todo(id: _uuid.v4(), description: description),
    ];
  }

  /// Intent: 切换完成状态
  void toggle(String id) {
    state = [
      for (final todo in state)
        if (todo.id == id)
          todo.copyWith(completed: !todo.completed)
        else
          todo,
    ];
  }

  /// Intent: 编辑描述
  void edit({required String id, required String description}) {
    state = [
      for (final todo in state)
        if (todo.id == id)
          todo.copyWith(description: description)
        else
          todo,
    ];
  }

  /// Intent: 删除 Todo
  void remove(String id) {
    state = state.where((todo) => todo.id != id).toList();
  }
}

View 层:订阅状态并渲染

dart 复制代码
// lib/features/todos/presentation/todos_screen.dart

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'providers/todo_list_provider.dart';

/// Todos 视图 - 订阅状态并渲染 UI
class TodosScreen extends ConsumerWidget {
  const TodosScreen({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    // 订阅 TodoList 状态
    final todos = ref.watch(todoListProvider);
    final controller = TextEditingController();

    return Scaffold(
      appBar: AppBar(title: const Text('Todos - MVI Example')),
      body: Column(
        children: [
          // 输入框
          Padding(
            padding: const EdgeInsets.all(16.0),
            child: TextField(
              controller: controller,
              decoration: const InputDecoration(
                labelText: 'What needs to be done?',
                border: OutlineInputBorder(),
              ),
              onSubmitted: (value) {
                if (value.isNotEmpty) {
                  // 发送 Intent
                  ref.read(todoListProvider.notifier).add(value);
                  controller.clear();
                }
              },
            ),
          ),
          
          // Todo 列表
          Expanded(
            child: ListView.builder(
              itemCount: todos.length,
              itemBuilder: (context, index) {
                final todo = todos[index];
                return ListTile(
                  leading: Checkbox(
                    value: todo.completed,
                    onChanged: (_) {
                      // 发送 Intent
                      ref.read(todoListProvider.notifier).toggle(todo.id);
                    },
                  ),
                  title: Text(
                    todo.description,
                    style: TextStyle(
                      decoration: todo.completed
                          ? TextDecoration.lineThrough
                          : null,
                    ),
                  ),
                  trailing: IconButton(
                    icon: const Icon(Icons.delete),
                    onPressed: () {
                      // 发送 Intent
                      ref.read(todoListProvider.notifier).remove(todo.id);
                    },
                  ),
                );
              },
            ),
          ),
        ],
      ),
    );
  }
}

3.3 数据流图解

scss 复制代码
┌─────────────────────────────────────────────────────────┐
│                     MVI 数据流                           │
└─────────────────────────────────────────────────────────┘

用户操作 (点击按钮)
    │
    ▼
Intent (ref.read().notifier.add())
    │
    ▼
Model 更新 (state = [...state, newTodo])
    │
    ▼
通知订阅者 (notifyListeners)
    │
    ▼
View 重建 (ref.watch() 触发)
    │
    ▼
UI 更新 (显示新 Todo)

4. Provider 类型选择指南

4.1 Provider 类型概览

Riverpod 提供多种 Provider 类型,每种都有特定的使用场景:

Provider 类型 使用场景 状态可变性 示例
Provider 提供不变的值或计算派生状态 不可变 配置、计算属性
StateProvider 简单的可变状态 可变 过滤器、主题切换
NotifierProvider 复杂的可变状态(推荐) 可变 业务逻辑、CRUD 操作
FutureProvider 异步数据获取 不可变 API 调用
StreamProvider 流式数据 不可变 WebSocket、实时数据

4.2 使用代码生成(推荐)

使用 @riverpod 注解,Riverpod 会自动选择合适的 Provider 类型:

dart 复制代码
import 'package:riverpod_annotation/riverpod_annotation.dart';

part 'my_provider.g.dart';

// 1. 同步数据 → NotifierProvider
@riverpod
class Counter extends _$Counter {
  @override
  int build() => 0;
  
  void increment() => state++;
}

// 2. 异步数据 → FutureProvider
@riverpod
Future<User> user(UserRef ref, String userId) async {
  final repository = ref.watch(userRepositoryProvider);
  return repository.fetchUser(userId);
}

// 3. 流式数据 → StreamProvider
@riverpod
Stream<Message> messages(MessagesRef ref) {
  final websocket = ref.watch(websocketProvider);
  return websocket.messageStream;
}

// 4. 计算派生 → Provider
@riverpod
int doubleCounter(DoubleCounterRef ref) {
  final count = ref.watch(counterProvider);
  return count * 2;
}

4.3 详细使用场景

场景 1:简单计数器(NotifierProvider)

dart 复制代码
@riverpod
class Counter extends _$Counter {
  @override
  int build() => 0;

  void increment() => state++;
  void decrement() => state--;
  void reset() => state = 0;
}

// 使用
class CounterView extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final count = ref.watch(counterProvider);
    
    return Column(
      children: [
        Text('Count: $count'),
        ElevatedButton(
          onPressed: () => ref.read(counterProvider.notifier).increment(),
          child: const Text('Increment'),
        ),
      ],
    );
  }
}

场景 2:API 数据获取(FutureProvider)

dart 复制代码
@riverpod
Future<List<Product>> products(ProductsRef ref) async {
  final api = ref.watch(apiClientProvider);
  return api.fetchProducts();
}

// 使用 - AsyncValue 自动处理 loading/data/error
class ProductsView extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final productsAsync = ref.watch(productsProvider);
    
    return productsAsync.when(
      data: (products) => ListView.builder(
        itemCount: products.length,
        itemBuilder: (context, index) => Text(products[index].name),
      ),
      loading: () => const CircularProgressIndicator(),
      error: (error, stack) => Text('Error: $error'),
    );
  }
}

场景 3:过滤器状态(StateProvider 或 NotifierProvider)

dart 复制代码
// 简单枚举 - 使用 StateProvider
enum TodoFilter { all, active, completed }

final todoFilterProvider = StateProvider<TodoFilter>((ref) => TodoFilter.all);

// 复杂逻辑 - 使用 NotifierProvider
@riverpod
class TodoFilter extends _$TodoFilter {
  @override
  TodoFilterType build() => TodoFilterType.all;
  
  void setFilter(TodoFilterType filter) {
    state = filter;
    // 可以添加额外逻辑,如日志记录
    print('Filter changed to: $filter');
  }
}

场景 4:派生状态(Provider)

dart 复制代码
@riverpod
List<Todo> filteredTodos(FilteredTodosRef ref) {
  final todos = ref.watch(todoListProvider);
  final filter = ref.watch(todoFilterProvider);
  
  return switch (filter) {
    TodoFilter.all => todos,
    TodoFilter.active => todos.where((t) => !t.completed).toList(),
    TodoFilter.completed => todos.where((t) => t.completed).toList(),
  };
}

@riverpod
int uncompletedTodosCount(UncompletedTodosCountRef ref) {
  final todos = ref.watch(todoListProvider);
  return todos.where((t) => !t.completed).length;
}

4.4 选择决策树

scala 复制代码
需要管理什么状态?
│
├─ 不变的值(配置、常量)
│  → Provider<T>
│
├─ 简单可变状态(单个值,无复杂逻辑)
│  ├─ 使用代码生成?
│  │  ├─ 是 → @riverpod class MyState extends _$MyState
│  │  └─ 否 → StateProvider<T>
│  │
│  └─ 复杂逻辑(多个方法、验证等)
│     → @riverpod class MyNotifier extends _$MyNotifier
│
├─ 异步数据
│  ├─ 一次性获取(API 调用)
│  │  → @riverpod Future<T> fetchData(...)
│  │
│  ├─ 流式数据(WebSocket、实时更新)
│  │  → @riverpod Stream<T> dataStream(...)
│  │
│  └─ 需要手动控制加载状态
│     → @riverpod class DataManager extends _$DataManager {
│          AsyncValue<T> state;
│        }
│
└─ 派生/计算状态(依赖其他 Provider)
   → @riverpod T computed(...) { ref.watch(...) }

5. 状态管理最佳实践

5.1 状态设计原则

原则 1:状态最小化

只存储必要的状态,其他数据通过计算派生。

dart 复制代码
// ❌ 错误:存储冗余状态
@riverpod
class BadTodoList extends _$BadTodoList {
  @override
  TodoState build() => TodoState(
    todos: [],
    completedCount: 0,  // 冗余!
    activeCount: 0,     // 冗余!
  );
}

// ✅ 正确:只存储必要状态
@riverpod
class TodoList extends _$TodoList {
  @override
  List<Todo> build() => [];
}

// 派生状态
@riverpod
int completedCount(CompletedCountRef ref) {
  final todos = ref.watch(todoListProvider);
  return todos.where((t) => t.completed).length;
}

原则 2:状态不可变

始终创建新对象,而非修改现有对象。

dart 复制代码
// ❌ 错误:直接修改状态
void addTodo(Todo todo) {
  state.add(todo);  // 不会触发更新!
}

// ✅ 正确:创建新列表
void addTodo(Todo todo) {
  state = [...state, todo];
}

原则 3:单一数据源

每个状态只有一个 Provider 负责管理。

dart 复制代码
// ❌ 错误:多个 Provider 管理同一状态
final todoListProvider1 = ...;
final todoListProvider2 = ...;  // 重复!

// ✅ 正确:单一 Provider
final todoListProvider = ...;

// 其他 Provider 通过依赖获取
@riverpod
List<Todo> activeTodos(ActiveTodosRef ref) {
  final todos = ref.watch(todoListProvider);
  return todos.where((t) => !t.completed).toList();
}

5.2 性能优化技巧

技巧 1:精确订阅

只订阅需要的数据,避免不必要的重建。

dart 复制代码
// ❌ 低效:订阅整个列表
class TodoCount extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final todos = ref.watch(todoListProvider);  // 列表变化就重建
    return Text('Count: ${todos.length}');
  }
}

// ✅ 高效:订阅派生的计数
@riverpod
int todoCount(TodoCountRef ref) {
  return ref.watch(todoListProvider).length;
}

class TodoCount extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final count = ref.watch(todoCountProvider);  // 只在计数变化时重建
    return Text('Count: $count');
  }
}

技巧 2:使用 select 精确订阅

dart 复制代码
// 只在特定字段变化时重建
class UserName extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final name = ref.watch(
      userProvider.select((user) => user.name),  // 只订阅 name
    );
    return Text(name);
  }
}

技巧 3:使用 Consumer 局部重建

dart 复制代码
class MyWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        const Text('Static content'),  // 不会重建
        
        // 只有这部分会重建
        Consumer(
          builder: (context, ref, child) {
            final count = ref.watch(counterProvider);
            return Text('Count: $count');
          },
        ),
        
        const Text('More static content'),  // 不会重建
      ],
    );
  }
}

5.3 错误处理模式

模式 1:使用 AsyncValue

dart 复制代码
@riverpod
class UserData extends _$UserData {
  @override
  Future<User> build(String userId) async {
    final api = ref.watch(apiProvider);
    return api.fetchUser(userId);
  }
  
  Future<void> refresh() async {
    // 保持之前的数据,显示加载指示器
    state = const AsyncValue.loading();
    
    // 使用 guard 自动捕获错误
    state = await AsyncValue.guard(() async {
      final api = ref.read(apiProvider);
      return api.fetchUser(userId);
    });
  }
}

// 使用
class UserView extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final userAsync = ref.watch(userDataProvider('123'));
    
    return userAsync.when(
      data: (user) => Text(user.name),
      loading: () => const CircularProgressIndicator(),
      error: (error, stack) => Text('Error: $error'),
    );
  }
}

模式 2:自定义错误状态

dart 复制代码
class DataState<T> {
  const DataState({
    required this.data,
    this.isLoading = false,
    this.error,
  });

  final T? data;
  final bool isLoading;
  final String? error;

  bool get hasData => data != null;
  bool get hasError => error != null;
}

@riverpod
class Products extends _$Products {
  @override
  DataState<List<Product>> build() => const DataState(data: []);
  
  Future<void> load() async {
    state = DataState(data: state.data, isLoading: true);
    
    try {
      final products = await ref.read(apiProvider).fetchProducts();
      state = DataState(data: products);
    } catch (e) {
      state = DataState(data: state.data, error: e.toString());
    }
  }
}

5.4 依赖注入模式

dart 复制代码
// 定义抽象接口
abstract class UserRepository {
  Future<User> fetchUser(String id);
}

// 实现
class ApiUserRepository implements UserRepository {
  @override
  Future<User> fetchUser(String id) async {
    // API 调用
  }
}

// Provider
@riverpod
UserRepository userRepository(UserRepositoryRef ref) {
  return ApiUserRepository();
}

// 使用
@riverpod
Future<User> user(UserRef ref, String userId) async {
  final repository = ref.watch(userRepositoryProvider);
  return repository.fetchUser(userId);
}

// 测试时可以轻松覆盖
final container = ProviderContainer(
  overrides: [
    userRepositoryProvider.overrideWithValue(MockUserRepository()),
  ],
);

6. 代码生成工作流

6.1 设置代码生成

1. 添加依赖

yaml 复制代码
# pubspec.yaml
dependencies:
  flutter_riverpod: ^3.2.1
  riverpod_annotation: ^4.0.2

dev_dependencies:
  build_runner: ^2.4.0
  riverpod_generator: ^3.0.0
  riverpod_lint: ^3.0.0

2. 创建 Provider

dart 复制代码
// lib/features/counter/providers/counter_provider.dart
import 'package:riverpod_annotation/riverpod_annotation.dart';

// 必须包含 part 指令
part 'counter_provider.g.dart';

@riverpod
class Counter extends _$Counter {
  @override
  int build() => 0;
  
  void increment() => state++;
}

3. 运行代码生成

bash 复制代码
# 一次性生成
flutter pub run build_runner build --delete-conflicting-outputs

# 监听模式(开发时推荐)
flutter pub run build_runner watch --delete-conflicting-outputs

6.2 生成的代码解析

dart 复制代码
// counter_provider.g.dart (自动生成)

String _$counterHash() => r'abc123...';

abstract class _$Counter extends BuildlessAutoDisposeNotifier<int> {
  @override
  int build();
}

final counterProvider = AutoDisposeNotifierProvider<Counter, int>.internal(
  Counter.new,
  name: r'counterProvider',
  debugGetCreateSourceHash: _$counterHash,
  dependencies: null,
  allTransitiveDependencies: null,
);

typedef _$Counter = AutoDisposeNotifier<int>;

6.3 代码生成最佳实践

实践 1:使用 part 文件

dart 复制代码
// ✅ 正确
part 'my_provider.g.dart';

// ❌ 错误
import 'my_provider.g.dart';  // 不要使用 import

实践 2:命名约定

dart 复制代码
// Provider 文件命名:<feature>_provider.dart
// 生成文件自动为:<feature>_provider.g.dart

lib/features/
  ├── counter/
  │   └── providers/
  │       ├── counter_provider.dart
  │       └── counter_provider.g.dart  // 自动生成
  └── todos/
      └── providers/
          ├── todo_list_provider.dart
          └── todo_list_provider.g.dart  // 自动生成

实践 3:忽略生成文件

gitignore 复制代码
# .gitignore
*.g.dart
*.freezed.dart

6.4 常见代码生成问题

问题 1:找不到生成的类

dart 复制代码
// 错误信息:Undefined class '_$Counter'

// 解决方案:
// 1. 确保已运行 build_runner
flutter pub run build_runner build

// 2. 检查 part 指令
part 'counter_provider.g.dart';  // 必须存在

// 3. 检查文件名匹配
// counter_provider.dart → counter_provider.g.dart

问题 2:生成文件冲突

bash 复制代码
# 错误信息:Conflicting outputs

# 解决方案:使用 --delete-conflicting-outputs
flutter pub run build_runner build --delete-conflicting-outputs

问题 3:监听模式不工作

bash 复制代码
# 如果 watch 模式不自动重新生成:

# 1. 停止 watch
# 2. 清理构建缓存
flutter pub run build_runner clean

# 3. 重新启动 watch
flutter pub run build_runner watch --delete-conflicting-outputs

7. 测试策略

7.1 单元测试 Provider

基础测试结构

dart 复制代码
// test/features/counter/counter_provider_test.dart

import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

void main() {
  group('Counter Provider', () {
    test('初始状态应该是 0', () {
      // 创建测试容器
      final container = ProviderContainer();
      addTearDown(container.dispose);
      
      // 读取初始状态
      expect(container.read(counterProvider), 0);
    });
    
    test('increment 应该增加计数', () {
      final container = ProviderContainer();
      addTearDown(container.dispose);
      
      // 执行操作
      container.read(counterProvider.notifier).increment();
      
      // 验证结果
      expect(container.read(counterProvider), 1);
    });
    
    test('多次 increment 应该累加', () {
      final container = ProviderContainer();
      addTearDown(container.dispose);
      
      final notifier = container.read(counterProvider.notifier);
      notifier.increment();
      notifier.increment();
      notifier.increment();
      
      expect(container.read(counterProvider), 3);
    });
  });
}

测试异步 Provider

dart 复制代码
test('应该成功获取用户数据', () async {
  final container = ProviderContainer(
    overrides: [
      // 模拟 API
      apiProvider.overrideWithValue(MockApi()),
    ],
  );
  addTearDown(container.dispose);
  
  // 等待异步完成
  final user = await container.read(userProvider('123').future);
  
  expect(user.id, '123');
  expect(user.name, 'Test User');
});

test('应该处理 API 错误', () async {
  final container = ProviderContainer(
    overrides: [
      apiProvider.overrideWithValue(MockApiWithError()),
    ],
  );
  addTearDown(container.dispose);
  
  // 验证抛出异常
  expect(
    () => container.read(userProvider('123').future),
    throwsA(isA<ApiException>()),
  );
});

7.2 测试 Widget

dart 复制代码
// test/features/counter/counter_screen_test.dart

import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

void main() {
  testWidgets('应该显示初始计数', (tester) async {
    await tester.pumpWidget(
      const ProviderScope(
        child: MaterialApp(
          home: CounterScreen(),
        ),
      ),
    );
    
    expect(find.text('0'), findsOneWidget);
  });
  
  testWidgets('点击按钮应该增加计数', (tester) async {
    await tester.pumpWidget(
      const ProviderScope(
        child: MaterialApp(
          home: CounterScreen(),
        ),
      ),
    );
    
    // 点击按钮
    await tester.tap(find.byIcon(Icons.add));
    await tester.pump();
    
    // 验证更新
    expect(find.text('1'), findsOneWidget);
  });
}

7.3 模拟和覆盖

dart 复制代码
// 创建 Mock
class MockUserRepository extends Mock implements UserRepository {}

test('使用 Mock Repository', () async {
  final mockRepo = MockUserRepository();
  
  // 设置 Mock 行为
  when(mockRepo.fetchUser('123')).thenAnswer(
    (_) async => const User(id: '123', name: 'Mock User'),
  );
  
  final container = ProviderContainer(
    overrides: [
      userRepositoryProvider.overrideWithValue(mockRepo),
    ],
  );
  addTearDown(container.dispose);
  
  final user = await container.read(userProvider('123').future);
  
  expect(user.name, 'Mock User');
  verify(mockRepo.fetchUser('123')).called(1);
});

7.4 测试最佳实践

实践 1:使用 ProviderContainer

dart 复制代码
// ✅ 推荐:使用 ProviderContainer
test('测试 Provider', () {
  final container = ProviderContainer();
  addTearDown(container.dispose);
  
  expect(container.read(myProvider), expectedValue);
});

// ❌ 不推荐:直接创建 Provider 实例
test('测试 Provider', () {
  final provider = MyProvider();  // 不要这样做
});

实践 2:清理资源

dart 复制代码
test('测试示例', () {
  final container = ProviderContainer();
  
  // 使用 addTearDown 确保清理
  addTearDown(container.dispose);
  
  // 测试逻辑...
});

实践 3:测试状态变化

dart 复制代码
test('应该监听状态变化', () {
  final container = ProviderContainer();
  addTearDown(container.dispose);
  
  final states = <int>[];
  
  // 监听状态变化
  container.listen(
    counterProvider,
    (previous, next) {
      states.add(next);
    },
  );
  
  container.read(counterProvider.notifier).increment();
  container.read(counterProvider.notifier).increment();
  
  expect(states, [1, 2]);
});

8. 常见问题和解决方案

8.1 Provider 相关问题

Q1: 何时使用 ref.watch vs ref.read vs ref.listen?

dart 复制代码
// ref.watch - 在 build 方法中使用,订阅状态变化
@override
Widget build(BuildContext context, WidgetRef ref) {
  final count = ref.watch(counterProvider);  // 状态变化时重建
  return Text('$count');
}

// ref.read - 在事件处理器中使用,不订阅
onPressed: () {
  ref.read(counterProvider.notifier).increment();  // 只读取,不监听
}

// ref.listen - 执行副作用(导航、显示 SnackBar 等)
@override
Widget build(BuildContext context, WidgetRef ref) {
  ref.listen(authProvider, (previous, next) {
    if (next == AuthState.loggedOut) {
      Navigator.pushReplacementNamed(context, '/login');
    }
  });
  
  return MyWidget();
}

Q2: Provider 什么时候被销毁?

dart 复制代码
// AutoDispose Provider - 无订阅者时自动销毁
@riverpod
class Counter extends _$Counter {  // 默认是 AutoDispose
  @override
  int build() => 0;
}

// 保持存活的 Provider
@Riverpod(keepAlive: true)
class GlobalConfig extends _$GlobalConfig {
  @override
  Config build() => Config();
}

Q3: 如何在 Provider 之间共享状态?

dart 复制代码
// 方法 1:通过 ref.watch 依赖其他 Provider
@riverpod
class UserProfile extends _$UserProfile {
  @override
  Future<Profile> build() async {
    final userId = ref.watch(currentUserIdProvider);  // 依赖
    final api = ref.watch(apiProvider);
    return api.fetchProfile(userId);
  }
}

// 方法 2:通过参数传递
@riverpod
Future<Profile> userProfile(UserProfileRef ref, String userId) async {
  final api = ref.watch(apiProvider);
  return api.fetchProfile(userId);
}

8.2 性能问题

Q4: 如何避免不必要的重建?

dart 复制代码
// 问题:整个 Widget 重建
class MyWidget extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final user = ref.watch(userProvider);  // user 的任何字段变化都会重建
    return Column(
      children: [
        Text(user.name),
        Text(user.email),
        ExpensiveWidget(),  // 即使不依赖 user 也会重建
      ],
    );
  }
}

// 解决方案 1:使用 select
class MyWidget extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final name = ref.watch(userProvider.select((u) => u.name));  // 只订阅 name
    return Text(name);
  }
}

// 解决方案 2:拆分 Widget
class MyWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        const UserName(),  // 只订阅 name
        const UserEmail(),  // 只订阅 email
        const ExpensiveWidget(),  // 不订阅任何状态
      ],
    );
  }
}

class UserName extends ConsumerWidget {
  const UserName({super.key});
  
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final name = ref.watch(userProvider.select((u) => u.name));
    return Text(name);
  }
}

Q5: 如何处理大列表性能?

dart 复制代码
// 问题:整个列表重建
@riverpod
class TodoList extends _$TodoList {
  @override
  List<Todo> build() => [];
  
  void toggle(String id) {
    state = [
      for (final todo in state)
        if (todo.id == id)
          todo.copyWith(completed: !todo.completed)
        else
          todo,
    ];  // 创建新列表,所有订阅者都会重建
  }
}

// 解决方案:为每个 Todo 创建独立的 Provider
@riverpod
class Todo extends _$Todo {
  @override
  TodoModel build(String id) {
    return ref.watch(todoListProvider).firstWhere((t) => t.id == id);
  }
  
  void toggle() {
    ref.read(todoListProvider.notifier).toggle(id);
  }
}

// 在列表中使用
ListView.builder(
  itemCount: todoIds.length,
  itemBuilder: (context, index) {
    return TodoItem(id: todoIds[index]);  // 每个 item 独立订阅
  },
)

8.3 架构问题

Q6: 如何组织大型项目的 Provider?

dart 复制代码
// 推荐的文件结构
lib/
├── core/
│   └── providers/
│       ├── app_providers.dart      // 全局 Provider
│       └── config_providers.dart   // 配置 Provider
│
├── features/
│   ├── auth/
│   │   ├── data/
│   │   │   └── repositories/
│   │   │       └── auth_repository.dart
│   │   ├── domain/
│   │   │   └── models/
│   │   │       └── user.dart
│   │   └── presentation/
│   │       ├── providers/
│   │       │   ├── auth_provider.dart
│   │       │   └── current_user_provider.dart
│   │       └── screens/
│   │           └── login_screen.dart
│   │
│   └── products/
│       └── ... (类似结构)
│
└── main.dart

Q7: 如何处理复杂的业务逻辑?

dart 复制代码
// 方法 1:使用 Use Case 模式
class LoginUseCase {
  Future<User> execute(String email, String password) async {
    // 复杂的业务逻辑
    final validated = _validateCredentials(email, password);
    final user = await _authenticate(validated);
    await _saveSession(user);
    return user;
  }
}

@riverpod
LoginUseCase loginUseCase(LoginUseCaseRef ref) {
  return LoginUseCase(
    authRepository: ref.watch(authRepositoryProvider),
    sessionManager: ref.watch(sessionManagerProvider),
  );
}

// Provider 只负责状态管理
@riverpod
class Auth extends _$Auth {
  @override
  AsyncValue<User?> build() => const AsyncValue.data(null);
  
  Future<void> login(String email, String password) async {
    state = const AsyncValue.loading();
    state = await AsyncValue.guard(() async {
      final useCase = ref.read(loginUseCaseProvider);
      return useCase.execute(email, password);
    });
  }
}

8.4 调试技巧

Q8: 如何调试 Provider 状态变化?

dart 复制代码
// 方法 1:使用 ProviderObserver
class MyObserver extends ProviderObserver {
  @override
  void didUpdateProvider(
    ProviderBase provider,
    Object? previousValue,
    Object? newValue,
    ProviderContainer container,
  ) {
    print('''
Provider: ${provider.name ?? provider.runtimeType}
Previous: $previousValue
New: $newValue
''');
  }
}

void main() {
  runApp(
    ProviderScope(
      observers: [MyObserver()],
      child: MyApp(),
    ),
  );
}

// 方法 2:使用 riverpod_lint
// 在 analysis_options.yaml 中启用
analyzer:
  plugins:
    - custom_lint

custom_lint:
  rules:
    - provider_dependencies

总结

核心要点回顾

  1. Riverpod 是什么

    • 编译时安全的状态管理库
    • Provider 的完全重写版本
    • 支持代码生成,减少样板代码
  2. MVI 架构

    • Model:不可变状态
    • View:订阅状态并渲染
    • Intent:通过 Notifier 处理用户操作
  3. Provider 选择

    • 简单状态 → @riverpod class
    • 异步数据 → @riverpod Future<T>
    • 派生状态 → @riverpod T computed()
  4. 最佳实践

    • 状态最小化、不可变、单一数据源
    • 使用 select 精确订阅
    • 使用 AsyncValue 处理异步状态
    • 依赖注入便于测试
  5. 性能优化

    • 拆分 Widget 减少重建范围
    • 使用 Consumer 局部重建
    • 为列表项创建独立 Provider

学习路径建议

  1. 入门(1-2 天):

    • 理解 Provider 基本概念
    • 实现简单的计数器示例
    • 学习 ref.watch 和 ref.read 的区别
  2. 进阶(3-5 天):

    • 掌握不同类型的 Provider
    • 实现 CRUD 操作(如 Todos)
    • 学习异步数据处理
  3. 高级(1-2 周):

    • 实践 MVI 架构
    • 编写单元测试
    • 性能优化技巧
  4. 专家(持续):

    • 大型项目架构设计
    • 自定义 Provider 模式
    • 贡献开源社区

参考资源


相关推荐
静水流深_沧海一粟18 小时前
04 | 别再写几十个参数的构造函数了——建造者模式
设计模式
StarkCoder18 小时前
从UIKit到SwiftUI的迁移感悟:数据驱动的革命
设计模式
阿星AI工作室1 天前
给openclaw龙虾造了间像素办公室!实时看它写代码、摸鱼、修bug、写日报,太可爱了吧!
前端·人工智能·设计模式
_哆啦A梦2 天前
Vibe Coding 全栈专业名词清单|设计模式·基础篇(创建型+结构型核心名词)
前端·设计模式·vibecoding
阿闽ooo5 天前
中介者模式打造多人聊天室系统
c++·设计模式·中介者模式
小米4965 天前
js设计模式 --- 工厂模式
设计模式
逆境不可逃5 天前
【从零入门23种设计模式08】结构型之组合模式(含电商业务场景)
线性代数·算法·设计模式·职场和发展·矩阵·组合模式
驴儿响叮当20105 天前
设计模式之状态模式
设计模式·状态模式
电子科技圈5 天前
XMOS推动智能音频等媒体处理技术从嵌入式系统转向全新边缘计算
人工智能·mcu·物联网·设计模式·音视频·边缘计算·iot