前沿
说到 Flutter 的状态管理框架,我们耳熟能详的有 Provider、Riverpod、Bloc 以及大名鼎鼎的 Getx 等,可谓是数不胜数。但要说其中哪一个更优秀,那可能 一千个人有一千个哈姆雷特
。今天我们就来介绍其中的 Riverpod,它也是 Provider 的开发团队 dash-overflow.net 针对 Provider 存在的缺点,重新开发设计的一个状态管理框架,由此可见其价值。
一、什么是状态管理?
在 Flutter 中,状态管理是指在应用中管理和修改数据的过程。简单来说,就是更新页面中的数据。
Flutter 的状态管理方式有很多种,我们可以根据自己的需要选择合适的方式来管理应用的状态。一些常见的状态管理方式包括:
-
局部状态:局部状态是指只影响单个组件的状态。可以使用 Flutter 的
setState
方法来更新局部状态。 -
全局状态:全局状态是指影响整个应用的状态。可以使用 Flutter 的
Provider
插件来管理全局状态。 -
共享状态:共享状态是指影响多个组件的状态。可以使用 Flutter 的
InheritedWidget
来共享状态。
二、认识Riverpod
1. Rivepod插件介绍
Riverpod(即 Provider 的变位词)适用于 Flutter / Dart 的响应式缓存框架。它可以自动获取、缓存、组合和重新计算网络请求,同时能还为你处理错误。
Riverpod 通过提供一种新的、独特的方式来编写业务逻辑,灵感来自 Flutter 的 Widget。Riverpod 在许多方面都与 Widget 类似,但在状态方面不同。
使用这种新方法,那些复杂的特性大部分都是默认完成,我们只需要关注 UI 即可。
例如下面的代码片段是使用 Riverpod 实现的 Pub.dev 应用的简化代码:
// 从pub.dev中获取package的列表
@riverpod
Future<List<Package>> fetchPackages(
FetchPackagesRef ref, {
required int page,
String search = '',
}) async {
final dio = Dio();
// 取一个API。这里我们使用的是package:dio,但我们也可以使用其他任何东西
final response = await dio.get(
'https://pub.dartlang.org/api/search?page=$page&q=${Uri.encodeQueryComponent(search)}',
);
// 将JSON响应解码为Dart类
final json = response.data as List;
return json.map(Package.fromJson).toList();
}
这段代码是“输入搜索”所需的全部业务逻辑,通过 @riverpod
注解的方式生成 Riverpod 代码。
2. 开始使用
- pubspec.yaml 引入 package
name: my_app_name
environment:
sdk: ">=2.17.0 <3.0.0"
flutter: ">=3.0.0"
dependencies:
flutter:
sdk: flutter
flutter_riverpod: ^2.1.1
riverpod_annotation: ^1.0.6
dev_dependencies:
build_runner:
riverpod_generator: ^1.0.6
- 使用例子:计数器
void main() {
runApp(
// 添加ProviderScope可以使Riverpod适用于整个项目
const ProviderScope(child: MyApp()),
);
}
/// provider是全局声明的,并指定如何创建一个状态
final counterProvider = StateProvider((ref) => 0);
class Home extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
return Scaffold(
body: Center(
// Consumer是一个允许读取providers的widget
child: Consumer(
builder: (context, ref, _) {
final count = ref.watch(counterProvider);
return Text('$count');
},
),
),
floatingActionButton: FloatingActionButton(
// read方法用于更新provider的值
onPressed: () => ref.read(counterProvider.notifier).state++,
child: const Icon(Icons.add),
),
);
}
}
3. Riverpod和provider之间的关系
Riverpod 是在寻找解决 Provider 所面临的各种技术限制的方案时诞生的。最初,Riverpod 被认为是 Provider 中解决这一问题的一个主要版本。但开发团队最终还是决定不这么做,因为这将是一个相当大的突破性变化,而 Provider 是最常用的 Flutter 包之一。
尽管如此,Riverpod 和 Provider 在概念上还是相当相似的。两个包都扮演类似的角色。两者都试图:
- 缓存并处理一些有状态的对象
- 提供一种在测试期间模拟这些对象的方法
- 为 Widgets 提供了一种简单的方式来监听这些对象。
Riverpod 修复了 Provider 的各种基本问题,例如但不限于:
- 显著简化了 “Providers” 的组合。Riverpod 没有使用繁琐且容易出错的
ProxyProvider
工具,而是使用了简单且强大的实用工具,例如ref.watch
和ref.listen
。 - 允许多个 “provider” 公开同一类型的值。这消除了在使用
int
orString
等类型值时定义自定义类的需要。 - 无需在测试中重新定义 providers。在 Riverpod 中,provider 默认情况下可以使用内部测试。
- 通过提供处理对象的替代方法( autoDispose )来减少对“作用域”处理对象的过度依赖。尽管功能强大,但确定提供者的作用域是相当高级的,而且很难做到正确。
4. 缺点
Riverpod 唯一的缺点是它需要改变 Widget类型 才能工作:
- 在 Riverpod 中,你应该继承 ConsumerWidget ,而不是继承 StatelessWidget 。
- 在 Riverpod 中,你应该继承 ConsumerStatefulWidget ,而不是继承 StatefulWidget 。
三、Providers
1. Provider
provier
是所有 providers 中的最基础的类。provider
通用用于:
- 缓存计算
- 向其他 providers 公开一个值(例如Respository / HttpClient)
- 为测试或 widget 提供覆盖值的方法
- 在非必要情况下减少 providers / Widget 的 rebuild 次数
使用示例:
假设我们的应用中有一个 StateNotifierProvider
来操作待办事项列表:
@riverpod
class Todos extends _$Todos {
@override
List<Todo> build() {
return [];
}
void addTodo(Todo todo) {
state = [...state, todo];
}
}
我们可以使用 Providertodos
的筛选列表,仅显示已完成的 todos:
@riverpod
List<Todo> completedTodos(CompletedTodosRef ref) {
final todos = ref.watch(todosProvider);
// 我们只返回完成的todo
return todos.where((todo) => todo.isCompleted).toList();
}
我们的 UI 现在可以通过监听来显示已完成的待办事项列表 completedTodosProvider
:
Consumer(builder: (context, ref, child) {
final completedTodos = ref.watch(completedTodosProvider);
// TODO 使用ListView/GridView/...显示已完成列表
});
2. StateNotifierProvider
StateNotifierProvider
是一个用于监听和公开 StateNotifier
的 providers(来自 Riverpod 重新导出的包 state_notifier) StateNotifierProvider
与 StateNotifier
一起是 Riverpod 推荐的用于管理状态的解决方案。 它通常用于:
- 公开一个不可变的状态,它可以在对自定义事件做出反应后随时间改变。
- 将修改某些状态的逻辑(也称为“业务逻辑”)集中在一个地方,随着时间的推移提高可维护性。
用法示例:
我们可以 StateNotifierProvider
用来实现一个待办事项列表,例如 addTodo
让 UI 修改与用户交互的待办事项列表:
// StateNotifier的状态应该是不可变的。
// 也可以使用像frozen这样的包来帮助实现。
@immutable
class Todo {
const Todo({required this.id, required this.description, required this.completed});
// 类中的所有属性都应该是final。
final String id;
final String description;
final bool completed;
// 由于Todo是不可变的,我们实现了一个方法,允许clone
// 内容略有不同的Todo。
Todo copyWith({String? id, String? description, bool? completed}) {
return Todo(
id: id ?? this.id,
description: description ?? this.description,
completed: completed ?? this.completed,
);
}
}
// 传递给StateNotifierProvider的StateNotifier类
// 这个类不应该在其"state"属性之外暴露状态,这意味着
// 没有公共getter属性
// 这个类的public方法将允许UI修改状态
class TodosNotifier extends StateNotifier<List<Todo>> {
// 将待办事项列表初始化为一个空列表
TodosNotifier(): super([]);
// 允许UI添加待办事项。
void addTodo(Todo todo) {
// 因为我们的状态是不可变的,所以不允许执行' state.add(todo) '
// 相反,我们应该创建一个新的待办事项列表,其中包含以前的项目和新的项目
// 在这里使用Dart的展开运算符是有帮助的
state = [...state, todo];
// 调用"state =" 不需要调用“notifyListeners”或类似的方法。
// 会在必要时自动rebuild UI
}
// 允许删除待办事项
void removeTodo(String todoId) {
// 同样,我们的状态是不可变的。所以我们创建了一个新的列表,而不是改变现有的列表。
state = [
for (final todo in state)
if (todo.id != todoId) todo,
];
}
// 把待办事项标记为已完成
void toggle(String todoId) {
state = [
for (final todo in state)
// 只标记已完成的匹配todo
if (todo.id == todoId)
// 同样,因为我们的状态是不可变的,所以需要创建todo的副本
// 使用之前实现的copyWith方法来帮助实现
todo.copyWith(completed: !todo.completed)
else
// other todos are not modified
todo,
];
}
}
// 最后,我们使用StateNotifierProvider来允许UI与我们的TodosNotifier类交互
final todosProvider = StateNotifierProvider<TodosNotifier, List<Todo>>((ref) {
return TodosNotifier();
});
现在我们已经定义了一个 StateNotifierProvider
,我们可以使用它与 UI 中的待办事项列表进行交互:
class TodoListView extends ConsumerWidget {
const TodoListView({Key? key}): super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) {
// 当待办事项列表更改时重新构建Widget
List<Todo> todos = ref.watch(todosProvider);
// 在一个可滚动的列表视图中呈现待办事项
return ListView(
children: [
for (final todo in todos)
CheckboxListTile(
value: todo.completed,
// 当点击待办事项时,更改其完成状态
onChanged: (value) => ref.read(todosProvider.notifier).toggle(todo.id),
title: Text(todo.description),
),
],
);
}
}
3. FutureProvider
FutureProvider
等价于 Provier
但用于异步代码。
FutureProvider
通常用于:
- 执行和缓存异步操作(例如网络请求)
- 很好地处理异步操作的错误/加载状态
- 将多个异步值组合成另一个值
使用示例:读取配置
/// 定义FutureProvider
@riverpod
Future<Configuration> fetchConfigration(FetchConfigrationRef ref) async {
final content = json.decode(
await rootBundle.loadString('assets/configurations.json'),
) as Map<String, Object?>;
return Configuration.fromJson(content);
}
/// 更新UI
Widget build(BuildContext context, WidgetRef ref) {
final config = ref.watch(fetchConfigrationProvider);
return config.when(
loading: () => const CircularProgressIndicator(),
error: (err, stack) => Text('Error: $err'),
data: (config) {
return Text(config.host);
},
);
}
4. StreamProvider
StreamProvider
类似于 FutureProvider
但用于 Stream
而不是 Future
。
StreamProvider
通常用于:
- 听 Firebase 或网络套接字
- 每隔几秒重建另一个供应商
使用 StreamProviderStreamBuilder
有很多好处:
- 它允许其他 providers 使用
ref.watch
收听流 - 得益于
AsyncValue
,它确保正确处理加载和错误情况 - 它消除了区分广播流和普通流的需要
- 它缓存流发出的最新值,确保如果在发出事件后添加侦听器,侦听器仍然可以立即访问最新的事件
- 它允许通过覆盖
StreamProvider
5. StateProvider
StateProvider
是一个提供者,它公开了一种修改其状态的方法。它是 StateNotifierProvider
的简化版,旨在避免为非常简单的用例编写 StateNotifier
类
StateProvider
存在的主要目的是允许 通过用户界面修改简单变量。 StateProvider
的使用通常是以下情景之一:
- 枚举,例如过滤器类型
- 字符串,通常是文本字段的原始内容
- 布尔值,用于复选框
- 数字,用于分页或年龄表单字段
如果出现以下情况,则不应使用 StateProvider
:
- 你的状态需要验证逻辑
- 您的状态是一个复杂的对象(例如自定义类、列表/地图……)
- 修改状态的逻辑比简单的
count++
.
使用示例:使用下拉菜单更改过滤器类型
为了简单起见,我们将获得的产品列表将直接构建在应用中,如下所示:
class Product {
Product({required this.name, required this.price});
final String name;
final double price;
}
final _products = [
Product(name: 'iPhone', price: 999),
Product(name: 'cookie', price: 2),
Product(name: 'ps5', price: 500),
];
final productsProvider = Provider<List<Product>>((ref) {
return _products;
});
然后,用户界面可以通过执行以下操作来显示产品列表:
Widget build(BuildContext context, WidgetRef ref) {
final products = ref.watch(productsProvider);
return Scaffold(
body: ListView.builder(
itemCount: products.length,
itemBuilder: (context, index) {
final product = products[index];
return ListTile(
title: Text(product.name),
subtitle: Text('${product.price} \$'),
);
},
),
);
}
我们可以添加一个下拉列表,这将允许按价格或按名称过滤我们的产品:
// 表示筛选器类型的枚举
enum ProductSortType {
name,
price,
}
Widget build(BuildContext context, WidgetRef ref) {
final products = ref.watch(productsProvider);
return Scaffold(
appBar: AppBar(
title: const Text('Products'),
actions: [
DropdownButton<ProductSortType>(
value: ProductSortType.price,
onChanged: (value) {},
items: const [
DropdownMenuItem(
value: ProductSortType.name,
child: Icon(Icons.sort_by_alpha),
),
DropdownMenuItem(
value: ProductSortType.price,
child: Icon(Icons.sort),
),
],
),
],
),
body: ListView.builder(
// ...
),
);
}
现在我们有了一个下拉列表,让我们创建一个下拉列表 StateProvider
并将其状态与我们的 providers 同步。
// 创建StateProvider
final productSortTypeProvider = StateProvider<ProductSortType>(
// 我们返回默认的排序类型,这里是name。
(ref) => ProductSortType.name,
);
// 通过以下方式将此提供程序与我们的下拉列表连接起来
DropdownButton<ProductSortType>(
// 当排序类型改变时,这将重新构建下拉列表以更新显示的图标。
value: ref.watch(productSortTypeProvider),
// 当用户与下拉菜单交互时,我们更新提供者状态。
onChanged: (value) =>
ref.read(productSortTypeProvider.notifier).state = value!,
items: [
// ...
],
),
// 更新productsProvider以对产品列表进行排序
final productsProvider = Provider<List<Product>>((ref) {
final sortType = ref.watch(productSortTypeProvider);
switch (sortType) {
case ProductSortType.name:
return _products.sorted((a, b) => a.name.compareTo(b.name));
case ProductSortType.price:
return _products.sorted((a, b) => a.price.compareTo(b.price));
}
});
就这样!此更改足以让用户界面在排序类型更改时自动重新呈现产品列表。
6. ChangeNotifierProvider
ChangeNotifierProvider
(仅限 flutter_riverpod / hooks_riverpod)是一个 provider
,用于从 Flutter 本身监听和公开 ChangeNotifier
。
Riverpod ChangeNotifierProvider
不鼓励使用,主要用于:
-
package:provider
从使用其时的轻松过渡ChangeNotifierProvider
- 支持可变状态,即使不可变状态是首选
使用可变状态而不是不可变状态有时会更有效。缺点是,它可能更难维护并且可能会破坏各种功能。 例如,provider.select
如果你的状态是可变的,那么使用来优化你的 Widget 的重建可能不起作用,因为 select
会认为该值没有改变。 因此,使用不可变数据结构有时会更快。因此,针对您的用例制定基准非常重要,以确保您通过使用 ChangeNotifierProvider
。
使用示例:
ChangeNotifierProvider
用来实现一个待办事项列表。这样做将允许我们公开方法,例如 addTodo
让 UI 修改用户交互的待办事项列表:
class Todo {
Todo({
required this.id,
required this.description,
required this.completed,
});
String id;
String description;
bool completed;
}
class TodosNotifier extends ChangeNotifier {
final todos = <Todo>[];
// 允许UI添加待办事项
void addTodo(Todo todo) {
todos.add(todo);
notifyListeners();
}
// 允许删除待办事项
void removeTodo(String todoId) {
todos.remove(todos.firstWhere((element) => element.id == todoId));
notifyListeners();
}
// 把待办事项标记为已完成
void toggle(String todoId) {
for (final todo in todos) {
if (todo.id == todoId) {
todo.completed = !todo.completed;
notifyListeners();
}
}
}
}
// 最后,使用StateNotifierProvider来允许UI与我们的TodosNotifier类交互。
final todosProvider = ChangeNotifierProvider<TodosNotifier>((ref) {
return TodosNotifier();
});
使用它与 UI 中的待办事项列表进行交互:
class TodoListView extends ConsumerWidget {
const TodoListView({Key? key}) : super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) {
// 当待办事项列表更改时重新构建widget
List<Todo> todos = ref.watch(todosProvider).todos;
// 在一个可滚动的列表视图中呈现待办事项
return ListView(
children: [
for (final todo in todos)
CheckboxListTile(
value: todo.completed,
// 当点击待办事项时,更改其完成状态
onChanged: (value) =>
ref.read(todosProvider.notifier).toggle(todo.id),
title: Text(todo.description),
),
],
);
}
}
四、关于Riverpod的代码生成
代码生成就是使用工具为我们生成代码的思想。
在 Dart 中,它的缺点是需要额外的步骤来“编译” 应用。虽然这个问题可能会在不久的将来得到解决,因为 Dart 团队正在研究这个问题的潜在解决方案。
在 Riverpod 的上下文中,代码生成是关于稍微改变定义 “providers” 的语法。例如:
final fetchUserProvider = FutureProvider.autoDispose.family<User, String>((ref, userId) async {
final json = await http.get('api/user/$userId');
return User.fromJson(json);
});
使用代码生成,我们会写:
@riverpod
Future<User> fetchUser(FetchUserRef ref, {required int userId}) async {
final json = await http.get('api/user/$userId');
return User.fromJson(json);
}
使用 Riverpod 时,代码生成是完全可选的。完全可以在没有的情况下使用 Riverpod。 同时,Riverpod 拥抱代码生成并推荐使用它。
五、关于Riverpod的钩子
“Hooks” 是独立于 Riverpod 的通用实用 package: flutter_hooks。 尽管 flutter_hooks 是一个完全独立的包并且与 Riverpod 没有任何关系(至少没有直接关系),但通常将 Riverpod 和 flutter_hooks 配对在一起。毕竟,Riverpod 和 flutter_hooks 是由同一个团队维护的。
钩子是完全可选的。你可以不必使用钩子,尤其是在你开始使用 Flutter 时。它们是强大的工具,但不是很像 Flutter。因此,首先从普通的 Flutter / Riverpod 开始可能是有意义的,一旦你有了更多的经验,再回到钩子上。
什么是钩子?
钩子是在小部件内部使用的函数。它们被设计为 StatefulWidget 的替代品,以使逻辑更具可重用性和可组合性。
如果 Riverpod 的提供者用于“全局” 应用,则挂钩用于本地 widget。钩子通常用于处理有状态的 UI 对象,例如 TextEditingController、 AnimationController。 它们还可以作为“构建器”模式的替代品,用不涉及“嵌套”的替代品替代,诸如 FutureBuilder / TweenAnimatedBuilder 之类的 Widget 显着提高可读性。
通常,钩子有助于:
- 形式
- 动画
- 响应用户事件
- ...
使用示例:
class FadeIn extends HookWidget {
const FadeIn({Key? key, required this.child}) : super(key: key);
final Widget child;
@override
Widget build(BuildContext context) {
// 创建一个AnimationController卸载Widget时,控制器将自动被处置。
final animationController = useAnimationController(
duration: const Duration(seconds: 2),
);
// useEffect相当于initState + didUpdateWidget + dispose
// 传递给useEffect的回调在钩子第一次出现时执行
// 被调用时执行,然后在作为第二个参数传递的列表发生变化时执行
// 因为我们在这里传递了一个空的const列表,这严格地等同于' initState '。
useEffect(() {
// 在widget第一次呈现时启动动画。
animationController.forward();
// 我们可以选择在这里返回一些“dispose”逻辑
return null;
}, const []);
// 告诉Flutter在动画更新时重新构建此Widget。
// 这相当于AnimatedBuilder
useAnimation(animationController);
return Opacity(
opacity: animationController.value,
child: child,
);
}
}
总结
今天先介绍了状态管理的一些基础概念,然后介绍了 Riverpod 和 Provider 之间的联系和差异,最后着重介绍了 Riverpod 的一些用法和 6 个 Provider,以及 Riverpod 的代码生成和钩子。下一篇我们将开始介绍Riverpod的特性和实现原理。
作者:Fitem
链接:https://juejin.cn/post/7182225820767748153
网友评论