开启生长之旅!这是我参与「日新计划 12 月更文应战」的第6天,点击检查活动详情
前沿
说到 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的特性和完结原理。