Flutter: 状态管理 — Riverpod
状态是应用的血液——管理不当,应用就会"内出血":数据不一致、重建风暴、内存泄漏。
📋 前置知识:需要先掌握以下内容
- 第5课:StatefulWidget 与交互
- 第11课:网络请求与 REST API
1. 你将学到
- Provider 体系:Provider / StateProvider / FutureProvider / StreamProvider / NotifierProvider
- Riverpod 2.x:Notifier / AsyncNotifier / @riverpod 注解代码生成
- ref.watch / ref.read / ref.listen 使用场景与性能取舍
- ProviderScope 与 ConsumerWidget / ConsumerStatefulWidget
- ShopApp:CartNotifier 管理购物车 + AsyncProductNotifier 异步加载商品
2. 一个状态灾难的真实故事
(1) 痛点:setState 的全局状态困局
Bob 的 ShopApp 用 setState 管理购物车状态。问题是:购物车数据在首页、详情页、购物车页都需要,必须层层回调传递。CartPage 修改数量后,返回 HomePage 时 AppBar 角标不更新——因为 HomePage 的 State 不知道 CartPage 改了什么。更糟的是,10 个页面有 5 份购物车数据的副本,彼此不同步。
(2) Riverpod 的解法
Riverpod 把状态提升到全局 Provider,任何页面都可以 ref.watch 监听同一个购物车状态,修改一处所有监听者自动更新。
DART
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
// ⚙️ **安装依赖**:flutter pub add flutter_riverpod riverpod_annotation
// ⚙️ **开发依赖**:flutter pub add --dev riverpod_generator build_runner
// 自定义类定义来源:
// - Product: 见第11课 json_serializable 模型
// - CartItem: 见下方 CartNotifier 示例中的定义
// Global cart state - any page can access
@riverpod
class CartNotifier extends _$CartNotifier {
@override
List<CartItem> build() => [];
void addItem(Product product) {
state = [...state, CartItem(product: product, quantity: 1)];
}
void removeItem(int productId) {
state = state.where((item) => item.product.id != productId).toList();
}
}
// Watch in any widget
final cart = ref.watch(cartNotifierProvider);
TEXT
📖 仅展示
> **输出:** 在本地 Flutter SDK(Flutter 3.x / Dart 3.x)运行。Piston 服务器未安装 Flutter,请在本机 `flutter run` 实操对照。实际 UI/状态会因平台略有差异。
(3) 收益:一处修改,全局同步
Bob 用 Riverpod 后,购物车状态只有一份,任何页面修改后所有监听者自动更新。回调地狱消失,数据始终一致。
3. Riverpod 核心概念
graph TD
PS[ProviderScope] --> CN[CartNotifier]
PS --> PN[ProductAsyncNotifier]
PS --> UN[UserNotifier]
CN --> |ref.watch| CV[CartView]
PN --> |ref.watch| PV[ProductListView]
UN --> |ref.watch| UV[UserProfileView]
CN --> |totalItems| Badge[BottomNav Badge]
TEXT
📖 仅展示
> **输出:** 在本地 Flutter SDK(Flutter 3.x / Dart 3.x)运行。Piston 服务器未安装 Flutter,请在本机 `flutter run` 实操对照。实际 UI/状态会因平台略有差异。
(1) Provider 类型
| Provider | 状态类型 | 适用场景 |
|---|---|---|
Provider |
不可变值 | 依赖注入、配置 |
StateProvider |
简单可变值 | 计数器、开关 |
FutureProvider |
Future 结果 | 一次性异步数据 |
StreamProvider |
Stream 结果 | 实时数据流 |
NotifierProvider |
Notifier 实例 | 复杂状态 + 业务逻辑 |
AsyncNotifierProvider |
AsyncNotifier | 异步初始化 + 业务逻辑 |
(2) ref 方法对比
| 方法 | 用途 | 触发重建 | 适用位置 |
|---|---|---|---|
ref.watch |
监听状态变化 | 是(状态变化时) | build 方法内 |
ref.read |
一次性读取 | 否 | 回调/事件处理 |
ref.listen |
监听 + 执行副作用 | 否 | build 内(导航/SnackBar) |
⚠️ 注意: 不要在 build 之外使用 ref.watch,不要在 build 内使用 ref.read 监听状态。
4. 手写 Provider
▶ 示例
TEXT
📖 仅展示
> **输出:** 在本地 Flutter SDK(Flutter 3.x / Dart 3.x)运行。Piston 服务器未安装 Flutter,请在本机 `flutter run` 实操对照。实际 UI/状态会因平台略有差异。
:StateProvider 简单状态
DART
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
// ⚙️ **安装依赖**:flutter pub add flutter_riverpod
// Simple counter with StateProvider
final counterProvider = StateProvider<int>((ref) => 0);
// Theme mode provider
final themeModeProvider = StateProvider<ThemeMode>((ref) => ThemeMode.system);
// Usage in widget
class CounterWidget 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).state++,
child: const Text('Increment'),
),
],
);
}
}
TEXT
📖 仅展示
> **输出:** 在本地 Flutter SDK(Flutter 3.x / Dart 3.x)运行。Piston 服务器未安装 Flutter,请在本机 `flutter run` 实操对照。实际 UI/状态会因平台略有差异。
▶ 示例
TEXT
📖 仅展示
> **输出:** 在本地 Flutter SDK(Flutter 3.x / Dart 3.x)运行。Piston 服务器未安装 Flutter,请在本机 `flutter run` 实操对照。实际 UI/状态会因平台略有差异。
:FutureProvider 异步数据
DART
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
// ⚙️ **安装依赖**:flutter pub add flutter_riverpod
// 自定义类定义来源:
// - Product: 见第11课 json_serializable 模型
// - ProductTile: 自定义 Widget,展示单个商品
// - productRepositoryProvider: 见第14课 ProductRepository
// Load products once
final productsProvider = FutureProvider<List<Product>>((ref) async {
final repo = ref.read(productRepositoryProvider);
return repo.getProducts();
});
// Usage
class ProductList extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final productsAsync = ref.watch(productsProvider);
return productsAsync.when(
loading: () => const CircularProgressIndicator(),
error: (err, _) => Text('Error: $err'),
data: (products) => ListView.builder(
itemCount: products.length,
itemBuilder: (_, i) => ProductTile(product: products[i]),
),
);
}
}
TEXT
📖 仅展示
> **输出:** 在本地 Flutter SDK(Flutter 3.x / Dart 3.x)运行。Piston 服务器未安装 Flutter,请在本机 `flutter run` 实操对照。实际 UI/状态会因平台略有差异。
5. Notifier 与 AsyncNotifier
▶ 示例
TEXT
📖 仅展示
> **输出:** 在本地 Flutter SDK(Flutter 3.x / Dart 3.x)运行。Piston 服务器未安装 Flutter,请在本机 `flutter run` 实操对照。实际 UI/状态会因平台略有差异。
:CartNotifier(完整购物车逻辑)
DART
import 'package:flutter_riverpod/flutter_riverpod.dart';
// ⚙️ **安装依赖**:flutter pub add flutter_riverpod
// 自定义类定义来源:
// - Product: 见第11课 json_serializable 模型(简化版如下)
/*
class Product {
final int id;
final String name;
final double price;
const Product({required this.id, required this.name, required this.price});
}
*/
class CartItem {
final Product product;
final int quantity;
const CartItem({required this.product, required this.quantity});
double get total => product.price * quantity;
CartItem copyWith({int? quantity}) => CartItem(product: product, quantity: quantity ?? this.quantity);
}
class CartNotifier extends Notifier<List<CartItem>> {
@override
List<CartItem> build() => [];
void addItem(Product product) {
final idx = state.indexWhere((i) => i.product.id == product.id);
if (idx >= 0) {
state = [...state]..[idx] = state[idx].copyWith(quantity: state[idx].quantity + 1);
} else {
state = [...state, CartItem(product: product, quantity: 1)];
}
}
void updateQuantity(int productId, int quantity) {
if (quantity <= 0) {
removeItem(productId);
return;
}
state = [
for (final item in state)
if (item.product.id == productId) item.copyWith(quantity: quantity) else item,
];
}
void removeItem(int productId) {
state = state.where((i) => i.product.id != productId).toList();
}
void clear() => state = [];
}
final cartProvider = NotifierProvider<CartNotifier, List<CartItem>>(CartNotifier.new);
// Derived state: total price
final cartTotalProvider = Provider<double>((ref) {
final items = ref.watch(cartProvider);
return items.fold(0.0, (sum, item) => sum + item.total);
});
// Derived state: item count
final cartItemCountProvider = Provider<int>((ref) {
final items = ref.watch(cartProvider);
return items.fold(0, (sum, item) => sum + item.quantity);
});
TEXT
📖 仅展示
> **输出:** 在本地 Flutter SDK(Flutter 3.x / Dart 3.x)运行。Piston 服务器未安装 Flutter,请在本机 `flutter run` 实操对照。实际 UI/状态会因平台略有差异。
▶ 示例
TEXT
📖 仅展示
> **输出:** 在本地 Flutter SDK(Flutter 3.x / Dart 3.x)运行。Piston 服务器未安装 Flutter,请在本机 `flutter run` 实操对照。实际 UI/状态会因平台略有差异。
:AsyncNotifier(异步初始化商品)
DART
import 'package:flutter_riverpod/flutter_riverpod.dart';
// ⚙️ **安装依赖**:flutter pub add flutter_riverpod
// 自定义类定义来源:
// - Product: 见第11课 json_serializable 模型
// - productRepositoryProvider: 见第14课 ProductRepository
class ProductAsyncNotifier extends AsyncNotifier<List<Product>> {
@override
Future<List<Product>> build() async {
final repo = ref.read(productRepositoryProvider);
return repo.getProducts();
}
Future<void> loadMore() async {
final current = state.valueOrNull ?? [];
state = const AsyncLoading();
state = await AsyncValue.guard(() async {
final more = await ref.read(productRepositoryProvider).getProducts(page: (current.length ~/ 20) + 1);
return [...current, ...more];
});
}
Future<void> refresh() async {
state = const AsyncLoading();
state = await AsyncValue.guard(() => ref.read(productRepositoryProvider).getProducts());
}
}
final productProvider = AsyncNotifierProvider<ProductAsyncNotifier, List<Product>>(
ProductAsyncNotifier.new,
);
TEXT
📖 仅展示
> **输出:** 在本地 Flutter SDK(Flutter 3.x / Dart 3.x)运行。Piston 服务器未安装 Flutter,请在本机 `flutter run` 实操对照。实际 UI/状态会因平台略有差异。
6. @riverpod 代码生成
▶ 示例
TEXT
📖 仅展示
> **输出:** 在本地 Flutter SDK(Flutter 3.x / Dart 3.x)运行。Piston 服务器未安装 Flutter,请在本机 `flutter run` 实操对照。实际 UI/状态会因平台略有差异。
:使用 riverpod_generator
DART
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
// ⚙️ **安装依赖**:flutter pub add flutter_riverpod riverpod_annotation
// ⚙️ **开发依赖**:flutter pub add --dev riverpod_generator build_runner
// 自定义类定义来源:
// - CartItem: 见本课第5节 CartNotifier 示例
// - User/AuthService: 见第14课 Auth Notifier
part 'providers.g.dart';
// Generated notifier
@riverpod
class Cart extends _$Cart {
@override
List<CartItem> build() => [];
void addItem(Product product) {
final idx = state.indexWhere((i) => i.product.id == product.id);
if (idx >= 0) {
state[idx] = state[idx].copyWith(quantity: state[idx].quantity + 1);
state = [...state]; // Trigger rebuild
} else {
state = [...state, CartItem(product: product, quantity: 1)];
}
}
}
// Generated provider (derived)
@riverpod
double cartTotal(CartTotalRef ref) {
final items = ref.watch(cartProvider);
return items.fold(0.0, (sum, item) => sum + item.total);
}
// Keep alive across widget lifecycle
@Riverpod(keepAlive: true)
class Auth extends _$Auth {
@override
AsyncValue<User?> build() => const AsyncData(null);
Future<void> login(String email, String password) async {
state = const AsyncLoading();
state = await AsyncValue.guard(() => AuthService.login(email, password));
}
}
TEXT
📖 仅展示
> **输出:** 在本地 Flutter SDK(Flutter 3.x / Dart 3.x)运行。Piston 服务器未安装 Flutter,请在本机 `flutter run` 实操对照。实际 UI/状态会因平台略有差异。
7. 完整示例:ShopApp Riverpod 集成
DART
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
// ⚙️ **安装依赖**:flutter pub add flutter_riverpod go_router
// 自定义类定义来源:
// - Product: 见第11课 json_serializable 模型
// - cartProvider/cartTotalProvider/cartItemCountProvider: 见本课第5节
// - themeModeProvider: 见本课第4节 StateProvider
// main.dart
void main() {
runApp(ProviderScope(child: ShopApp()));
}
class ShopApp extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final themeMode = ref.watch(themeModeProvider);
return MaterialApp.router(
title: 'ShopApp',
theme: ThemeData(colorSchemeSeed: Colors.blue, useMaterial3: true),
darkTheme: ThemeData(colorSchemeSeed: Colors.blue, useMaterial3: true, brightness: Brightness.dark),
themeMode: themeMode,
routerConfig: router,
);
}
}
// Cart badge in AppBar - auto-updates
class CartBadge extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final count = ref.watch(cartItemCountProvider);
return Stack(
alignment: Alignment.center,
children: [
const Icon(Icons.shopping_cart),
if (count > 0)
Positioned(right: 0, top: 0,
child: CircleAvatar(radius: 8, backgroundColor: Colors.red,
child: Text('$count', style: const TextStyle(fontSize: 9, color: Colors.white)))),
],
);
}
}
// Add to cart action
class AddToCartButton extends ConsumerWidget {
final Product product;
const AddToCartButton({super.key, required this.product});
@override
Widget build(BuildContext context, WidgetRef ref) {
return FilledButton.icon(
onPressed: () {
ref.read(cartProvider.notifier).addItem(product);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('${product.name} added to cart')),
);
},
icon: const Icon(Icons.shopping_cart),
label: const Text('Add to Cart'),
);
}
}
// Cart page with Riverpod
class CartPage extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final items = ref.watch(cartProvider);
final total = ref.watch(cartTotalProvider);
return Scaffold(
appBar: AppBar(title: Text('Cart (${items.length})')),
body: items.isEmpty
? const Center(child: Text('Cart is empty'))
: Column(children: [
Expanded(child: ListView.separated(
itemCount: items.length,
separatorBuilder: (_, __) => const Divider(),
itemBuilder: (_, i) {
final item = items[i];
return ListTile(
title: Text(item.product.name),
subtitle: Text('\$${item.total.toStringAsFixed(2)}'),
trailing: Row(mainAxisSize: MainAxisSize.min, children: [
IconButton(icon: const Icon(Icons.remove), onPressed: () =>
ref.read(cartProvider.notifier).updateQuantity(item.product.id, item.quantity - 1)),
Text('${item.quantity}'),
IconButton(icon: const Icon(Icons.add), onPressed: () =>
ref.read(cartProvider.notifier).updateQuantity(item.product.id, item.quantity + 1)),
]),
);
},
)),
Padding(padding: const EdgeInsets.all(16),
child: Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [
Text('\$${total.toStringAsFixed(2)}', style: const TextStyle(fontSize: 24, fontWeight: FontWeight.bold)),
FilledButton(onPressed: () => context.push('/checkout'), child: const Text('Checkout')),
])),
]),
);
}
}
TEXT
📖 仅展示
> **输出:** 在本地 Flutter SDK(Flutter 3.x / Dart 3.x)运行。Piston 服务器未安装 Flutter,请在本机 `flutter run` 实操对照。实际 UI/状态会因平台略有差异。
❓ 常见问题
Q Riverpod 和 Provider 包有什么区别?
A Riverpod 是 Provider 的进化版,解决了 Provider 的依赖注入不安全、无法测试等问题。新项目直接用 Riverpod。
Q ref.watch 和 ref.read 怎么选?
A build 方法内用 ref.watch(需要 UI 响应状态变化);回调/事件处理中用 ref.read(不需要重建)。
Q state = [...state] 是必须的吗?
A 是的。Riverpod 用 identical 检查状态是否变化。修改 List 内容不创建新对象,Riverpod 不会通知更新。
Q AsyncNotifier 的 AsyncLoading/AsyncData/AsyncError 怎么处理?
A 用
.when(loading:, error:, data:) 模式匹配,分别显示加载指示器、错误页面、数据内容。Q @riverpod 注解需要什么依赖?
A pubspec.yaml 添加 riverpod_annotation + riverpod_generator + build_runner,运行
dart run build_runner watch。Q keepAlive 和 autoDispose 有什么区别?
A autoDispose(默认)当没有监听者时自动销毁;keepAlive 永远不销毁。全局状态(如 Auth)用 keepAlive。
📖 小节
- Riverpod 统一管理全局状态,一处修改所有监听者自动更新
- Notifier 适合复杂状态+业务逻辑,AsyncNotifier 适合异步初始化
- ref.watch 在 build 内监听,ref.read 在回调中操作,ref.listen 处理副作用
- 派生 Provider(cartTotalProvider)自动计算依赖状态
- @riverpod 代码生成减少样板代码,推荐新项目使用
📝 作业
- 基础题(难度⭐):用 StateProvider 实现暗色模式切换,用 ConsumerWidget 在 AppBar 显示当前模式。
- 进阶题(难度⭐⭐):用 Notifier 实现 CartNotifier,支持增删改查和总价计算,在两个不同页面展示购物车状态。
- 挑战题(难度⭐⭐⭐):用 @riverpod 代码生成实现 Auth + Product + Cart 三个 Notifier,实现:登录后自动加载商品,加购后角标自动更新。