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

相关推荐
jason_yang3 小时前
vue3自定义渲染内容如何当参数传递
前端·javascript·vue.js
年年测试3 小时前
Browser Use 浏览器自动化 Agent:让浏览器自动为你工作
前端·数据库·自动化
维维酱3 小时前
React Fiber 架构与渲染流程
前端·react.js
gitboyzcf3 小时前
基于Taro4最新版微信小程序、H5的多端开发简单模板
前端·vue.js·taro
姓王者3 小时前
解决Tauri2.x拖拽事件问题
前端
冲!!4 小时前
vue3存储/获取本地或会话存储,封装存储工具,结合pina使用存储
前端·javascript·vue.js
zzz100664 小时前
Web 与 Nginx 网站服务:从基础到实践
运维·前端·nginx
良木林4 小时前
JS对象进阶
前端·javascript