
在 Flutter 状态管理的世界里,Riverpod 以其声明式、响应式和可组合的理念,迅速成为社区的热门选择。它不仅解决了 Provider 包的一些固有痛点,还通过 riverpod_generator 带来了更加现代化和类型安全的开发体验。
当我们通过阅读文档以及之前的学习掌握了 Provider 、FutureProvider 和 Notifier 等基础用法后,很快就会面临更复杂的业务场景:
- 代码重复:多个 Provider 中存在大量相似的逻辑。
- 灵活性差:状态的获取依赖于硬编码的常量,无法动态调整。
- 多重依赖:一个状态的计算需要依赖其他多个状态,如何优雅地组织它们?
这些问题的答案,就隐藏在 Riverpod 的两大核心能力之中:组合(Combination)与参数化(Parameterization) 。本文将深入探讨 Riverpod v3 中如何运用这两种技术,构建出高内聚、低耦合且极具扩展性的状态管理架构。
Riverpod 3 中的组合(Combination)
什么是组合
在 Riverpod 中,组合 指的是将多个独立的 Provider 相互关联,协同工作,从而派生出新的、更复杂的状态。这与 Flutter 中"万物皆 Widget"的思想异曲同工,在 Riverpod 中,我们可以称之为"万物皆 Provider"。状态不再是孤立的,而是可以像乐高积木一样,自由拼接成一个完整的数据模型。
常见场景:
- 一个 Provider 需要读取另一个 Provider 的数据进行计算。
- 将多个分散的状态(如用户配置、用户信息、应用主题)聚合成一个统一的视图模型(ViewModel)。
- 根据一个状态的变化,去触发另一个状态的更新或重新获取。
组合的常见方式
Riverpod 提供了简洁的 API 来实现 Provider 之间的组合。
- ref.watch :这是实现组合最核心的方法。在一个 Provider 内部使用 ref.watch 监听另一个 Provider,当被监听的 Provider 状态发生变化时,当前 Provider 会自动重新计算(recompute)或重建(rebuild),确保数据流的响应式。
- select :这是一个性能优化利器。当你只关心被监听 Provider 状态中的某一个特定字段时,使用 select 可以避免因不相关字段的变化而导致不必要的重建。
csharp
// 假设 userInfoProvider 持有一个 User 对象
// 只有当 user 的 name 字段变化时,本 Provider 才会重新计算
final userName = ref.watch(userInfoProvider.select((user) => user.name));
示例:用户与配置的组合
假设我们有两个独立的 Provider:一个用于管理用户基本信息,另一个用于管理应用配置(如主题模式)。
dart
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'config_provider.g.dart';
// 1. 应用配置模型
class AppConfig {
final String theme;
const AppConfig({required this.theme});
}
// 2. 应用配置 Notifier
@riverpod
class Config extends _$Config {
@override
AppConfig build() {
// 默认配置
return const AppConfig(theme: 'dark');
}
void changeTheme(String newTheme) {
state = AppConfig(theme: newTheme);
}
}
// lib/providers/user_provider.dart
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'user_provider.g.dart';
// 3. 用户信息模型
class User {
final String name;
const User({required this.name});
}
// 4. 用户信息 Notifier (模拟异步获取)
@riverpod
class UserInfo extends _$UserInfo {
@override
Future<User> build() async {
// 模拟网络请求
await Future.delayed(const Duration(seconds: 1));
return const User(name: 'Alice');
}
}
现在,我们需要一个 Provider 来生成一段欢迎语,它同时依赖用户信息和应用主题。
dart
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'user_provider.dart';
import 'config_provider.dart';
part 'welcome_message_provider.g.dart';
// 5. 组合 Provider
@riverpod
String welcomeMessage(WelcomeMessageRef ref) {
// 使用 ref.watch 监听用户和配置 Provider
final userAsyncValue = ref.watch(userInfoProvider);
final config = ref.watch(configProvider);
// 处理异步状态
return userAsyncValue.when(
data: (user) => '你好, ${user.name}! 当前主题是 ${config.theme}.',
loading: () => '加载中...',
error: (err, stack) => '加载失败',
);
}
在这个例子中,welcomeMessageProvider 通过 ref.watch 订阅了 userInfoProvider 和 configProvider 。任何一个被依赖的 Provider 状态发生改变(例如用户重新登录或主题切换),welcomeMessageProvider 都会自动重新计算,UI 也会随之刷新,整个过程是完全自动和响应式的。
参数化 Provider(Parameterized Provider)
为什么需要参数化?
在真实应用中,我们经常需要根据外部传入的参数来获取特定的数据。例如:
- 根据 cityId 获取不同城市的天气信息。
- 根据 userId 获取特定用户的详细资料。
- 根据搜索关键词 query 获取搜索结果。
如果为每个参数都创建一个独立的 Provider,代码会变得冗余且难以维护。参数化允许我们创建 Provider 的"工厂",根据传入的参数动态地创建和管理状态实例。
Riverpod 3 中的参数化写法
在 Riverpod 3 的代码生成方案中,实现参数化变得极其简单:只需给 build 方法添加参数即可 。riverpod_generator 会自动识别并生成对应的 Family。
示例:带参天气查询
让我们创建一个根据城市名称查询天气的 Provider。
首先定义天气的数据模型
dart
class Weather {
final String city;
final double temperature;
final String description;
const Weather({
required this.city,
required this.temperature,
required this.description,
});
}
// lib/api/weather_api.dart
// 模拟一个天气 API
Future<Weather> fetchWeather(String city) async {
await Future.delayed(const Duration(milliseconds: 500));
// 模拟不同城市返回不同数据
if (city.toLowerCase() == 'beijing') {
return Weather(city: city, temperature: 25.0, description: '晴');
}
return Weather(city: city, temperature: 18.0, description: '多云');
}
创建provider
dart
import 'package:riverpod_annotation/riverpod_annotation.dart';
import '../models/weather.dart';
import '../api/weather_api.dart';
part 'weather_provider.g.dart';
// 只需在 build 方法中添加 city 参数
@riverpod
class WeatherController extends _$WeatherController {
@override
Future<Weather> build(String city) async {
// build 方法体内的逻辑就是获取数据的过程
return await fetchWeather(city);
}
// 可以在这里添加其他业务方法,比如刷新
Future<void> refresh() async {
// state = const AsyncValue.loading(); // 可以选择性地设置为加载状态
state = await AsyncValue.guard(() => fetchWeather(state.value!.city));
}
}
在 UI 中调用时,我们像调用函数一样传递参数:
dart
Consumer(
builder: (context, ref, child) {
// 传入参数 'Beijing'
final weatherAsync = ref.watch(weatherControllerProvider('Beijing'));
return weatherAsync.when(
data: (weather) => Text('${weather.city}: ${weather.temperature}°C'),
loading: () => const CircularProgressIndicator(),
error: (e, st) => Text('Error: $e'),
);
},
)
整体思路跟我之前的文章中的天气示例是一样的。
参数化与组合结合
参数化的 Provider 同样可以与其它 Provider 组合,这使得它威力倍增。例如,我们的天气查询可以依赖一个 settingsProvider 来决定使用摄氏度还是华氏度。
scala
enum TemperatureUnit { celsius, fahrenheit }
@riverpod
class Settings extends _$Settings {
@override
TemperatureUnit build() => TemperatureUnit.celsius;
void setUnit(TemperatureUnit unit) {
state = unit;
}
}
// weather_provider.dart (修改版)
@riverpod
class WeatherController extends _$WeatherController {
@override
Future<Weather> build(String city) async {
// 组合:watch 设置 Provider
final unit = ref.watch(settingsProvider);
// API 调用现在可以带上单位参数
return await fetchWeather(city, unit: unit);
}
现在,weatherControllerProvider('Beijing') 不仅依赖于参数 Beijing,还依赖于 settingsProvider 的状态。当用户切换温度单位时,settingsProvider 更新,所有正在被监听的天气 Provider 实例都会自动刷新,获取新的单位下的天气数据。
高级应用与最佳实践
避免重复创建:参数缓存机制
Riverpod 的参数化 Provider 内置了强大的缓存机制。只要传入的参数相同,Riverpod 就会返回同一个 Provider 实例。
ini
// 这两次调用返回的是同一个 Provider 实例和它的状态
final beijingWeather1 = ref.watch(weatherControllerProvider('Beijing'));
final beijingWeather2 = ref.watch(weatherControllerProvider('Beijing'));
// 这将创建或返回一个与北京天气完全独立的 Provider 实例
final londonWeather = ref.watch(weatherControllerProvider('London'));
这个机制确保了对于相同的请求,不会发生重复的网络调用或状态计算,极大地提升了性能和资源利用率。
搭配 .autoDispose 使用
参数化 Provider 常常是按需创建的,例如用户进入详情页时创建一个 Provider,退出后这个 Provider 的状态就不再需要了。为了防止内存泄漏,我们应该使用 @riverpod(keepAlive: false) (默认行为)或 @Riverpod.autoDispose 标注。
当一个 autoDispose 的 Provider 不再被任何地方 watch 时,它会在一段时间后被自动销毁,释放其占用的内存和资源。这对于参数化 Provider 尤其重要,否则随着参数的增多,内存中会残留大量无用的 Provider 实例。
scala
@riverpod(keepAlive: false) // 这是默认行为,等同于 .autoDispose
class WeatherController extends _$WeatherController { ... }
常见坑与解决方案
-
参数类型过于复杂:
- 问题 :如果使用复杂的自定义对象作为参数,必须确保正确实现了 == 和 hashCode。否则,即使是内容相同的两个对象实例,Riverpod 也会认为是不同的参数,导致缓存失效,创建不必要的 Provider。
- 解决方案 :尽量使用简单、可比较的类型作为参数,如 String 、int 、enum 。如果必须使用复杂对象,请使用 freezed 或 equatable 库来自动生成正确的 == 和 hashCode。
-
依赖链过长:
- 问题 :Provider A -> B -> C -> D 这样的长依赖链会使状态逻辑变得难以追踪和调试。一个微小的改动可能引发连锁反应。
- 解决方案:保持依赖链的扁平化和简短。通过组合,将多个源 Provider 合并为一个高层级的 Provider,而不是让它们线性地互相依赖。
-
ref.watch 与 ref.read 的使用场景区分:
- ref.watch :用于 build 方法中,或任何需要响应式地根据状态变化来重建 UI 或 Provider 的地方。
- ref.read :用于一次性 读取 Provider 的当前状态,通常在用户交互的回调函数中,如 onPressed、onSubmitted。在这些地方使用 ref.watch 是错误的,因为它会导致不必要的重建。
这里我强烈建议如果你的应用中数据类多、字段常变、要做等值比较/拷贝/JSON,有状态机/分支状态(如加载中/成功/失败、认证已登出/已登录/未知)都是用freezed而不是手写数据模型,下面我简单对比一下这两者的区别。
首先是使用freezed:
数据类:
dart
import 'package:freezed_annotation/freezed_annotation.dart';
part 'todo.freezed.dart';
part 'todo.g.dart';
@freezed
class Todo with _$Todo {
const factory Todo({
required String id,
required String title,
@Default(false) bool done,
DateTime? dueAt,
}) = _Todo;
factory Todo.fromJson(Map<String, dynamic> json) => _$TodoFromJson(json);
}
使用示例:
ini
final t1 = Todo(id: '1', title: 'Write article');
final t2 = t1.copyWith(done: true);
final json = t2.toJson(); // 序列化
final back = Todo.fromJson(json); // 反序列化
assert(t2 == back); // 值等价比较
联合类型建模"认证状态机":
dart
import 'package:freezed_annotation/freezed_annotation.dart';
part 'auth_state.freezed.dart';
@freezed
class AuthState with _$AuthState {
const factory AuthState.unknown() = _Unknown;
const factory AuthState.signedOut({String? reason}) = _SignedOut;
const factory AuthState.signedIn({required String userId}) = _SignedIn;
}
使用示例:
javascript
String banner(AuthState s) {
return s.when(
unknown: () => 'Checking session...',
signedOut: (reason) => 'Please login${reason == null ? '' : ' ($reason)'}',
signedIn: (userId) => 'Welcome, $userId!',
);
}
接着是不使用freezed,纯手写:
数据类:
dart
import 'dart:convert';
class TodoManual {
final String id;
final String title;
final bool done;
final DateTime? dueAt;
const TodoManual({
required this.id,
required this.title,
this.done = false,
this.dueAt,
});
TodoManual copyWith({
String? id,
String? title,
bool? done,
DateTime? dueAt,
}) {
return TodoManual(
id: id ?? this.id,
title: title ?? this.title,
done: done ?? this.done,
dueAt: dueAt ?? this.dueAt,
);
}
Map<String, dynamic> toJson() => {
'id': id,
'title': title,
'done': done,
'dueAt': dueAt?.toIso8601String(),
};
factory TodoManual.fromJson(Map<String, dynamic> json) {
return TodoManual(
id: json['id'] as String,
title: json['title'] as String,
done: json['done'] as bool? ?? false,
dueAt: json['dueAt'] == null ? null : DateTime.parse(json['dueAt']),
);
}
// 值等价:需要自己维护所有字段
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other is TodoManual &&
other.id == id &&
other.title == title &&
other.done == done &&
other.dueAt == dueAt);
}
@override
int get hashCode => Object.hash(id, title, done, dueAt);
@override
String toString() => jsonEncode(toJson());
}
使用示例:
ini
final t1 = TodoManual(id: '1', title: 'Write article');
final t2 = t1.copyWith(done: true);
final json = t2.toJson();
final back = TodoManual.fromJson(json);
assert(t2 == back); // 成立,但完全靠你手写的 ==/hashCode 正确性
手写联合类型:
dart
abstract class AuthStateManual {
const AuthStateManual();
T when<T>({
required T Function() unknown,
required T Function(String? reason) signedOut,
required T Function(String userId) signedIn,
}) {
final self = this;
if (self is AuthUnknown) return unknown();
if (self is AuthSignedOut) return signedOut(self.reason);
if (self is AuthSignedIn) return signedIn(self.userId);
throw StateError('Unhandled state: $runtimeType');
}
}
class AuthUnknown extends AuthStateManual {
const AuthUnknown();
}
class AuthSignedOut extends AuthStateManual {
final String? reason;
const AuthSignedOut({this.reason});
}
class AuthSignedIn extends AuthStateManual {
final String userId;
const AuthSignedIn({required this.userId});
}
// 使用
String bannerManual(AuthStateManual s) {
return s.when(
unknown: () => 'Checking session...',
signedOut: (reason) => 'Please login${reason == null ? '' : ' ($reason)'}',
signedIn: (userId) => 'Welcome, $userId!',
);
}
通过对比我们不难发现,如果选择手写 ,我们不得不需要自己实现并维护 copyWith、==/hashCode、toString、toJson/fromJson。并且联合类型没有编译期的"分支穷举校验",更容易出错,代码量大、重复劳动多、重构的成本很高。
实战案例:新闻应用的多维度状态管理
接下来,我们通过一个简单的新闻应用,来展示如何综合运用组合与参数化
需求:
- 新闻列表:可按分类(如'科技'、'体育')筛选新闻。
- 用户收藏:用户可以看到自己收藏的新闻列表。
- 搜索功能:用户可以根据关键词搜索新闻。
Provider 设计:
参数化的新闻列表 Provider:
rust
@riverpod(keepAlive: true) // 列表数据可以缓存
Future<List<News>> newsList(NewsListRef ref, String category) async {
return ref.watch(newsRepositoryProvider).fetchNews(category);
}
组合的用户收藏 Provider:
typescript
@riverpod
Future<List<News>> userFavorites(UserFavoritesRef ref) {
// 组合:依赖当前用户信息
final userId = ref.watch(userProvider.select((user) => user.id));
if (userId == null) return Future.value([]);
// 依赖新闻仓库
return ref.watch(newsRepositoryProvider).fetchFavorites(userId);
}
参数化的搜索 Provider:
rust
@riverpod
Future<List<News>> newsSearch(NewsSearchRef ref, String query) async {
if (query.isEmpty) return [];
// 对 query 做 debounce 处理是常见的优化
return ref.watch(newsRepositoryProvider).searchNews(query);
}
在UI中进行使用:
less
// 在 UI 中
final newsAsync = ref.watch(newsListProvider('科技'));
// ...
newsAsync.when(
// ...
error: (err, stack) => ElevatedButton(
// ref.invalidate 会让 Provider 重新执行 build 方法
onPressed: () => ref.invalidate(newsListProvider('科技')),
child: Text('重试'),
),
// ...
);
// 或者使用 Pull-to-Refresh
RefreshIndicator(
onRefresh: () => ref.refresh(newsListProvider('科技').future),
// ...
)
整个项目完成后的效果如下:

总结
组合 与参数化是 Riverpod v3 中从"能用"到"好用"的必经之路。
- 组合 让我们的 Provider 形成一个"状态拼装机"。我们可以创建单一职责的原子 Provider,然后像搭积木一样将它们组合成任何复杂的业务状态,实现了真正的关注点分离。
- 参数化 则为我们的 Provider 插上了动态的翅膀。它消除了硬编码,让 Provider 能够根据上下文按需生成状态实例,是构建可复用数据获取逻辑的不二法门。
当二者结合时,我们将获得一个强大、灵活且类型安全的状态管理工具链,足以应对从小型项目到大型复杂应用(如新闻客户端、电商 App)的各种挑战。