Riverpod 3:组合与参数化的进阶实践

在 Flutter 状态管理的世界里,Riverpod 以其声明式、响应式和可组合的理念,迅速成为社区的热门选择。它不仅解决了 Provider 包的一些固有痛点,还通过 riverpod_generator 带来了更加现代化和类型安全的开发体验。

当我们通过阅读文档以及之前的学习掌握了 ProviderFutureProviderNotifier 等基础用法后,很快就会面临更复杂的业务场景:

  • 代码重复:多个 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 订阅了 userInfoProviderconfigProvider 。任何一个被依赖的 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。
    • 解决方案 :尽量使用简单、可比较的类型作为参数,如 Stringintenum 。如果必须使用复杂对象,请使用 freezedequatable 库来自动生成正确的 ==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。并且联合类型没有编译期的"分支穷举校验",更容易出错,代码量大、重复劳动多、重构的成本很高。

实战案例:新闻应用的多维度状态管理

接下来,我们通过一个简单的新闻应用,来展示如何综合运用组合与参数化

需求:

  1. 新闻列表:可按分类(如'科技'、'体育')筛选新闻。
  2. 用户收藏:用户可以看到自己收藏的新闻列表。
  3. 搜索功能:用户可以根据关键词搜索新闻。

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)的各种挑战。

相关推荐
前端一课几秒前
分享:基于Next.js的企业级提示词AI平台
前端
程序员老刘·几秒前
ArkUI-X 6.0 跨平台框架能否取代 Flutter?
flutter·鸿蒙系统·跨平台开发·客户端开发
小高0072 分钟前
🔥「从零到一」我用 Node BFF 手撸一个 Vue3 SSR 项目(附源码)
前端·javascript·vue.js
SailingCoder4 分钟前
AI 流式对话该怎么做?SSE、fetch、axios 一次讲清楚
前端·javascript·人工智能·ai·node.js
hxjhnct8 分钟前
Vue 实现多行文本“展开收起”
前端·javascript·vue.js
橙子的AI笔记10 分钟前
2025年全球最受欢迎的JS鉴权框架Better Auth,3分钟带你学会
前端·ai编程
百锦再10 分钟前
Vue大屏开发全流程及技术细节详解
前端·javascript·vue.js·微信小程序·小程序·架构·ecmascript
独自破碎E14 分钟前
你知道Spring Boot配置文件的加载优先级吗?
前端·spring boot·后端
一树山茶16 分钟前
Vue变化响应
前端