
如果你在过去一年多的时间里对Flutter包有一定的了解,你肯定听说过Riverpod,这是一个反应式缓存和数据绑定,或者像有些人说的那样,是状态管理包,是心爱的Provider的一种升级。实际上,在它的API还不稳定的时候,我就用一篇教程介绍过它。
从那时起,Riverpod已经有了长足的进步--它更成熟、更有帮助、更多变。所有这些变化自然意味着是时候编写一个新的教程,让你准备好充分利用Riverpod 2.0的力量,而且很可能也是其即将到来的版本。
无论你使用哪个版本,Riverpod的基本原则都是一样的;只是你做某些事情的方式不同而已。因此,如果你想了解Provider软件包的不足之处,以及为什么Riverpod这么好,请查看我的旧教程,在那里我对Provider做了更详细的介绍。本文将纯粹关注Riverpod,你不需要任何先前的知识就可以跟上。
如果你是一个完全的初学者,对状态管理没有什么经验,我建议你在继续学习Riverpod之前,先阅读这篇Flutter官方文章。
Riverpod的目的与InheritedWidget ,Provider,get_it,以及部分GetX等类和包有很多共同之处。也就是说,允许你在你的应用程序的不同部分访问对象,而不需要将各种回调和对象作为构造函数参数传递给Widgets。
那么,是什么让它与所有这些从各方提供给你的其他选项不同呢?是它将易用性、干净的编码实践、与Flutter的完全独立(对测试很有帮助!)、编译时的安全性(相对于处理运行时的错误)和性能优化结合在一个包里。为了实现这一切,Riverpod对你如何声明你想在你的应用程序周围提供的对象有一个独特的方法。
我们使用的包的版本是2.0.0-dev.5,所以要确保在你的pubspec中至少使用这个版本。一旦稳定版本发布,我们在本教程中写的一切也将有效。
pubspec.yaml
dependencies:
flutter:
sdk: flutter
flutter_riverpod: ^2.0.0-dev.5
提供者
让我们先处理一个最简单的例子,比如说你想让一个String ,以便在你的应用程序中都能访问。
main.dart
// Provider declaration is top-level (global)
final myStringProvider = Provider((ref) => 'Hello world!');
如果我们分解上面的这行代码,你刚刚声明了一个名为myStringProvider 的提供者,它将在你需要的地方提供 "Hello world!"String 。这是一个最基本的Provider ,只是暴露了一个只读的值。我们很快就会看一下其他更高级的提供者类型。ref 参数属于ProviderReference 类型,除其他事项外,它还用于与其他提供者进行交互--我们稍后也会探讨ref 参数。
从现在开始,我们将把提供的对象称为**"state"**
这个提供者的声明与声明一个类高度相似。类的声明是可以全局访问的,但是一旦你实例化了一个对象,它就不再是全局的了,这极大地帮助了应用程序的可维护性,并使测试成为可能,因为硬编码的全局的使用不允许嘲讽。
main.dart
class MyClass {
int myField;
MyClass(this.myField);
}
// The object has to be passed into the function.
// We can't access it globally.
void myFunction(MyClass object) {
object.myField = 5;
}
以同样的方式,一个提供者的声明是全局的,但它所提供的实际状态不是全局的。相反,它被存储和管理在一个叫做ProviderScope 的部件中,我们很快就会仔细研究这个部件,这使我们的应用程序具有可维护性和可测试性。从本质上讲,我们可以说,我们得到了全局变量的所有好处,而没有任何缺点。是的,有时听起来好得不能再好的事情确实是真的。
Riverpod & Widget Tree

现在让我们来看看一个更真实的例子--一个计数器应用!是的,正是那个你认为的计数器应用。是的,正是那个你已经厌倦了的计数器应用程序,但不要绝望,因为这次你会学到新的东西。我保证!
这里是最终的结果。
当然,我们将使用Riverpod来进行状态管理,而不是使用经典的StatefulWidget 。现在,你已经看到了最基本的Provider 类,它提供了只读的数据。由于我们想在用户按下按钮时增加计数器,我们显然也需要对数据进行写入。最简单的方法是用一个StateProvider 。
main.dart
final counterProvider = StateProvider((ref) => 0);
void main() => runApp(MyApp());
...
Riverpod实际上只是一种在应用程序周围提供对象的方式,虽然它同时内置了一些简单和高级的方法来管理状态,但你完全不限于此。你可以使用ChangeNotifier,Bloc,Cubit 或其他任何你想与Riverpod结合的方式。
哦,那我前面提到的单个ProviderScope widget呢?那只需要简单地将整个应用部件包裹在main 方法中。
main.dart
void main() {
runApp(
ProviderScope(
child: MyApp(),
),
);
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Counter App',
home: const HomePage(),
);
}
}
我们还为这个应用程序添加了一个小插曲,即不在 "主页 "路线中直接进行计数。相反,我们将使用户首先从HomePage ,导航到CounterPage widget,所以HomePage 将只包含一个按钮。
main.dart
class HomePage extends StatelessWidget {
const HomePage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Home'),
),
body: Center(
child: ElevatedButton(
child: const Text('Go to Counter Page'),
onPressed: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: ((context) => const CounterPage()),
),
);
},
),
),
);
}
}
正如你所看到的,到目前为止,我们还没有在任何地方使用counterProvider 。我们通过声明提供者本身和设置ProviderScope ,使其有可能被使用,但如果我们现在运行这个应用程序,就不会有实际的counterProvider ,更不用说利用和增加了。让我们在CounterPage 中改变这一点。
main.dart
// ConsumerWidget is like a StatelessWidget
// but with a WidgetRef parameter added in the build method.
class CounterPage extends ConsumerWidget {
const CounterPage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) {
// Using the WidgetRef to get the counter int from the counterProvider.
// The watch method makes the widget rebuild whenever the int changes value.
// - something like setState() but automatic
final int counter = ref.watch(counterProvider);
return Scaffold(
appBar: AppBar(
title: const Text('Counter'),
),
body: Center(
child: Text(
counter.toString(),
style: Theme.of(context).textTheme.displayMedium,
),
),
floatingActionButton: FloatingActionButton(
child: const Icon(Icons.add),
onPressed: () {
// Using the WidgetRef to read() the counterProvider just one time.
// - unlike watch(), this will never rebuild the widget automatically
// We don't want to get the int but the actual StateNotifier, hence we access it.
// StateNotifier exposes the int which we can then mutate (in our case increment).
ref.read(counterProvider.notifier).state++;
},
),
);
}
}
不保留状态
现在应用程序成功地增加了计数器,甚至保留了状态,在这种情况下,一个计数器的整数在应用程序会话的生命周期内。但是,如果我们想让用户在打开CounterPage (甚至在之前被关闭后重新打开)后,总是从零开始计数呢?
这就很简单了!我们唯一需要做的就是在文件最上方的counterProvider ,添加autoDispose 修改器。
main.dart
final counterProvider = StateProvider.autoDispose((ref) => 0);
这是怎么做到的?为什么在用户关闭和处置了CounterPage 之后,counterProvider'的状态现在被处置了?
Riverpod知道哪些widget使用个别的提供者。毕竟,我们通过调用watch 方法持续订阅了CounterPage 中的counterProvider 。在我们的例子中,这也恰好是整个应用程序中对counterProvider 的唯一订阅,所以一旦这个订阅不再存在,因为CounterPage widget已经被关闭和处置,Riverpod知道counterProvider的状态也可以被处置。
那是通过什么样的魔法完成的呢?嗯,CounterPage 是来自Riverpod包的ConsumerWidget 的子类,所以所有负责处置提供者状态的必要代码都隐藏在那里。
手动重设状态
当提供者不再被使用时,处置状态并释放资源是一件事,但有时你可能想手动重置状态,例如,用一个按钮。使用ref.invalidate ,这很容易。
main.dart
class CounterPage extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
return Scaffold(
appBar: AppBar(
title: const Text('Counter'),
actions: [
IconButton(
onPressed: () {
ref.invalidate(counterProvider);
},
icon: const Icon(Icons.refresh),
),
],
),
...
虽然你应该更喜欢我们上面的做法,但有时你可能想调用ref.refresh ,这将返回新重置的状态--在我们的例子中,这将是一个整数0。
基于状态执行操作

现在我们已经看到,watch 是在build 方法中使用的,用于获取提供者的状态并用它重建一个部件,而read 是用于在build 方法之外对提供者做一次性的操作--通常是在按钮onPressed 或类似的回调中。
但是,比如说,只要提供者的状态改变为所需的值,我们如何能够进行导航、显示黑板、警报或做任何其他的动作呢?我们不能使用从watch 中得到的状态,直接在build 方法中做这些动作,因为我们会得到臭名昭著的*"setState() or markNeedsBuild() called during build"*错误。相反,我们需要使用listen 方法。
比方说,我们认为数字5大得危险,想显示一个警告用户的对话框,就像这样。
下面的ref.listen 调用是需要进入CounterPage 。
main.dart
class CounterPage extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final int counter = ref.watch(counterProvider);
ref.listen<int>(
counterProvider,
// "next" is referring to the new state.
// The "previous" state is sometimes useful for logic in the callback.
(previous, next) {
if (next >= 5) {
showDialog(
context: context,
builder: (context) {
return AlertDialog(
title: Text('Warning'),
content:
Text('Counter dangerously high. Consider resetting it.'),
actions: [
TextButton(
onPressed: () {
Navigator.of(context).pop();
},
child: Text('OK'),
)
],
);
},
);
}
},
);
...
}
提供商之间的依赖关系
应用程序并不总是简单地拥有一个计数器的提供者,这意味着你通常要同时拥有多个提供者。如果这还不够的话,提供者所提供的对象往往会相互依赖。例如,你可能有一个Cubit 或一个ChangeNotifier ,它依赖于一个Repository ,从那里获得数据。
提供者使处理类之间的依赖关系变得非常简单。事实上,你已经在这篇文章中看到了用于它的语法,如果你是Riverpod的新手,你可能甚至都没有注意到。
比方说,我们想把我们熟悉的计数器应用升级到更大的范围。大家都知道,把所有的状态保持在本地是很蹩脚的,很酷的孩子们把所有的东西都放在无服务器的服务器上™,我们当然不想落伍!我们将创建一个终极的计数器应用程序。我们将创建一个终极的计数器应用,通过WebSocket获取其计数器的整数值。算是吧...
为了使该应用适合教程,我们将伪造WebSocket,并简单地返回本地生成的Stream ,这些整数每隔半秒就会递增。我们还将利用一个抽象类作为接口,以便代码可以很容易地被替换为真正的实现或测试。
main.dart
abstract class WebsocketClient {
Stream<int> getCounterStream();
}
class FakeWebsocketClient implements WebsocketClient {
@override
Stream<int> getCounterStream() async* {
int i = 0;
while (true) {
await Future.delayed(const Duration(milliseconds: 500));
yield i++;
}
}
}
提供FakeWebsocketClient 对象是非常直接的。
main.dart
final websocketClientProvider = Provider<WebsocketClient>(
(ref) {
return FakeWebsocketClient();
},
);
但这并不是我们希望UI能够访问的最终提供者。我们需要调用WebsocketClient 上的getCounterStream 方法来获得我们一直在寻找的计数器Stream 。
当然,我们要创建一个新的counterProvider ,类型为StreamProvider 。但为了调用getCounterStream 方法,我们首先需要有WebsocketClient 对象,该对象由我们在上面的代码片段中创建的提供者提供。
StreamProvider 是另一种类型的提供者,其声明语法与我们已经见过的所有其他提供者相同。很明显,你用它提供的对象必须是 类型。这使得在从widget树上获取数据时可以使用一些很好的语法--不再有笨重的 s!Stream StreamBuilder
要访问WebsocketClient ,我们可以简单地使用ref 参数读取websocketClientProvider ,该参数包含在每个提供者的创建回调中。
main.dart
final counterProvider = StreamProvider<int>((ref) {
final wsClient = ref.watch(websocketClientProvider);
return wsClient.getCounterStream();
});
ref.watch 的调用看起来很熟悉,不是吗?当然了--这和你在widget树中做的事情完全一样,细微的差别是这里的ref 参数不是WidgetRef ,而是StreamProviderRef<int> 。
回调中的ref 参数允许你做所有WidgetRef 允许你做的事情(watch,read,listen,invalidate...)以及更多,例如添加不同的回调。
我们新的、花哨的CounterPage ,将计数器的状态管理外包给一个假的无服务器的服务器,现在看起来如下。请注意,我们在用户界面中使用Stream ,非常方便。事实上,我们甚至没有看到它,因为它被Riverpod转换为一个AsyncValue 。
main.dart
class CounterPage extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
// AsyncValue is a union of 3 cases - data, error and loading
final AsyncValue<int> counter = ref.watch(counterProvider);
return Scaffold(
appBar: AppBar(
title: const Text('Counter'),
),
body: Center(
child: Text(
counter
.when(
data: (int value) => value,
error: (Object e, _) => e,
// While we're waiting for the first counter value to arrive
// we want the text to display zero.
loading: () => 0,
)
.toString(),
style: Theme.of(context).textTheme.displayMedium,
),
),
);
}
}
传递一个参数给提供者

目前计数器客户端的实现总是从0开始,但我们的客户开始给我们一星评价,说他们需要能够修改计数器的起始值。所以我们自然而然地实现了他们的功能要求,如下所示。
main.dart
abstract class WebsocketClient {
Stream<int> getCounterStream([int start]);
}
class FakeWebsocketClient implements WebsocketClient {
@override
Stream<int> getCounterStream([int start = 0]) async* {
int i = start;
while (true) {
await Future.delayed(const Duration(milliseconds: 500));
yield i++;
}
}
}
这将是WebsocketClient 及其 "假 "的实现,但我们还需要以某种方式将起始值传递给counterProvider ,因为那是小部件实际用来获取计数器的Stream 。
直到现在,我们还没有向提供者传递任何东西。我们可以将read 、watch 或listen 给它,但它总是包含了所需的一切,并没有从外部得到任何传递。幸运的是,由于有了family 修改器,向提供者传递参数非常简单。
main.dart
// The "family" modifier's first type argument is the type of the provider
// and the second type argument is the type that's passed in.
final counterProvider = StreamProvider.family<int, int>((ref, start) {
final wsClient = ref.watch(websocketClientProvider);
return wsClient.getCounterStream(start);
});
family 修改器可以与autoDispose 修改器结合使用,如StreamProvider.autoDispose.family<int, int>
family 修改器使我们的counterProvider 成为一个可调用的类,所以要从CounterPage 传递一个开始值,我们可以简单地这样做。
main.dart
class CounterPage extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
// Just hardcoding the start value 5 for simplicity
final AsyncValue<int> counter = ref.watch(counterProvider(5));
...
}
}
结语
就这样,你已经学会了如何通过建立和扩展简单的计数器应用来使用强大的Riverpod包。你现在已经准备好在你自己的复杂和酷的应用程序中使用这些知识了,Riverpod使你在构建和维护方面都很轻松--至少在涉及到在应用程序中获取对象时是这样的 😉
The postRiverpod 2.0 - Complete Guide (Flutter Tutorial)appeared first onReso Coder.