欢迎大家加入[开源鸿蒙跨平台开发者社区](https://openharmonycrossplatform.csdn.net),一起共建开源鸿蒙跨平台生态。

Flutter 作为跨平台开发框架的标杆,其核心优势在于 “一次编码,多端运行”,但状态管理始终是开发者绕不开的核心难点。从早期的 setState 到 Provider、Bloc,再到如今的 Riverpod,Flutter 状态管理方案一直在向 “更简洁、更安全、更可测试” 演进。本文将以 Riverpod 2.0 为核心,从零构建一个响应式 TodoList 应用,不仅详解核心 API 的使用逻辑,还会拆解 Flutter 响应式编程的底层思维,让你既能写出严谨的代码,又能理解背后的设计哲学。

一、为什么选择 Riverpod 2.0?

在深入代码之前,我们先厘清一个问题:为什么放弃传统 Provider 而选择 Riverpod?

  • 彻底解决 Provider 的 “上下文依赖”:Provider 必须依托 BuildContext 获取状态,而 Riverpod 通过 “全局提供者 + 局部消费” 的模式,摆脱了上下文束缚,代码可测试性大幅提升;
  • 编译时安全:Riverpod 通过静态类型检查避免了 Provider 常见的 “找不到对应 provider” 运行时错误;
  • 更灵活的状态控制:支持多提供者监听、状态自动缓存、手动刷新等高级特性;
  • 与 Flutter 3.x 无缝兼容:完全适配空安全,支持 Dart 3.0 的新特性。

本文所有代码基于以下环境:

plaintext

Flutter 3.16.0
Dart 3.2.0
flutter_riverpod: ^2.4.9
hooks_riverpod: ^2.4.9 (可选,简化状态消费)

二、项目初始化与核心概念拆解

2.1 项目结构设计

一个规范的 Flutter 项目应遵循 “关注点分离” 原则,我们的 TodoList 项目结构如下:

plaintext

lib/
├── main.dart          # 入口文件,初始化Riverpod
├── providers/         # 状态提供者目录
│   └── todo_providers.dart # Todo相关状态管理
├── models/            # 数据模型目录
│   └── todo_model.dart     # Todo实体类
├── widgets/           # 自定义组件目录
│   ├── todo_input.dart     # 待办输入组件
│   └── todo_list.dart      # 待办列表组件
└── screens/           # 页面目录
    └── todo_screen.dart    # 主页面

2.2 核心依赖引入

pubspec.yaml中添加依赖:

yaml

dependencies:
  flutter:
    sdk: flutter
  flutter_riverpod: ^2.4.9
  hooks_riverpod: ^2.4.9 # 可选,使用HookConsumerWidget简化代码
  uuid: ^4.3.3 # 生成唯一Todo ID

三、核心代码实现与深度解析

3.1 第一步:定义 Todo 数据模型

首先创建models/todo_model.dart,定义不可变的 Todo 实体类(Flutter 中推荐使用不可变对象管理状态,避免意外的状态篡改):

dart

import 'package:uuid/uuid.dart';

// 全局UUID生成器
const uuid = Uuid();

// Todo状态枚举
enum TodoStatus { pending, completed }

// Todo实体类(不可变)
class Todo {
  // 唯一标识
  final String id;
  // 待办内容
  final String content;
  // 完成状态
  final TodoStatus status;

  // 构造函数(强制必填参数,id可选,默认自动生成)
  Todo({
    String? id,
    required this.content,
    this.status = TodoStatus.pending,
  }) : id = id ?? uuid.v4();

  // 复制方法(不可变对象更新状态的标准方式)
  Todo copyWith({
    String? id,
    String? content,
    TodoStatus? status,
  }) {
    return Todo(
      id: id ?? this.id,
      content: content ?? this.content,
      status: status ?? this.status,
    );
  }
}

代码解析

  • 使用uuid生成唯一 ID,避免列表操作时的 key 冲突;
  • 枚举TodoStatus规范状态值,避免魔法字符串;
  • copyWith方法是不可变对象的核心:每次更新状态都会返回新对象,而非修改原对象,这符合 Flutter 的 “不可变状态” 设计理念,也能让 Riverpod 精准监听状态变化。

3.2 第二步:实现 Riverpod 状态提供者

创建providers/todo_providers.dart,这是整个应用的状态核心:

dart

import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../models/todo_model.dart';

// 1. 定义待办列表状态提供者(StateNotifierProvider)
// 泛型说明:第一个参数是StateNotifier,第二个是状态类型(List<Todo>)
final todoListProvider = StateNotifierProvider<TodoListNotifier, List<Todo>>((ref) {
  return TodoListNotifier();
});

// 2. 状态管理类(继承StateNotifier)
class TodoListNotifier extends StateNotifier<List<Todo>> {
  // 初始化状态为空列表
  TodoListNotifier() : super([]);

  // 添加待办
  void addTodo(String content) {
    if (content.trim().isEmpty) return; // 空内容过滤
    // 不可变更新:创建新列表,添加新Todo
    state = [
      ...state,
      Todo(content: content),
    ];
  }

  // 切换待办状态
  void toggleTodoStatus(String todoId) {
    // 不可变更新:遍历列表,匹配ID后更新状态
    state = state.map((todo) {
      if (todo.id == todoId) {
        return todo.copyWith(
          status: todo.status == TodoStatus.pending 
              ? TodoStatus.completed 
              : TodoStatus.pending,
        );
      }
      return todo;
    }).toList();
  }

  // 删除待办
  void deleteTodo(String todoId) {
    // 不可变更新:过滤掉指定ID的Todo
    state = state.where((todo) => todo.id != todoId).toList();
  }

  // 清空所有待办
  void clearAllTodos() {
    state = [];
  }
}

// 3. 派生状态:已完成的待办数量(Provider)
// 基于todoListProvider的状态计算派生状态
final completedTodoCountProvider = Provider<int>((ref) {
  // 监听todoListProvider的状态变化
  final todos = ref.watch(todoListProvider);
  // 计算已完成数量
  return todos.where((todo) => todo.status == TodoStatus.completed).length;
});

代码解析

  • StateNotifierProvider是 Riverpod 中管理 “可变更状态” 的核心 Provider:
    • 第一个泛型参数TodoListNotifier是自定义的状态管理类,负责处理状态逻辑;
    • 第二个泛型参数List<Todo>是状态的具体类型;
  • TodoListNotifier继承StateNotifier,必须通过state属性更新状态,且必须保证状态的不可变性(因此我们始终创建新列表,而非修改原列表);
  • completedTodoCountProvider是 “派生状态”:基于已有状态计算新值,无需手动管理,当todoListProvider的状态变化时,该 Provider 会自动重新计算。

3.3 第三步:实现 UI 组件

3.3.1 待办输入组件(widgets/todo_input.dart)

dart

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

class TodoInput extends ConsumerStatefulWidget {
  const TodoInput({super.key});

  @override
  ConsumerState<TodoInput> createState() => _TodoInputState();
}

class _TodoInputState extends ConsumerState<TodoInput> {
  // 输入控制器
  final TextEditingController _controller = TextEditingController();

  @override
  void dispose() {
    _controller.dispose(); // 释放资源,避免内存泄漏
    super.dispose();
  }

  // 提交待办
  void _submitTodo() {
    final content = _controller.text;
    // 调用状态提供者的addTodo方法
    ref.read(todoListProvider.notifier).addTodo(content);
    // 清空输入框
    _controller.clear();
    // 收起键盘
    FocusScope.of(context).unfocus();
  }

  @override
  Widget build(BuildContext context) {
    return Row(
      children: [
        Expanded(
          child: TextField(
            controller: _controller,
            decoration: const InputDecoration(
              hintText: "请输入待办内容...",
              border: OutlineInputBorder(),
              contentPadding: EdgeInsets.symmetric(horizontal: 16, vertical: 12),
            ),
            onSubmitted: (_) => _submitTodo(), // 回车提交
          ),
        ),
        const SizedBox(width: 8),
        ElevatedButton(
          onPressed: _submitTodo,
          style: ElevatedButton.styleFrom(
            padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
          ),
          child: const Text("添加"),
        ),
      ],
    );
  }
}

代码解析

  • 使用ConsumerStatefulWidget替代普通StatefulWidget,可以直接通过ref访问 Riverpod 提供者;
  • ref.read vs ref.watch
    • ref.watch:监听状态变化,状态更新时重建 Widget;
    • ref.read:仅读取 / 调用方法,不监听状态,适合事件处理(如按钮点击);
  • 重写dispose方法释放TextEditingController,这是 Flutter 开发的 “必做项”,避免内存泄漏。
3.3.2 待办列表组件(widgets/todo_list.dart)

dart

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../models/todo_model.dart';
import '../providers/todo_providers.dart';

class TodoList extends ConsumerWidget {
  const TodoList({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    // 监听待办列表状态(状态变化时自动重建)
    final todos = ref.watch(todoListProvider);

    if (todos.isEmpty) {
      return const Center(
        child: Text(
          "暂无待办事项,添加一个吧!",
          style: TextStyle(fontSize: 16, color: Colors.grey),
        ),
      );
    }

    return ListView.builder(
      shrinkWrap: true, // 适配父组件高度
      itemCount: todos.length,
      itemBuilder: (context, index) {
        final todo = todos[index];
        return ListTile(
          key: Key(todo.id), // 唯一key,优化列表性能
          leading: Checkbox(
            value: todo.status == TodoStatus.completed,
            onChanged: (_) {
              // 切换待办状态
              ref.read(todoListProvider.notifier).toggleTodoStatus(todo.id);
            },
          ),
          title: Text(
            todo.content,
            style: TextStyle(
              decoration: todo.status == TodoStatus.completed
                  ? TextDecoration.lineThrough
                  : TextDecoration.none,
              color: todo.status == TodoStatus.completed ? Colors.grey : null,
            ),
          ),
          trailing: IconButton(
            icon: const Icon(Icons.delete, color: Colors.red),
            onPressed: () {
              // 删除待办
              ref.read(todoListProvider.notifier).deleteTodo(todo.id);
            },
          ),
        );
      },
    );
  }
}

代码解析

  • ConsumerWidget是无状态组件消费 Riverpod 状态的最佳方式,通过build方法的WidgetRef参数访问状态;
  • ListView.builder是长列表的最优选择,仅构建可视区域的 item,避免性能问题;
  • Key(todo.id)为每个列表项设置唯一 key,Flutter 通过 key 识别列表项的变化,避免重建整个列表。

3.4 第四步:组装主页面

创建screens/todo_screen.dart

dart

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../providers/todo_providers.dart';
import '../widgets/todo_input.dart';
import '../widgets/todo_list.dart';

class TodoScreen extends ConsumerWidget {
  const TodoScreen({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    // 监听已完成待办数量
    final completedCount = ref.watch(completedTodoCountProvider);
    // 监听总待办数量
    final totalCount = ref.watch(todoListProvider).length;

    return Scaffold(
      appBar: AppBar(
        title: const Text("Riverpod TodoList"),
        actions: [
          // 已完成数量展示
          Padding(
            padding: const EdgeInsets.symmetric(horizontal: 16),
            child: Text("已完成:$completedCount/$totalCount"),
          ),
          // 清空按钮
          IconButton(
            icon: const Icon(Icons.clear_all),
            onPressed: () {
              ref.read(todoListProvider.notifier).clearAllTodos();
            },
          ),
        ],
      ),
      body: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          children: [
            const TodoInput(),
            const SizedBox(height: 16),
            // 列表区域(占满剩余空间)
            Expanded(
              child: const TodoList(),
            ),
          ],
        ),
      ),
    );
  }
}

代码解析

  • 页面通过ref.watch同时监听completedTodoCountProvidertodoListProvider,实现实时的数量统计;
  • Expanded包裹TodoList,让列表占满剩余空间,避免布局溢出;
  • AppBar 的actions区域整合了 “数量展示” 和 “清空按钮”,符合用户操作习惯。

3.5 第五步:入口文件配置

修改main.dart,初始化 Riverpod:

dart

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'screens/todo_screen.dart';

void main() {
  runApp(
    // 必须用ProviderScope包裹根组件,才能使用Riverpod
    const ProviderScope(
      child: MyApp(),
    ),
  );
}

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Riverpod Todo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
        useMaterial3: true, // 启用Material3设计
      ),
      home: const TodoScreen(),
      debugShowCheckedModeBanner: false, // 隐藏调试横幅
    );
  }
}

代码解析

  • ProviderScope是 Riverpod 的核心容器,所有使用 Riverpod 的组件必须在其内部;
  • 启用useMaterial3适配最新的 Material Design 3 规范,提升 UI 美观度。

四、核心特性与扩展思考

4.1 Riverpod 的核心优势体现

无上下文依赖

Riverpod 彻底摆脱了 Flutter 传统状态管理方案对 BuildContext 的依赖。这种设计带来了诸多便利:

  • 测试简化:在单元测试中可以直接操作状态,例如 ref.read(todoListProvider.notifier).addTodo("测试"),无需构建复杂的 Widget 树
  • 业务逻辑复用:状态操作可以在任何地方执行,包括 isolate、后台任务等非 UI 环境
  • 代码组织清晰:避免了上下文层层传递的麻烦,使代码更加模块化

状态隔离

Riverpod 的 Provider 设计实现了完美的关注点分离:

  • 独立作用域:每个 Provider 都有明确的作用范围,不会意外影响其他状态
  • 测试友好:可以单独测试某个 Provider 的逻辑,无需搭建完整应用环境
  • UI 解耦:状态变更逻辑完全独立于 Widget,例如:
    // 业务逻辑层
    class TodoListNotifier extends StateNotifier<List<Todo>> {
      void addTodo(String content) {
        state = [...state, Todo(content: content)];
      }
    }
    
    // UI 层
    Consumer(builder: (context, ref, _) {
      final todos = ref.watch(todoListProvider);
      return ListView.builder(...);
    })
    

派生状态自动更新

Riverpod 的响应式系统能智能处理状态依赖关系:

  • 自动追踪:当定义 final completedTodoCountProvider = Provider((ref) {...}) 时,系统会自动建立与 todoListProvider 的依赖关系
  • 高效更新:只有相关状态变化时才会触发重新计算,避免不必要的重建
  • 组合灵活:可以轻松创建基于多个状态的派生状态,例如:
    final importantTodosProvider = Provider((ref) {
      final todos = ref.watch(todoListProvider);
      final filter = ref.watch(filterProvider);
      return todos.where((t) => t.important && t.matches(filter)).toList();
    });
    

4.2 扩展方向

持久化存储

集成本地存储方案实现数据持久化:

  1. 初始化加载:在 Notifier 的初始化中读取本地数据
    class TodoListNotifier extends StateNotifier<List<Todo>> {
      TodoListNotifier() : super([]) {
        _loadTodos();
      }
      
      Future<void> _loadTodos() async {
        final prefs = await SharedPreferences.getInstance();
        state = loadFromJson(prefs.getString('todos'));
      }
    }
    

  2. 自动同步:在状态变更时自动保存
    void addTodo(String content) {
      state = [...state, Todo(content: content)];
      _saveTodos(); // 自动触发保存
    }
    

网络请求整合

使用 FutureProvider 处理异步数据:

final remoteTodosProvider = FutureProvider<List<Todo>>((ref) async {
  final response = await http.get(Uri.parse('https://api.example.com/todos'));
  return parseTodos(response.body);
});

// UI 中使用 AsyncValue 处理加载状态
Consumer(builder: (context, ref, _) {
  final asyncTodos = ref.watch(remoteTodosProvider);
  return asyncTodos.when(
    loading: () => CircularProgressIndicator(),
    error: (err, _) => Text('Error: $err'),
    data: (todos) => ListView.builder(...),
  );
})

状态防抖/节流

防止用户快速操作导致的状态问题:

void addTodo(String content) {
  // 最后一次调用后500ms才实际执行
  _debounceTimer?.cancel();
  _debounceTimer = Timer(Duration(milliseconds: 500), () {
    state = [...state, Todo(content: content)];
  });
}

主题切换

实现动态主题管理:

  1. 定义主题状态
    final themeProvider = StateProvider<ThemeMode>((ref) => ThemeMode.light);
    

  2. 在 MaterialApp 中应用
    Consumer(builder: (context, ref, _) {
      final themeMode = ref.watch(themeProvider);
      return MaterialApp(
        themeMode: themeMode,
        theme: lightTheme,
        darkTheme: darkTheme,
      );
    })
    

  3. 切换主题
    ref.read(themeProvider.notifier).state = ThemeMode.dark;
    

五、总结

本文以 Riverpod 2.0 为核心,从零构建了一个规范、可扩展的 TodoList 应用,覆盖了 Flutter 状态管理的核心知识点:

  • 不可变数据模型的设计思路;
  • Riverpod 的核心 Provider 类型(StateNotifierProvider/Provider);
  • 状态的不可变更新原则;
  • UI 组件与状态的解耦设计。

Flutter 状态管理的本质是 “状态的统一管理与高效分发”,Riverpod 通过简洁的 API 和严格的类型检查,让状态管理从 “玄学” 变成 “工程化实践”。希望本文能帮助你理解 Riverpod 的核心逻辑,也能为你后续的 Flutter 项目提供可复用的代码范式。

最后,附上完整项目的核心亮点:

  • ✅ 严格遵循空安全规范;
  • ✅ 状态与 UI 完全解耦;
  • ✅ 不可变状态更新;
  • ✅ 完整的异常处理(空内容过滤);
  • ✅ 性能优化(ListView.builder、唯一 Key)。

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐