flutter状态管理:Riverpod 入门
熟悉我的朋友应该都知道,我好几年前写过一个「Flutter状态管理之路」系列,那个时候介绍的是Provider,这也是官方推荐的状态管理工具,但当时没有写完,因为写着写着,觉得有很多地方不尽人意,用着很别扭,所以在写了7篇文章之后,就暂时搁置了。
一晃时间过了这么久,Flutter内部依然没有一个能够碾压一切的状态管理框架,GetX可能是,但是我觉得不是,InheritedWidget系的状态管理,才应该是正统的状态管理。
最近在留意Provider的后续进展时,意外发现了一个新的库——Riverpod,号称是新一代的状态管理工具,仔细一看,嘿,居然还是Provider的作者,好家伙,这是搬起石头砸自己的脚啊。
就像作者所说,Riverpod就是对Provider的重写,可不是吗,字母都没变,就换了个顺序,这名字也是取的博大精深。
其实Provider在使用上已经非常不错了,只不过随着Flutter的更加深入,大家对它的需求也就越来越高,特别是对Provider中因为InheritedWidget层次问题导致的异常和BuildContext的使用这些问题诟病很多,而Riverpod,正是在Provider的基础上,探索出了一条心的状态管理之路。
大家可以先把官方文档看一看 riverpod.dev ,看完之后发现还是一脸懵逼,那就对了,Riverpod和Provider一样,有很多类型的Provider,分别用于不同的场景,所以,理清这些Provider的不同作用和使用场景,对于我们用好Riverpod是非常有帮助的。
官网的文档,虽然是作者精心编写的,但它的教程,站在的是一个创作者的角度,所以很多入门的初学者看上去会有点摸不清方向,所以,这才有了这个系列的文章。
我将在这个系列中,带领大家对文档进行一次精读,进行一次赏析,本文不全是对文档的翻译,而且讲解的顺序也不一样,所以,如果你想入门Riverpod进行状态管理,那么本文一定是你的最佳选择。
Provider第一眼
首先,我们为什么要进行状态管理,状态管理是解决申明式UI开发,关于数据状态的一个处理操作,例如Widget A依赖于同级的Widget B的数据,那么这个时候,就只能把数据状态上提到它们的父类,但是这样比较麻烦,Riverpod和Provider这样的状态管理框架,就是为了解决类似的问题而产生的。
将一个state包裹在一个Provider中可以有下面一些好处。
- 允许在多个位置轻松访问该状态。Provider可以完全替代Singletons、Service Locators、依赖注入或InheritedWidgets等模式
- 简化了这个状态与其他状态的结合,你有没有为,如何把多个对象合并成一个而苦恼过?这种场景可以直接在Provider内部实现
- 实现了性能优化。无论是过滤Widget的重建,还是缓存昂贵的状态计算;Provider确保只有受状态变化影响的部分才被重新计算
- 增加了你的应用程序的可测试性。使用Provider,你不需要复杂的setUp/tearDown步骤。此外,任何Provider都可以被重写,以便在测试期间有不同的行为,这可以轻松地测试一个非常具体的行为
- 允许与高级功能轻松集成,如logging或pull-to-refresh
首先,我们通过一个简单的例子,来感受下,Riverpod是怎么进行状态管理的。
Provider是Riverpod应用程序中最重要的部分。Provider是一个对象,它封装了一个state并允许监听该state。Provider有很多变体形式,但它们的工作方式都是一样的。
最常见的用法是将它们声明为全局常量,例如下面这样。
final myProvider = Provider((ref) {
return MyValue();
});
不要被Provider的全局变量所吓倒。Provider是完全final的。声明一个Provider与声明一个函数没有什么不同,而且Provider是可测试和可维护的。
这段代码由三个部分组成。
- final myProvider,一个变量的声明。这个变量是我们将来用来读取我们Provider的状态的。Provider应该始终是final的
- Provider,我们决定使用的Provider类型。Provider是所有Provider类型中最基本的。它暴露了一个永不改变的对象。我们可以用其他Provider如StreamProvider或StateNotifierProvider来替换Provider,以改变值的交互方式
- 一个创建共享状态的函数。该函数将始终接收一个名为ref的对象作为参数。这个对象允许我们读取其他Provider,在我们Provider的状态将被销毁时执行一些操作,以及其它一些事情
传递给Provider的函数返回的对象的类型,取决于所使用的Provider。例如,一个Provider的函数可以创建任何对象。另一方面,StreamProvider的回调将被期望返回一个Stream。
你可以不受限制地声明你想要的多个Provider。与使用package:provider不同的是,Riverpod允许创建多个暴露相同 "类型 "的状态的provider。
final cityProvider = Provider((ref) => 'London'); final countryProvider = Provider((ref) => 'England');
两个Provider都创建了一个字符串,但这并没有任何问题。
为了使Provider发挥作用,您必须在Flutter应用程序的根部添加ProviderScope。
void main() {
runApp(ProviderScope(child: MyApp()));
}
以上就是Riverpod最简单的使用,我们看下完整的示例代码。
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
// We create a "provider", which will store a value (here "Hello world").
// By using a provider, this allows us to mock/override the value exposed.
final helloWorldProvider = Provider((_) => 'Hello world');
void main() {
runApp(
// For widgets to be able to read providers, we need to wrap the entire
// application in a "ProviderScope" widget.
// This is where the state of our providers will be stored.
ProviderScope(
child: MyApp(),
),
);
}
// Extend ConsumerWidget instead of StatelessWidget, which is exposed by Riverpod
class MyApp extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final String value = ref.watch(helloWorldProvider);
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: const Text('Example')),
body: Center(
child: Text(value),
),
),
);
}
}
可以发现,Riverpod的使用比package:Provider还要简单,申明一个全局变量来管理状态数据,然后就可以在任意地方获取数据了。
如何读取Provider的状态值
在有了一个简单的了解后,我们先来了解下关于状态中的「读」。
在Riverpod中,我们不像package:Provider那样需要依赖BuildContext,取而代之的是一个「ref」变量。这个东西,就是联系存取双方的纽带,这个对象允许我们与Provider互动,不管是来自一个Widget还是另一个Provider。
从Provider中获取ref
所有Provider都有一个 "ref "作为参数。
final provider = Provider((ref) {
// use ref to obtain other providers
final repository = ref.watch(repositoryProvider);
return SomeValue(repository);
})
这个参数可以安全地传递给其它Provider或者类,来获取所需要的值。
例如,一个常见的用例是将Provider的 "ref "传递给一个StateNotifier。
final counterProvider = StateNotifierProvider<Counter, int>((ref) {
return Counter(ref);
});
class Counter extends StateNotifier<int> {
Counter(this.ref): super(0);
final Ref ref;
void increment() {
// Counter can use the "ref" to read other providers
final repository = ref.read(repositoryProvider);
repository.post('...');
}
}
这样做,可以使我们的Counter类能够读取Provider。
这种方式是联系组件和Provider的一个重要方式。
从Widget中获取ref
Widgets自然没有一个ref参数。但是Riverpod提供了多种解决方案来从widget中获得这个参数。
扩展ConsumerWidget
在widget树中获得一个ref的最常见的方法是用ConsumerWidget代替StatelessWidget。
ConsumerWidget在使用上与StatelessWidget相同,唯一的区别是它的构建方法上有一个额外的参数:"ref "对象。
一个典型的ConsumerWidget看起来像这样。
class HomeView extends ConsumerWidget {
const HomeView({Key? key}): super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) {
// use ref to listen to a provider
final counter = ref.watch(counterProvider);
return Text('$counter');
}
}
扩展ConsumerStatefulWidget
与ConsumerWidget类似,ConsumerStatefulWidget和ConsumerState相当于一个带有状态的StatefulWidget,不同的是,state有一个 "ref "对象。
这一次,"ref "不是作为构建方法的参数传递,而是作为ConsumerState对象的一个属性。
class HomeView extends ConsumerStatefulWidget {
const HomeView({Key? key}): super(key: key);
@override
HomeViewState createState() => HomeViewState();
}
class HomeViewState extends ConsumerState<HomeView> {
@override
void initState() {
super.initState();
// "ref" can be used in all life-cycles of a StatefulWidget.
ref.read(counterProvider);
}
@override
Widget build(BuildContext context) {
// We can also use "ref" to listen to a provider inside the build method
final counter = ref.watch(counterProvider);
return Text('$counter');
}
}
通过ref来获取状态
现在我们有了一个 "ref",我们可以开始使用它。
ref "有三个主要用途。
- 获得一个Provider的值并监听变化,这样,当这个值发生变化时,这将重建订阅该值的Widget或Provider。这是通过ref.watch完成的
- 在一个Provider上添加一个监听器,以执行一个action,如导航到一个新的页面或在该Provider发生变化时执行一些操作。这是通过 ref.listen 完成的
- 获取一个Provider的值,同时忽略它的变化。当我们在一个事件中需要一个Provider的值时,这很有用,比如 "点击操作"。这是通过ref.read完成的
只要有可能,最好使用 ref.watch 而不是 ref.read 或 ref.listen 来实现一个功能。 通过依赖ref.watch,你的应用程序变得既是反应式的又是声明式的,这使得它更容易维护。
通过ref.watch观察Provider的状态
ref.watch在Widget的构建方法中使用,或者在Provider的主体中使用,以使得Widget/Provider可以监听另一个Provider。
例如,Provider可以使用 ref.watch 来将多个Provider合并成一个新的值。
一个例子是过滤一个todo-list,我们需要两个Provider。
- filterTypeProvider,一个暴露当前过滤器类型的Provider(None,表示只显示已完成的任务)
- todosProvider,一个暴露整个任务列表的Provider
通过使用ref.watch,我们可以制作第三个Provider,结合这两个Provider来创建一个过滤后的任务列表。
final filterTypeProvider = StateProvider<FilterType>((ref) => FilterType.none);
final todosProvider = StateNotifierProvider<TodoList, List<Todo>>((ref) => TodoList());
final filteredTodoListProvider = Provider((ref) {
// obtains both the filter and the list of todos
final FilterType filter = ref.watch(filterTypeProvider);
final List<Todo> todos = ref.watch(todosProvider);
switch (filter) {
case FilterType.completed:
// return the completed list of todos
return todos.where((todo) => todo.isCompleted).toList();
case FilterType.none:
// returns the unfiltered list of todos
return todos;
}
});
有了这段代码,filteredTodoListProvider现在就可以管理过滤后的任务列表。
如果过滤器或任务列表发生变化,过滤后的列表也会自动更新。同时,如果过滤器和任务列表都没有改变,过滤后的列表将不会被重新计算。
类似地,一个Widget可以使用ref.watch来显示来自Provider的内容,并在该内容发生变化时更新用户界面。
final counterProvider = StateProvider((ref) => 0);
class HomeView extends ConsumerWidget {
const HomeView({Key? key}): super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) {
// use ref to listen to a provider
final counter = ref.watch(counterProvider);
return Text('$counter');
}
}
这段代码显示了一个Widget,它监听了一个存储计数的Provider。如果该计数发生变化,该Widget将重建,用户界面将更新以显示新的值。
ref.watch方法不应该被异步调用,比如在ElevatedButton的onPressed中。也不应该在initState和其他State的生命周期内使用它。在这些情况下,考虑使用 ref.read 来代替。
通过ref.listen监听Provider的变化
与ref.watch类似,可以使用ref.listen来观察一个Provider。
它们之间的主要区别是,如果被监听的Provider发生变化,使用ref.listen不会重建widget/provider,而是会调用一个自定义函数。
这对于在某个变化发生时执行某些操作是很有用的,比如在发生错误时显示一个snackbar。
ref.listen方法需要2个参数,第一个是Provider,第二个是当状态改变时我们要执行的回调函数。回调函数在被调用时将被传递2个值,即先前状态的值和新状态的值。
ref.listen方法也可以在Provider的体内使用。
final counterProvider = StateNotifierProvider<Counter, int>((ref) => Counter(ref));
final anotherProvider = Provider((ref) {
ref.listen<int>(counterProvider, (int? previousCount, int newCount) {
print('The counter changed $newCount');
});
// ...
});
或在一个Widget的Build方法中使用。
final counterProvider = StateNotifierProvider<Counter, int>((ref) => Counter(ref));
class HomeView extends ConsumerWidget {
const HomeView({Key? key}): super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) {
ref.listen<int>(counterProvider, (int? previousCount, int newCount) {
print('The counter changed $newCount');
});
return Container();
}
}
ref.listen也不应该被异步调用,比如在ElevatedButton的onPressed中。也不应该在initState和其他State的生命周期内使用它。
通过ref.read来读取Provider的状态
ref.read方法是一种在不监听的情况下获取Provider的状态的方法。
它通常用于由用户交互触发的函数中。例如,当用户点击一个按钮时,我们可以使用ref.read来增加一个计数器的值。
final counterProvider = StateNotifierProvider<Counter, int>((ref) => Counter(ref));
class HomeView extends ConsumerWidget {
const HomeView({Key? key}): super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) {
return Scaffold(
floatingActionButton: FloatingActionButton(
onPressed: () {
// Call `increment()` on the `Counter` class
ref.read(counterProvider.notifier).increment();
},
),
);
}
}
应该尽可能地避免使用ref.read,因为它不是响应式的。
它存在于使用watch或listen会导致问题的情况下。如果可以的话,使用watch/listen几乎总是更好的,尤其是watch。
关于ref.read到底什么时候用
首先,永远不要在Widget的build函数中直接使用ref.read。
你可能很想使用ref.read来优化一个Widget的性能,例如通过下面的代码来实现。
final counterProvider = StateProvider((ref) => 0);
Widget build(BuildContext context, WidgetRef ref) {
// use "read" to ignore updates on a provider
final counter = ref.read(counterProvider.notifier);
return ElevatedButton(
onPressed: () => counter.state++,
child: const Text('button'),
);
}
但这是一种非常糟糕的做法,会导致难以追踪的错误。
以这种方式使用 ref.read 通常与这样的想法有关:"Provider所暴露的值永远不会改变,所以使用'ref.read'是安全的"。这个假设的问题是,虽然今天该Provider可能确实从未更新过它的值,但不能保证明天也是如此。
软件往往变化很大,而且很可能在未来,一个以前从未改变的值需要改变。
如果你使用ref.read,当这个值需要改变时,你必须翻阅整个代码库,将ref.read改为ref.watch--这很容易出错,而且你很可能会忘记一些情况。
如果你一开始就使用ref.watch,你在重构时就会减少问题。
但是如果我想用ref.read来减少我的widget重构的次数呢?
虽然这个目标值得称赞,但需要注意的是,你可以用ref.watch代替来达到完全相同的效果(减少构建的次数)。
Provider提供了各种方法来获得一个值,同时减少重建的次数,你可以用这些方法来代替。
例如下面的代码(bad)。
final counterProvider = StateProvider((ref) => 0);
Widget build(BuildContext context, WidgetRef ref) {
StateController<int> counter = ref.read(counterProvider.notifier);
return ElevatedButton(
onPressed: () => counter.state++,
child: const Text('button'),
);
}
我们可以这样改。
final counterProvider = StateProvider((ref) => 0);
Widget build(BuildContext context, WidgetRef ref) {
StateController<int> counter = ref.watch(counterProvider.notifier);
return ElevatedButton(
onPressed: () => counter.state++,
child: const Text('button'),
);
}
这两个片段代码都达到了同样的效果:当计数器增加时,我们的按钮将不会重建。
另一方面,第二种方法支持计数器被重置的情况。例如,应用程序的另一部分可以调用。
ref.refresh(counterProvider);
这将重新创建StateController对象。
如果我们在这里使用ref.read,我们的按钮仍然会使用之前的StateController实例,而这个实例已经被弃置,不应该再被使用。
而使用ref.watch则可以正确地重建按钮,使用新的StateController。
关于ref.read可以读哪些值
根据你想监听的Provider,你可能有多个可能的值可以监听。
作为一个例子,考虑下面的StreamProvider。
final userProvider = StreamProvider<User>(...);
当读取这个userProvider时,你可以像下面这样。
- 通过监听userProvider本身同步读取当前状态。
Widget build(BuildContext context, WidgetRef ref) {
AsyncValue<User> user = ref.watch(userProvider);
return user.when(
loading: () => const CircularProgressIndicator(),
error: (error, stack) => const Text('Oops'),
data: (user) => Text(user.name),
);
}
- 通过监听userProvider.stream来获得相关的Stream。
Widget build(BuildContext context, WidgetRef ref) {
Stream<User> user = ref.watch(userProvider.stream);
}
- 通过监听userProvider.future获得一个Future,该Future以最新发出的值进行解析。
Widget build(BuildContext context, WidgetRef ref) {
Future<User> user = ref.watch(userProvider.future);
}
其他Provider可能提供不同的替代值。
欲了解更多信息,请查阅API参考资料,参考每个Provider的API文档。
通过select来控制精确的读范围
最后要提到的一个与读取Provider有关的功能是,能够减少Widget/Provider从ref.watch重建的次数,或者ref.listen执行函数的频率的功能。
这一点很重要,因为默认情况下,监听一个Provider会监听整个对象的状态。但有时,一个Widget/Provider可能只关心一些属性的变化,而不是整个对象。
例如,一个Provider可能暴露了一个User对象。
abstract class User {
String get name;
int get age;
}
但一个Widget可能只使用用户名。
Widget build(BuildContext context, WidgetRef ref) {
User user = ref.watch(userProvider);
return Text(user.name);
}
如果我们简单地使用ref.watch,当用户的年龄发生变化时,这将重建widget。
解决方案是使用select来明确地告诉Riverpod我们只想监听用户的名字属性。
更新后的代码将是这样。
Widget build(BuildContext context, WidgetRef ref) {
String name = ref.watch(userProvider.select((user) => user.name));
return Text(name);
}
通过使用select,我们能够指定一个函数来返回我们关心的属性。
每当用户改变时,Riverpod将调用这个函数并比较之前和新的结果。如果它们是不同的(例如当名字改变时),Riverpod将重建Widget。然而,如果它们是相等的(例如当年龄改变时),Riverpod将不会重建Widget。
这个场景也可以使用select和ref.listen。
ref.listen<String>( userProvider.select((user) => user.name), (String? previousName, String newName) { print('The user name changed $newName'); } );
这样做也将只在名称改变时调用listener。
另外,你不一定要返回对象的一个属性。任何覆盖==的值都可以使用。例如,你可以这样做。
final label = ref.watch(userProvider.select((user) => 'Mr ${user.name}'));
读取状态,是一个非常重要的部分,什么时候用什么样的方式来读,都会有不同的效果。
ProviderObserver
ProviderObserver可以监听一个ProviderContainer的变化。
要使用它,你可以扩展ProviderObserver类并覆盖你想使用的方法。ProviderObserver有三个方法。
- didAddProvider:在每次初始化一个Provider时被调用
- didDisposeProvider:在每次销毁Provider的时候被调用
- didUpdateProvider:每次在Provider更新时都会被调用
ProviderObserver的一个简单用例是通过覆盖didUpdateProvider方法来记录Provider的变化。
// A Counter example implemented with riverpod with Logger
class Logger extends ProviderObserver {
@override
void didUpdateProvider(
ProviderBase provider,
Object? previousValue,
Object? newValue,
ProviderContainer container,
) {
print('''
{
"provider": "${provider.name ?? provider.runtimeType}",
"newValue": "$newValue"
}''');
}
}
void main() {
runApp(
// Adding ProviderScope enables Riverpod for the entire project
// Adding our Logger to the list of observers
ProviderScope(observers: [Logger()], child: const MyApp()),
);
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return MaterialApp(home: Home());
}
}
final counterProvider = StateProvider((ref) => 0, name: 'counter');
class Home extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final count = ref.watch(counterProvider);
return Scaffold(
appBar: AppBar(title: const Text('Counter example')),
body: Center(
child: Text('$count'),
),
floatingActionButton: FloatingActionButton(
onPressed: () => ref.read(counterProvider.notifier).state++,
child: const Icon(Icons.add),
),
);
}
}
现在,每当我们的Provider的值被更新时,logger将记录它。
I/flutter (16783): {
I/flutter (16783): "provider": "counter",
I/flutter (16783): "newValue": "1"
I/flutter (16783): }
对于诸如StateController(StateProvider.state的状态)和ChangeNotifier等可改变的状态,previousValue和newValue将是相同的。因为它们引用的是同一个StateController / ChangeNotifier。
这些是对Riverpod的最基本了解,但是却是很重要的部分,特别是如何对状态值进行读取,这是我们用好Riverpod的核心。
推荐原作者博客: xuyisheng.top/