本文同步发布于公众号:移动开发那些事 Flutter高效开发利器:Riverpod框架简介及实践指南
1 简介
Riverpod
是一个用于 Flutter
的状态管理库,由 Provider
的作者开发,是对 Provider
的重构和改进,提供一种更灵活,更简洁和高效的状态管理工具,它具有以下几个关键特性:
- 类型安全: 在运行时和编译时都能获得类型检查,减少运行时错误,提高代码的可靠性和可维护性;
- 不依赖
BuildContext
:允许在不依赖BuildContext
的情况下访问和管理状态,简化了许多常见的Flutter
状态管理问题,使代码更加清晰和易于维护。 - 自动化状态清理:状态会在不再需要时自动清理,避免内存泄漏,提高了应用的性能和稳定性;
- 支持 "Scoped" 状态 提供了更强大的
scoping
机制,可以在不同的widget
树中有选择地管理和隔离状态,有助于更细粒度的状态管理和性能优化。
2 基础使用
Provider
是Riverpod
库的核心概念,主要负责管理和更新状态,可以将 Provider
看作是一个数据源,它负责提供数据,并在数据发生变化时通知相关的组件。它有以下几种常用的Provider
:
Provider
:基础只读值;FutureProvider
: 异步操作封装StreamProvider
: 流式数据处理NotifierProvider
: 提供一种更灵活的方式来管理状态和业务逻辑,支持任何类型的 "Notifier"AsyncNotifierProvider
: 专门用于管理异步操作的状态,如网络请求,它提供了一个结构化的方法来处理异步数据的加载、成功、错误和状态更新- 其他
使用之前,我们首先要在pubspec.yaml
文件中添加对应的依赖:
yaml
dependencies:
flutter_riverpod: ^2.3.6
# 可选的,这个主要是用来使用注解,自动生成对应的provider的
riverpod_annotation: ^2.1.1
dev_dependencies:
# 与注解相结合,自动生成dart文件
build_runner:
# Riverpod代码生成器
riverpod_generator: ^2.4.0
一个基础使用Riverpod
库的步骤为:
- 创建一个
Provider
: 创建一个Provider
,去观察某一个值,并在值有变化时去通知监听者; - 使用
Provider
: 使用Provider
的前提是使用了ProviderScope
来包裹对应的widget
; - 监听状态: 在对应的
widget
(这个widget
需要继承自ConsumerWidget
)中使用ref.watch
方法来监听对应的值; - 更新状态: 在对应的
widget
中使用ref.read
方法获取对应的监听器来修改状态值;
2.1 简单使用
最简单的使用Riverpod
的例子为
scala
// 步骤1 :通过StateProvider创建一个provider,监听一个int值,这个provider会对这个值的状态进行监听管理
final counterProvider = StateProvider<int>((ref) => 0);
// 步骤2 :用ProviderScope包裹对应的app
void main() {
runApp (const ProviderScope(child:CounterPage()));
}
// 步骤3 继承 ConsumerWidget 编写widget,用于监听provider的状态
class CounterPage extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
// 步骤4 监听前面定义的 counterProvider
final count = ref.watch(counterProvider);
return Scaffold(
// 只要counterProvider的状态有变化,这个text就会自动刷新
body: Text('Count: $count'),
floatingActionButton: FloatingActionButton(
onPressed: () {
// 步骤5(按需) 通过read方法改变其状态
ref.read(counterProvider.notifier).state++,
}
child: Icon(Icons.add),
),
);
}
}
3 高级使用
3.1 组合状态
在实际开发中,应用的状态管理往往会涉及到多个相互关联的状态和复杂的业务逻辑,可以多个独立的状态类组合到一个类中,从而实现某个功能统一的管理
scala
// 定义多个状态
final counterProvider = StateProvider((ref) => 0);
final nameProvider = StateProvider((ref) => "");
// 创建组合状态
class CombinedState {
final int counter;
final String name;
CombinedState(this.counter, this.name);
}
// 创建组合的Provider
final combinedProvider = Provider((ref) {
final counter = ref.watch(counterProvider);
final name = ref.watch(nameProvider);
return CombinedState(counter, name);
});
// 使用组合状态
class MyWidget extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final state = ref.watch(combinedProvider);
return Text('${state.counter}, ${state.name}');
}
}
3.2 特定状态
若你想仅关注某个状态里的特定值,可借助select
功能达成此目的。这种方式十分高效,因为它能让组件只在特定值发生变化时才进行重建。
scala
// StateProvider也可用StateNotifierProvider替代
// 假设我们有一个 User 状态
final userProvider = StateProvider((ref) => User(name: "John", age: 30));
class User {
final String name;
final int age;
User({required this.name, required this.age});
}
// 只监听 name 字段
class MyWidget extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
// 使用 select 只监听 name 字段的变化
final name = ref.watch(userProvider.select((user) => user.name));
return Text('Name: $name'); // 仅当 name 变化时重建
}
}
如果要用select
处理比较复杂的状态,如状态是包括列表的,或者是需要进行计算转换的,关键在于高效提取所需数据并确保正确的状态比较,这里有几点经验可参考:
- 针对状态包含列表,可通过
map
或where
过滤后提取数据:
javascript
ref.watch(
todosProvider.select((todos) => todos.where((todo) => !todo.completed).length)
- 避免返回对象,若要提取多个对象,优先返回一个元组而非新对象:
scss
final (name, age) = ref.watch(
userProvider.select((user) => (user.profile.name, user.profile.age)),
);
3.3 异步请求
使用FutureProvider
或者StreamProvider
可以很容易构建出响应式的数据请求,比如我们需要从另一个服务中获取到用户id后,再去请求这个用户的数据,那 这里的功能利用FutureProvider
可以很简洁地写出来:
dart
// 基于用户 ID 获取用户详情
// userIdProvider 用于 获取用户的id,这里provider可按需选用任意一个
final userIdProvider = StateProvider((ref) => 1);
// userDetailsProvider 用于获取用户的详情,这里接受一个userId的参数
final userDetailsProvider = FutureProvider.autoDispose.family<User, int>((ref, userId) async {
// 可监听其他状态(如网络状态)
final networkStatus = ref.watch(networkStatusProvider);
if (!networkStatus) throw Exception('No internet');
// 使用 userId 发起请求
return ref.watch(apiServiceProvider).getUser(userId);
});
// 使用方法
class UserDetailsWidget extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
// 先通过 userIdProvider 获取到用户id
final userId = ref.watch(userIdProvider);
// 再通过用户id,去获取用户详情 (这里也可以用前面组合的方式来写)
final userAsync = ref.watch(userDetailsProvider(userId));
return userAsync.when(
data: (user) => Text('User: ${user.name}'),
loading: () => CircularProgressIndicator(),
error: (err, stack) => Text('Error: $err'),
);
}
}
FutureProvider.autoDispose.family
:
autoDispose
: 在没有组件监听时,会自动取消异步操作并释放资源,再次监听时,会重新触发操作family
: 接收参数,动态生成不同的异步操作
而且FutureProvider
会自动处理异步操作的不同状态(加载中、数据返回、出错),通过when
方法可以根据不同状态展示相应的 UI。
3.4 其他
如果觉得手动写Provider
的方式比较繁锁,Riverpod
库也提供一注解的方式来简化,使用注解的话,需要增加一个依赖: riverpod_generator: ^2.4.0
typescript
// 生成FutureProvider
@riverpod
Future<User> fetchUser(FetchUserRef ref, String userId) async {
// 可监听其他 Provider
final apiClient = ref.watch(apiClientProvider);
// 执行异步操作
return apiClient.fetchUser(userId);
}
// 生成StreamProvider
@riverpod
Stream<int> counterStream(CounterStreamRef ref) {
return Stream.periodic(Duration(seconds: 1), (i) => i);
}
写完后,需要在命令行执行一个命令:dart run build_runner build
,然后就能很方便使用对应的Provider
了。
4 总结
本文主要探讨了Riverpod
框架的一些特点,包括它的的基础和高级使用方式,并希望通过本文能够帮助大家入门Riverpod
并在实际开发中充分发挥Riverpod
的优势,以一种更简洁,更方便的方式来管理应用的状态。