Flutter Riverpod 2.0:终极指南
Riverpod 是一款响应式缓存和数据绑定框架,它作为Provider包的演进而诞生。
根据官方文档的描述:
许多人仍将其视为“状态管理”框架。
但它不仅如此。
事实上,Riverpod 2.0 借鉴了React Query中的许多有价值的概念,并将其引入了Flutter世界。
Riverpod非常灵活,您可以使用它来:
- 在编译时捕捉编程错误,而不是在运行时
- 轻松获取、缓存和更新来自远程源的数据
- 执行响应式缓存并轻松更新您的UI
- 依赖于异步或计算状态
- 创建、使用和组合提供者,减少样板代码
- 在不再使用时释放提供者的状态
- 编写可测试的代码,并将逻辑保持在小部件树之外
Riverpod实现了明确定义的检索和缓存数据的模式,因此您无需重新实现它们。
它还有助于建立良好的应用程序架构(如果正确使用),使您可以专注于以最小摩擦构建功能。
开始使用Riverpod很容易。
但如果要充分发挥其功能,还需要一些学习曲线,因此我创建了这个指南,以涵盖所有基本概念和API。
本指南的组织结构
为了更容易跟随,我将这个指南分为三个主要部分:
- 为什么使用Riverpod,如何安装它以及核心概念
- 八种不同类型的提供者的概述(以及何时使用它们)
- 附加的Riverpod功能(修饰符、提供者覆盖、过滤、测试支持、日志记录等) 这份指南内容详尽且更新,您可以将其作为参考,除了官方文档之外。
我们将使用简单示例来探讨主要的Riverpod API和概念。
在适当的情况下,我还包含了链接到单独文章的链接,这些文章涵盖了这里无法涵盖的更复杂的实际示例。
作为Riverpod 2.0版本发布的一部分,新的riverpod_generator包已经发布。这引入了一个新的
@riverpod
注解API,您可以使用它来自动生成代码中的类和方法的提供程序(使用代码生成)。要了解更多,请阅读:如何使用Flutter Riverpod Generator自动生成提供程序。
准备好了吗?让我们开始吧! 🚀
为什么使用Riverpod?
要理解为什么我们需要Riverpod,让我们看一下Provider包的主要缺点。
从设计上来说,Provider是对InheritedWidget
的改进,因此它依赖于小部件树。
这是一个不幸的设计决策,可能会导致常见的ProviderNotFoundException
:
访问小部件树中的提供者 然而,Riverpod是编译安全的,因为所有提供者都是全局声明的,可以在任何地方访问。
这意味着您可以创建提供者来保存您的应用程序状态和业务逻辑,而不必放在小部件树内。
由于Riverpod是一种响应式框架,它使得只在需要时重新构建提供者和小部件变得更容易。
那么,让我们看看如何安装和使用它。👇
Riverpod 安装
第一步是将最新版本的 flutter_riverpod
添加为依赖项到我们的 pubspec.yaml
文件中:
dependencies:
flutter:
sdk: flutter
flutter_riverpod: ^2.3.6
注意:如果您的应用程序已经使用flutter_hooks,您可以安装hooks_riverpod包。这个包包括一些额外功能,使得将Hooks与Riverpod集成更加容易。在本教程中,为了简化起见,我们将仅关注flutter_riverpod。
如果您想要使用新的Riverpod Generator,您需要安装一些额外的包。有关所有详细信息,请阅读:如何使用Flutter Riverpod Generator自动生成提供程序。
顶部提示:为了更轻松地在您的代码中添加Riverpod提供程序,您可以安装VSCode或Android Studio / IntelliJ的Flutter Riverpod Snippets扩展。
有关更多信息,请阅读Riverpod.dev的入门页面。
ProviderScope
一旦安装了Riverpod,我们可以使用ProviderScope
来包装我们的根部件:
void main() {
// wrap the entire app with a ProviderScope so that widgets
// will be able to read providers
runApp(ProviderScope(
child: MyApp(),
));
}
ProviderScope
是一个小部件,用来存储我们创建的所有提供程序的状态。
在内部,
ProviderScope
创建了一个ProviderContainer
实例。大多数情况下,您不需要关心ProviderContainer
或直接使用它。有关ProviderContainer
和UncontrolledProviderScope
的更多详细信息,请阅读 Flutter Riverpod: 如何在应用启动时注册监听器。
完成了初始设置后,我们可以开始学习有关提供程序的内容。
什么是 Riverpod 提供程序?
Riverpod 文档 定义提供程序为封装状态片段并允许监听该状态的对象。
在 Riverpod 中,提供程序是一切的核心:
- 它们完全取代了设计模式,如单例、服务定位器、依赖注入和 InheritedWidgets。
- 它们允许您存储某些状态并在多个位置轻松访问它。
- 它们允许您通过过滤小部件重建或缓存昂贵的状态计算来优化性能。
- 它们使您的代码更具可测试性,因为每个提供程序可以在测试期间被覆盖以在测试期间表现出不同的行为。
因此,让我们看看如何使用它们。👇
创建和读取提供程序
让我们从创建一个基本的 "Hello world" 提供程序开始:
// provider that returns a string value
final helloWorldProvider = Provider<String>((ref) {
return 'Hello world';
});
这由三部分组成:
- 声明:
final helloWorldProvider
是我们将用于读取提供程序状态的全局变量。 - 提供程序:
Provider
告诉我们我们正在使用什么类型的提供程序(下文将详细介绍),以及它所持有的状态的类型。 - 创建状态的函数。这为我们提供了一个
ref
参数,我们可以使用它来读取其他提供程序、执行一些自定义的清理逻辑等操作。
一旦我们有了一个提供程序,我们如何在小部件内部使用它呢?
class HelloWorldWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Text(
/* how to read the provider value here? */,
);
}
}
所有Flutter小部件都有一个BuildContext
对象,我们可以使用它来访问小部件树内的内容(例如Theme.of(context)
)。
但是,Riverpod提供者存在于小部件树之外,要读取它们,我们需要一个额外的ref
对象。以下是三种不同获取ref
对象的方法👇
1. 使用ConsumerWidget
最简单的方法是使用ConsumerWidget
:
final helloWorldProvider = Provider<String>((_) => 'Hello world');
// 1. widget class now extends [ConsumerWidget]
class HelloWorldWidget extends ConsumerWidget {
@override
// 2. build method has an extra [WidgetRef] argument
Widget build(BuildContext context, WidgetRef ref) {
// 3. use ref.watch() to get the value of the provider
final helloWorld = ref.watch(helloWorldProvider);
return Text(helloWorld);
}
}
通过对 ConsumerWidget
进行子类化,而不是 StatelessWidget
,我们的小部件的 build
方法会获得一个额外的引用对象(类型为 WidgetRef
),我们可以使用它来监视我们的提供程序。
使用 ConsumerWidget
是最常见的选择,大多数情况下,您应该选择它。
2. 使用 Consumer
作为一种替代方案,我们可以使用 Consumer
来包装我们的 Text
小部件:
final helloWorldProvider = Provider<String>((_) => 'Hello world');
class HelloWorldWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
// 1. Add a Consumer
return Consumer(
// 2. specify the builder and obtain a WidgetRef
builder: (_, WidgetRef ref, __) {
// 3. use ref.watch() to get the value of the provider
final helloWorld = ref.watch(helloWorldProvider);
return Text(helloWorld);
},
);
}
}
在这种情况下,“ref”对象是“Consumer”的一个构建器参数之一,我们可以使用它来监视提供者的值。
这种方法是有效的,但相较于以前的解决方案更加冗长。
那么,什么情况下应该使用“Consumer”而不是“ConsumerWidget”呢?
以下是一个示例:
final helloWorldProvider = Provider<String>((_) => 'Hello world');
class HelloWorldWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
// 1. Add a Consumer
body: Consumer(
// 2. specify the builder and obtain a WidgetRef
builder: (_, WidgetRef ref, __) {
// 3. use ref.watch() to get the value of the provider
final helloWorld = ref.watch(helloWorldProvider);
return Text(helloWorld);
},
),
);
}
}
在这种情况下,我们只是用一个 Consumer
小部件包裹 Text
,而没有包裹父级的 Scaffold
:
Scaffold
├─ AppBar
└─ Consumer
└─ Text
因此,只有在提供者的值发生变化时,Text
部分将重新构建(更多细节请参考下文)。
这可能看起来像一个小细节,但如果您有一个包含复杂布局的大型小部件类,您可以使用 Consumer
来仅重新构建依赖于提供者的小部件。正如我在之前的文章中所提到的:
创建小而可重用的小部件有利于组合,使代码更加简洁、性能更高,更易于理解。
如果您遵循这一原则并创建小而可重用的小部件,那么大部分时间您将自然而然地使用 ConsumerWidget
。
3. 使用 ConsumerStatefulWidget 和 ConsumerState
ConsumerWidget
是 StatelessWidget
的良好替代品,为我们提供了一种方便的方式来以最少的代码访问提供者。
但如果我们有一个 StatefulWidget
呢?
这里是相同的示例,你可以看到:
final helloWorldProvider = Provider<String>((_) => 'Hello world');
// 1. extend [ConsumerStatefulWidget]
class HelloWorldWidget extends ConsumerStatefulWidget {
@override
ConsumerState<HelloWorldWidget> createState() => _HelloWorldWidgetState();
}
// 2. extend [ConsumerState]
class _HelloWorldWidgetState extends ConsumerState<HelloWorldWidget> {
@override
void initState() {
super.initState();
// 3. if needed, we can read the provider inside initState
final helloWorld = ref.read(helloWorldProvider);
print(helloWorld); // "Hello world"
}
@override
Widget build(BuildContext context) {
// 4. use ref.watch() to get the value of the provider
final helloWorld = ref.watch(helloWorldProvider);
return Text(helloWorld);
}
}
通过从ConsumerStatefulWidget
和ConsumerState
进行子类化,我们可以在build
方法中调用ref.watch()
,就像我们以前所做的那样。
如果我们需要在任何其他小部件生命周期方法中读取提供程序的值,我们可以使用ref.read()
。
当我们从
ConsumerState
进行子类化时,我们可以在所有小部件生命周期方法中访问ref
对象。这是因为ConsumerState
声明WidgetRef
作为属性,类似于Flutter的State
类声明BuildContext
作为可以在所有小部件生命周期方法中直接访问的属性。
如果您使用hooks_riverpod包,还可以使用
HookConsumerWidget
和StatefulHookConsumerWidget
。官方文档在这些小部件上有更详细的介绍。
什么是WidgetRef?
正如我们所看到的,我们可以使用类型为WidgetRef
的ref
对象来监视提供程序的值。当我们使用Consumer
或ConsumerWidget
时,这将作为参数提供,当我们从ConsumerState
进行子类化时,它将作为属性提供。
Riverpod文档将WidgetRef
定义为一个允许小部件与提供程序进行交互的对象。
请注意,BuildContext
和WidgetRef
之间存在一些相似之处:
BuildContext
允许我们访问小部件树中的祖先小部件(例如Theme.of(context)
和MediaQuery.of(context)
)。WidgetRef
允许我们在应用程序中访问任何提供者。
换句话说,WidgetRef
允许我们在我们的代码库中访问任何提供者(只要我们导入相应的文件)。这是有意设计的,因为所有 Riverpod 提供者都是全局的。
这很重要,因为将应用程序状态和逻辑保留在我们的小部件内会导致关注点分离不足。将其移到提供者内部使我们的代码更具可测试性和可维护性。👍
八种不同类型的提供者
到目前为止,我们已经学习了如何创建一个简单的 Provider
,并使用 ref
对象在小部件内部进行观察。
但是,Riverpod 提供了八种不同类型的提供者,都适用于不同的用例:
Provider
StateProvider
(已过时)StateNotifierProvider
(已过时)FutureProvider
StreamProvider
ChangeNotifierProvider
(已过时)NotifierProvider
(Riverpod 2.0 中的新功能)AsyncNotifierProvider
(Riverpod 2.0 中的新功能)
因此,让我们进行回顾并了解何时使用它们。
如果您使用新的 riverpod_generator 包,您将不再需要手动声明提供者(尽管我仍建议您熟悉所有八种提供者)。要了解更多信息,请阅读:如何使用 Flutter Riverpod Generator 自动生成提供者。
1. Provider
我们已经学习了关于这个:
// provider that returns a string value
final helloWorldProvider = Provider<String>((ref) {
return 'Hello world';
});
Provider
用于访问不会改变的依赖项和对象。
您可以使用它来访问存储库、记录器或其他不包含可变状态的类。
例如,这是一个返回 DateFormat
的提供者:
// declare the provider
final dateFormatterProvider = Provider<DateFormat>((ref) {
return DateFormat.MMMEd();
});
class SomeWidget extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
// retrieve the formatter
final formatter = ref.watch(dateFormatterProvider);
// use it
return Text(formatter.format(DateTime.now()));
}
}
markdown
`Provider` 适用于访问那些不会改变的依赖项,比如我们应用中的仓库。欲了解更多信息,请参考:[Flutter App Architecture: The Repository Pattern](https://codewithandrea.com/articles/flutter-repository-pattern/)。
更多信息请查看以下链接:
- [Provider | Riverpod.dev](https://riverpod.dev/docs/providers/provider)
### [2. StateProvider](#2-stateprovider)
`StateProvider` 适用于存储可以发生变化的简单状态对象,比如计数器值:
final counterStateProvider = StateProvider<int>((ref) {
return 0;
});
如果您在build
方法内观察它,那么当状态发生变化时,小部件将重新构建。
而且,您可以通过调用ref.read()
在按钮回调内更新其状态:
class CounterWidget extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
// 1. watch the provider and rebuild when the value changes
final counter = ref.watch(counterStateProvider);
return ElevatedButton(
// 2. use the value
child: Text('Value: $counter'),
// 3. change the state inside a button callback
onPressed: () => ref.read(counterStateProvider.notifier).state++,
);
}
}
StateProvider
适用于存储简单的状态变量,如枚举、字符串、布尔值和数字。Notifier
也可用于相同的目的,且更加灵活。对于更复杂或异步的状态,请使用AsyncNotifierProvider
、FutureProvider
或StreamProvider
,如下所述。
更多信息和示例请参考:
3. StateNotifierProvider
使用它来监听和暴露一个StateNotifier
。
StateNotifierProvider
和StateNotifier
非常适合管理可能会因事件或用户交互而发生变化的状态。
例如,这里有一个简单的Clock
类:
import 'dart:async';
class Clock extends StateNotifier<DateTime> {
// 1. initialize with current time
Clock() : super(DateTime.now()) {
// 2. create a timer that fires every second
_timer = Timer.periodic(Duration(seconds: 1), (_) {
// 3. update the state with the current time
state = DateTime.now();
});
}
late final Timer _timer;
// 4. cancel the timer when finished
@override
void dispose() {
_timer.cancel();
super.dispose();
}
}
这个类通过在构造函数中调用 super(DateTime.now())
来设置初始状态,并使用定时器每秒更新一次状态。
有了这个,我们可以创建一个新的提供程序:
// Note: StateNotifierProvider has *two* type annotations
final clockProvider = StateNotifierProvider<Clock, DateTime>((ref) {
return Clock();
});
然后,我们可以在一个ConsumerWidget
内观察clockProvider
,以获取当前时间并将其显示在一个Text
小部件内:
import 'package:intl/intl.dart';
class ClockWidget extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
// watch the StateNotifierProvider to return a DateTime (the state)
final currentTime = ref.watch(clockProvider);
// format the time as `hh:mm:ss`
final timeFormatted = DateFormat.Hms().format(currentTime);
return Text(timeFormatted);
}
}
由于我们使用了 ref.watch(clockProvider)
,我们的小部件将在状态发生更改时重新构建(每秒一次),以显示更新后的时间。
注意:
ref.watch(clockProvider)
返回提供程序的状态。要访问底层状态通知器对象,请改为调用ref.read(clockProvider.notifier)
。
有关如何以及何时使用 StateNotifierProvider
的完整示例,请阅读此文章:
更多信息请参考:
截至 Riverpod 2.0,
StateNotifier
被认为是遗留的,可以由新的AsyncNotifier
类替代。有关更多详细信息,请阅读:如何在新的 Flutter Riverpod 生成器中使用 Notifier 和 AsyncNotifier(英文)。
请注意,如果您只需要读取一些异步数据,则使用 StateNotifierProvider
可能有点过于复杂。这就是 FutureProvider
的用途。👇
4. FutureProvider
想要获取返回 Future
的 API 调用的结果吗?
只需创建一个类似这样的 FutureProvider
:
final weatherFutureProvider = FutureProvider.autoDispose<Weather>((ref) {
// get repository from the provider below
final weatherRepository = ref.watch(weatherRepositoryProvider);
// call method that returns a Future<Weather>
return weatherRepository.getWeather(city: 'London');
});
// example weather repository provider
final weatherRepositoryProvider = Provider<WeatherRepository>((ref) {
return WeatherRepository(); // declared elsewhere
});
FutureProvider
通常与autoDispose
修饰符一起使用。请阅读下方的文本以获取更多信息。
接着,您可以在 build
方法中观察它,并使用模式匹配将生成的 AsyncValue
(数据、加载、错误)映射到您的用户界面:
Widget build(BuildContext context, WidgetRef ref) {
// watch the FutureProvider and get an AsyncValue<Weather>
final weatherAsync = ref.watch(weatherFutureProvider);
// use pattern matching to map the state to the UI
return weatherAsync.when(
loading: () => const CircularProgressIndicator(),
error: (err, stack) => Text('Error: $err'),
data: (weather) => Text(weather.toString()),
);
}
注意:当您观察
FutureProvider
或StreamProvider
时,返回类型是AsyncValue
。AsyncValue 是 Riverpod 中用于处理异步数据的实用类。更多详细信息请查看:Flutter Riverpod Tip: 使用 AsyncValue 而不是 FutureBuilder 或 StreamBuilder
FutureProvider
非常强大,您可以使用它来:
- 执行和缓存异步操作(例如网络请求)
- 处理异步操作的错误和加载状态
- 将多个异步值组合成另一个值
- 重新获取和刷新数据(对于下拉刷新操作非常有用)
更多信息请查看:
5. StreamProvider
使用 StreamProvider
来监视来自实时 API 的结果流,并以响应式方式重新构建 UI。
例如,以下是如何为 FirebaseAuth 类的 authStateChanges 方法创建一个 StreamProvider
:
final authStateChangesProvider = StreamProvider.autoDispose<User?>((ref) {
// get FirebaseAuth from the provider below
final firebaseAuth = ref.watch(firebaseAuthProvider);
// call a method that returns a Stream<User?>
return firebaseAuth.authStateChanges();
});
// provider to access the FirebaseAuth instance
final firebaseAuthProvider = Provider<FirebaseAuth>((ref) {
return FirebaseAuth.instance;
});
这是如何在小部件中使用它的方式:
Widget build(BuildContext context, WidgetRef ref) {
// watch the StreamProvider and get an AsyncValue<User?>
final authStateAsync = ref.watch(authStateChangesProvider);
// use pattern matching to map the state to the UI
return authStateAsync.when(
data: (user) => user != null ? HomePage() : SignInPage(),
loading: () => const CircularProgressIndicator(),
error: (err, stack) => Text('Error: $err'),
);
}
StreamProvider
相对于 StreamBuilder widget 具有许多优点,这些优点在以下链接中都有详细列出:
6. ChangeNotifierProvider
ChangeNotifier
类是Flutter SDK的一部分。
我们可以使用它来存储一些状态,并在状态发生变化时通知侦听器。
例如,下面是一个 ChangeNotifier
子类以及相应的 ChangeNotifierProvider
:
class AuthController extends ChangeNotifier {
// mutable state
User? user;
// computed state
bool get isSignedIn => user != null;
Future<void> signOut() {
// update state
user = null;
// and notify any listeners
notifyListeners();
}
}
final authControllerProvider = ChangeNotifierProvider<AuthController>((ref) {
return AuthController();
});
以下是使用小部件的 build
方法的示例:
Widget build(BuildContext context, WidgetRef ref) {
return ElevatedButton(
onPressed: () => ref.read(authControllerProvider).signOut(),
child: const Text('Logout'),
);
}
ChangeNotifier
API让我们违反了两个重要的规则:不可变状态和单向数据流。
因此,不鼓励使用ChangeNotifier
,而应该使用StateNotifier
。
当不正确使用
ChangeNotifier
时,会导致可变状态,并使我们的代码更难维护。StateNotifier
为我们提供了处理不可变状态的简单API。要了解更详细的信息,请阅读:Flutter状态管理:从setState
到Freezed
和StateNotifier
与Provider
更多信息请参考以下链接:
Riverpod 2.0中的新功能:NotifierProvider和AsyncNotifierProvider
Riverpod 2.0引入了新的Notifier和AsyncNotifier类,以及相应的提供者。
我在这篇文章中单独介绍了它们:
何时使用ref.watch
和ref.read
?
在上面的示例中,我们遇到了两种读取提供者的方法:ref.read
和ref.watch
。
在build
方法内获取提供者的值时,我们一直使用ref.watch
。这确保了如果提供者的值发生更改,我们会重建依赖它的小部件。
但也有情况下,我们不应使用ref.watch
。
例如,在按钮的onPressed
回调内,我们应该使用ref.read
:
final counterStateProvider = StateProvider<int>((_) => 0);
class CounterWidget extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
// 1. watch the provider and rebuild when the value changes
final counter = ref.watch(counterStateProvider);
return ElevatedButton(
// 2. use the value
child: Text('Value: $counter'),
// 3. change the state inside a button callback
onPressed: () => ref.read(counterStateProvider.notifier).state++,
);
}
}
根据经验,我们应该:
- 在
build
方法中调用ref.watch(provider)
来观察提供程序的状态,并在其发生变化时重建小部件。 - 在
initState
或其他生命周期方法中调用ref.read(provider)
来仅一次读取提供程序的状态。
然而,在上述代码中,我们调用了ref.read(provider.notifier)
并用它来修改其状态。
.notifier
语法仅适用于StateProvider
和StateNotifierProvider
,其工作方式如下:
- 在
StateProvider
上调用ref.read(provider.notifier)
,以返回底层的StateController
,我们可以使用它来修改状态。 - 在
StateNotifierProvider
上调用ref.read(provider.notifier)
,以返回底层的StateNotifier
,以便我们可以调用其方法。
除了在小部件内部使用
ref.watch
和ref.read
之外,我们还可以在提供程序内部使用它们。有关更多信息,请阅读下面关于与Riverpod结合使用提供程序的内容。
除了ref.read
和ref.watch
之外,我们还有ref.listen
。👇
监听提供程序状态变化
有时,当提供程序的状态发生变化时,我们希望显示警报对话框或SnackBar
。
我们可以通过在build
方法中调用ref.listen()
来实现这一点:
final counterStateProvider = StateProvider<int>((_) => 0);
class CounterWidget extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
// if we use a StateProvider<T>, the type of the previous and current
// values is StateController<T>
ref.listen<StateController<int>>(counterStateProvider.state, (previous, current) {
// note: this callback executes when the provider value changes,
// not when the build method is called
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Value is ${current.state}')),
);
});
// watch the provider and rebuild when the value changes
final counter = ref.watch(counterStateProvider);
return ElevatedButton(
// use the value
child: Text('Value: $counter'),
// change the state inside a button callback
onPressed: () => ref.read(counterStateProvider.notifier).state++,
);
}
}
在这种情况下,回调函数提供了供应商的先前状态和当前状态,我们可以使用它来显示一个 SnackBar
。
ref.listen()
为我们提供了一个回调函数,当供应商的值发生变化时执行,而不是在build
方法被调用时执行。因此,我们可以使用它来运行任何异步代码(例如显示对话框),就像我们在按钮回调中所做的那样。有关在Flutter小部件内运行异步代码的更多信息,请阅读我的有关Flutter中的副作用的文章。
除了
watch
、read
和listen
,Riverpod 2.0 还引入了一些新方法,我们可以使用它们来明确地刷新或使供应商失效。我将在单独的文章中介绍它们。
Riverpod的附加功能
到目前为止,我们已经涵盖了大部分核心概念和六种主要类型的供应商。
接下来,让我们看一些在使用Riverpod的真实项目中经常需要的附加功能。
autoDispose修饰符
如果我们使用FutureProvider
或StreamProvider
,当我们的供应商不再使用时,我们将希望处理任何监听器的释放。
我们可以通过将autoDispose
修饰符添加到我们的供应商来实现:
final authStateChangesProvider = StreamProvider.autoDispose<User?>((ref) {
// get FirebaseAuth from another provider
final firebaseAuth = ref.watch(firebaseAuthProvider);
// call method that returns a Stream<User?>
return firebaseAuth.authStateChanges();
});
这将确保在我们离开观看提供者的页面时,流连接会被立即关闭。
在底层,Riverpod会跟踪附加到任何给定提供者的所有监听器(小部件或其他提供者)(通过
ref.watch
或ref.listen
)。如果我们使用autoDispose
,提供者将在所有监听器被移除时被处置(也就是在小部件被卸载时)。
另一个使用autoDispose
的情况是当我们将FutureProvider
用作在用户打开新屏幕时触发的HTTP请求的包装器时。
如果我们希望在用户在请求完成之前离开屏幕时取消HTTP请求,我们可以使用ref.onDispose()
来执行一些自定义的取消逻辑:
final movieProvider = FutureProvider.autoDispose<TMDBMovieBasic>((ref) async {
// get the repository
final moviesRepo = ref.watch(fetchMoviesRepositoryProvider);
// an object from package:dio that allows cancelling http requests
final cancelToken = CancelToken();
// when the provider is destroyed, cancel the http request
ref.onDispose(() => cancelToken.cancel());
// call method that returns a Future<TMDBMovieBasic>
return moviesRepo.movie(movieId: 550, cancelToken: cancelToken);
});
使用超时缓存
如果需要的话,我们可以调用 ref.keepAlive()
来保持状态,这样如果用户离开并再次进入相同的屏幕,请求不会再次触发:
final movieProvider = FutureProvider.autoDispose<TMDBMovieBasic>((ref) async {
// get the repository
final moviesRepo = ref.watch(fetchMoviesRepositoryProvider);
// an object from package:dio that allows cancelling http requests
final cancelToken = CancelToken();
// when the provider is destroyed, cancel the http request
ref.onDispose(() => cancelToken.cancel());
// if the request is successful, keep the response
ref.keepAlive();
// call method that returns a Future<TMDBMovieBasic>
return moviesRepo.movie(movieId: 550, cancelToken: cancelToken);
});
keepAlive
方法会告诉提供程序保持其状态不受限制,只有在我们刷新或使其无效时才会更新。
我们甚至可以使用 KeepAliveLink
来实现基于超时的缓存策略,在给定的持续时间后销毁提供程序的状态:
// get the [KeepAliveLink]
final link = ref.keepAlive();
// start a 30 second timer
final timer = Timer(const Duration(seconds: 30), () {
// dispose on timeout
link.close();
});
// make sure to cancel the timer when the provider state is disposed
// (prevents undesired test failures)
ref.onDispose(() => timer.cancel());
如果您希望使这段代码更具可重用性,您可以创建一个AutoDisposeRef
扩展(如这里所解释的):
extension AutoDisposeRefCache on AutoDisposeRef {
// keeps the provider alive for [duration] since when it was first created
// (even if all the listeners are removed before then)
void cacheFor(Duration duration) {
final link = keepAlive();
final timer = Timer(duration, () => link.close());
onDispose(() => timer.cancel());
}
}
final myProvider = Provider.autoDispose<int>((ref) {
// use like this:
ref.cacheFor(const Duration(minutes: 5));
return 42;
});
Riverpod帮助我们用简单的代码解决复杂的问题,特别在数据缓存方面表现出色。要充分发挥其优势,请阅读:Riverpod数据缓存和提供者生命周期:完整指南
家族修改器
family
是一个修饰符,我们可以使用它来向提供者传递参数。
它通过添加第二个类型注释和一个额外的参数来工作,在提供者体内我们可以使用这个参数:
final movieProvider = FutureProvider.autoDispose
// additional movieId argument of type int
.family<TMDBMovieBasic, int>((ref, movieId) async {
// get the repository
final moviesRepo = ref.watch(fetchMoviesRepositoryProvider);
// call method that returns a Future<TMDBMovieBasic>, passing the movieId as an argument
return moviesRepo.movie(movieId: movieId, cancelToken: cancelToken);
});
然后,在build
方法中调用ref.watch
时,我们只需将我们想要传递的值传递给提供者:
final movieAsync = ref.watch(movieProvider(550));
当用户从电影的ListView
中选择一个项目,并且我们推送一个带有movieId
作为参数的MovieDetailsScreen
时,我们可以在这种情况下使用这段代码。
class MovieDetailsScreen extends ConsumerWidget {
const MovieDetailsScreen({super.key, required this.movieId});
// pass this as a property
final int movieId;
@override
Widget build(BuildContext context, WidgetRef ref) {
// fetch the movie data for the given movieId
final movieAsync = ref.watch(movieProvider(movieId));
// map to the UI using pattern matching
return movieAsync.when(
data: (movie) => MovieWidget(movie: movie),
loading: (_) => Center(child: CircularProgressIndicator()),
error: (e, __) => Center(child: Text(e.toString())),
);
}
}
向家族传递多个参数
在某些情况下,您可能需要向家族传递多个值。
尽管 Riverpod 不直接支持此功能,但您可以传递任何实现了 hashCode
和相等运算符的自定义对象(比如使用 Freezed 生成的对象或使用 equatable 的对象)。
要了解更多详情,请参考:
为了克服这一限制,您可以使用新的 riverpod_generator 包,并传递任意数量的命名参数或位置参数。详细信息请参阅以下内容:
使用 Riverpod 进行依赖覆盖
有时我们希望创建一个 Provider
来存储当前不可立即获取的值或对象。
例如,我们只能使用基于 Future 的 API 获取 SharedPreferences
实例:
final sharedPreferences = await SharedPreferences.getInstance();
但我们不能在同步提供程序内部返回这个。
final sharedPreferencesProvider = Provider<SharedPreferences>((ref) {
return SharedPreferences.getInstance();
// The return type Future<SharedPreferences> isn't a 'SharedPreferences',
// as required by the closure's context.
});
相反,我们必须通过引发 UnimplementedError
来初始化这个提供程序:
final sharedPreferencesProvider = Provider<SharedPreferences>((ref) {
throw UnimplementedError();
});
当我们需要的对象可用时,我们可以在ProviderScope
小部件内为我们的提供程序设置依赖项覆盖:
// asynchronous initialization can be performed in the main method
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
final sharedPreferences = await SharedPreferences.getInstance();
runApp(ProviderScope(
overrides: [
// override the previous value with the new object
sharedPreferencesProvider.overrideWithValue(sharedPreferences),
],
child: MyApp(),
));
}
在调用runApp()
之前初始化sharedPreferences
的优点是,我们可以在任何地方观察sharedPreferencesProvider
对象,而无需使用基于Future
的API。
此示例在小部件树的根部使用了
ProviderScope
,但如果需要,我们也可以创建嵌套的ProviderScope
小部件。更多信息请参阅下文。
要了解更复杂的异步应用程序初始化示例,请阅读以下文章:
将Providers与Riverpod结合使用
Providers可以依赖于其他Providers。
例如,这里我们定义了一个SettingsRepository
类,它接受一个显式的SharedPreferences
参数:
class SettingsRepository {
const SettingsRepository(this.sharedPreferences);
final SharedPreferences sharedPreferences;
// synchronous read
bool onboardingComplete() {
return sharedPreferences.getBool('onboardingComplete') ?? false;
}
// asynchronous write
Future<void> setOnboardingComplete(bool complete) {
return sharedPreferences.setBool('onboardingComplete', complete);
}
}
然后,我们创建一个名为 settingsRepositoryProvider
的提供者,它依赖于我们上面创建的 sharedPreferencesProvider
。
final settingsRepositoryProvider = Provider<SettingsRepository>((ref) {
// watch another provider to obtain a dependency
final sharedPreferences = ref.watch(sharedPreferencesProvider);
// pass it as an argument to the object we need to return
return SettingsRepository(sharedPreferences);
});
使用 ref.watch()
确保在依赖的提供程序更改时更新提供程序。因此,任何依赖的小部件和提供程序也将重新构建。
将 Ref 作为参数传递
作为一种替代方法,我们可以在创建 SettingsRepository
时将 Ref
作为参数传递:
class SettingsRepository {
const SettingsRepository(this.ref);
final Ref ref;
// synchronous read
bool onboardingComplete() {
final sharedPreferences = ref.read(sharedPreferencesProvider);
return sharedPreferences.getBool('onboardingComplete') ?? false;
}
// asynchronous write
Future<void> setOnboardingComplete(bool complete) {
final sharedPreferences = ref.read(sharedPreferencesProvider);
return sharedPreferences.setBool('onboardingComplete', complete);
}
}
这样一来,sharedPreferencesProvider
就成为了一个隐式依赖项,我们可以通过调用 ref.read()
来访问它。
这样我们的 settingsRepositoryProvider
声明就变得简单了:
final settingsRepositoryProvider = Provider<SettingsRepository>((ref) {
return SettingsRepository(ref);
});
使用Riverpod,我们可以声明包含复杂逻辑或依赖于其他提供者的提供者,而这些提供者都在小部件树之外。这是与Provider包相比的一个巨大优势,使得编写仅包含UI代码的小部件变得更加容易。
要了解如何在复杂应用程序中结合提供者并处理多个依赖关系的实际示例,请阅读以下内容:
作用域提供者
使用Riverpod,我们可以对提供者进行作用域设置,以便它们在应用程序的特定部分表现出不同行为。
一个示例是,当我们有一个ListView
显示产品列表,每个项目都需要知道正确的产品ID或索引时:
class ProductList extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ListView.builder(
itemBuilder: (_, index) => ProductItem(index: index),
);
}
}
在上面的代码中,我们将构建器的索引作为构造函数参数传递给 ProductItem
小部件:
class ProductItem extends StatelessWidget {
const ProductItem({super.key, required this.index});
final int index;
@override
Widget build(BuildContext context) {
// do something with the index
}
}
这种方法有效,但如果ListView
重新构建,它的所有子项也将重新构建。
作为替代方法,我们可以在嵌套的ProviderScope
内部覆盖提供者的值:
// 1. Declare a Provider
final currentProductIndex = Provider<int>((_) => throw UnimplementedError());
class ProductList extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ListView.builder(itemBuilder: (context, index) {
// 2. Add a parent ProviderScope
return ProviderScope(
overrides: [
// 3. Add a dependency override on the index
currentProductIndex.overrideWithValue(index),
],
// 4. return a **const** ProductItem with no constructor arguments
child: const ProductItem(),
);
});
}
}
class ProductItem extends ConsumerWidget {
const ProductItem({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
// 5. Access the index via WidgetRef
final index = ref.watch(currentProductIndex);
// do something with the index
}
}
在这种情况下:
- 我们创建一个默认抛出
UnimplementedError
的Provider
。 - 通过将父
ProviderScope
添加到ProductItem
小部件来覆盖其值。 - 我们在
ProductItem
的build
方法中监视索引。
这对性能更有益,因为我们可以将ProductItem
作为const
小部件创建在ListView.builder
中。因此,即使ListView
重新构建,除非其索引发生更改,否则我们的ProductItem
将不会重新构建。
使用 "select" 过滤小部件的重新构建
有时您有一个具有多个属性的模型类,并且您只希望在特定属性更改时重新构建小部件。
例如,考虑这个Connection
类,以及一个读取它的提供程序和小部件类:
class Connection {
Connection({this.bytesSent = 0, this.bytesReceived = 0});
final int bytesSent;
final int bytesReceived;
}
// Using [StateProvider] for simplicity.
// This would be a [FutureProvider] or [StreamProvider] in real-world usage.
final connectionProvider = StateProvider<Connection>((ref) {
return Connection();
});
class BytesReceivedText extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
// rebuild when bytesSent OR bytesReceived changes
final counter = ref.watch(connectionProvider).state;
return Text('${counter.bytesReceived}');
}
}
如果我们调用ref.watch(connectionProvider)
,我们的小部件将(不正确地)在bytesSent
值更改时重建。
相反,我们可以使用select()
仅监听特定的属性:
class BytesReceivedText extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
// only rebuild when bytesReceived changes
final bytesReceived = ref.watch(connectionProvider.select(
(connection) => connection.state.bytesReceived
));
return Text('$bytesReceived');
}
}
每当Connection
发生变化时,Riverpod会比较我们返回的值(connection.state.bytesReceived
),只有在它与先前的值不同的情况下才会重新构建小部件。
select
方法可用于所有Riverpod提供程序,可以在我们调用ref.watch()
或ref.listen()
时使用。欲了解更多信息,请阅读Riverpod文档中的"使用“select”来筛选重建"。
使用Riverpod进行测试
正如我们所见,Riverpod提供程序是全局的,但它们的状态不是。
提供程序的状态存储在ProviderContainer
内,这是由ProviderScope
隐式创建的对象。
这意味着不同的小部件测试永远不会共享任何状态,因此不需要setUp
和tearDown
方法。
例如,这是一个使用StateProvider
来存储计数器值的简单计数器应用程序:
final counterProvider = StateProvider((ref) => 0);
void main() {
runApp(ProviderScope(child: MyApp()));
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Consumer(builder: (_, ref, __) {
final counter = ref.watch(counterProvider);
return ElevatedButton(
onPressed: () => ref.read(counterProvider.notifier).state++,
child: Text('${counter.state}'),
);
}),
);
}
}
以上的代码使用了ElevatedButton
来展示计数值,并通过onPressed
回调来增加它。
在编写小部件测试时,我们只需要这些:
await tester.pumpWidget(ProviderScope(child: MyApp()));
使用这种设置,多个测试之间不共享任何状态,因为每个测试都具有不同的 ProviderScope
:
void main() {
testWidgets('incrementing the state updates the UI', (tester) async {
await tester.pumpWidget(ProviderScope(child: MyApp()));
// The default value is `0`, as declared in our provider
expect(find.text('0'), findsOneWidget);
expect(find.text('1'), findsNothing);
// Increment the state and re-render
await tester.tap(find.byType(ElevatedButton));
await tester.pump();
// The state have properly incremented
expect(find.text('1'), findsOneWidget);
expect(find.text('0'), findsNothing);
});
testWidgets('the counter state is not shared between tests', (tester) async {
await tester.pumpWidget(ProviderScope(child: MyApp()));
// The state is `0` once again, with no tearDown/setUp needed
expect(find.text('0'), findsOneWidget);
expect(find.text('1'), findsNothing);
});
}
如何在测试中模拟和覆盖依赖关系
许多应用程序需要调用REST API或与外部服务通信。
例如,这里有一个MoviesRepository
,我们可以使用它来获取一组喜爱的电影列表:
class MoviesRepository {
Future<List<Movie>> favouriteMovies() async {
// get data from the network or local database
}
}
我们可以创建一个 moviesProvider
来获取我们需要的数据:
final moviesRepositoryProvider = Provider((ref) => MoviesRepository());
final moviesProvider = FutureProvider<List<Movie>>((ref) {
// access the provider above
final repository = ref.watch(moviesRepositoryProvider);
// use it to return a Future
return repository.favouriteMovies();
});
在编写小部件测试时,我们希望将我们的 MoviesRepository
替换为一个模拟对象,该模拟对象返回一个固定的响应,而不是进行网络调用。
正如我们所见,我们可以使用依赖项覆盖来改变提供程序的行为,通过将其替换为不同的实现。
因此,我们可以实现一个 MockMoviesRepository
:
class MockMoviesRepository implements MoviesRepository {
@override
Future<List<Movie>> favouriteMovies() {
return Future.value([
Movie(id: 1, title: 'Rick & Morty', posterUrl: 'https://nnbd.me/1.png'),
Movie(id: 2, title: 'Seinfeld', posterUrl: 'https://nnbd.me/2.png'),
]);
}
}
在我们的小部件测试中,我们可以重写存储库提供程序:
void main() {
testWidgets('Override moviesRepositoryProvider', (tester) async {
await tester.pumpWidget(
ProviderScope(
overrides: [
moviesRepositoryProvider
.overrideWithValue(MockMoviesRepository())
],
child: MoviesApp(),
),
);
});
}
因此,MoviesApp
小部件将在测试运行时从 MockMoviesRepository
中加载数据。
如果在测试中使用 mocktail,这个设置也适用。您可以存根您的模拟方法以返回值或引发异常,并验证它们是否被调用。
有关如何使用Riverpod进行测试的更多信息
上面的示例相当基本。如果您想要认真进行测试,您将需要一些更多的资源。👇
官方的 Riverpod 测试指南 包括一些更详细的信息(包括如何使用 ProviderContainer
编写单元测试),但不太全面。
如果您有一些带有依赖关系的自定义通知器并希望对它们进行单元测试,请阅读以下内容:
使用ProviderObserver记录日志
在许多应用程序中,监视状态变化是有益的。
Riverpod 包括一个 ProviderObserver
类,我们可以子类化以实现一个 Logger
:
class Logger extends ProviderObserver {
@override
void didUpdateProvider(
ProviderBase provider,
Object? previousValue,
Object? newValue,
ProviderContainer container,
) {
print('[${provider.name ?? provider.runtimeType}] value: $newValue');
}
}
这使我们能够访问先前和新的数值。
通过将Logger
添加到ProviderScope
内的观察者列表,我们可以为整个应用启用日志记录:
void main() {
runApp(
ProviderScope(observers: [Logger()], child: MyApp()),
);
}
为了提升日志记录的输出,我们可以给我们的提供者(providers)添加一个名称:
final counterStateProvider = StateProvider<int>((ref) {
return 0;
}, name: 'counter');
如有需要,我们可以根据观察到的数值来调整记录器的输出:
class Logger extends ProviderObserver {
@override
void didUpdateProvider(
ProviderBase provider,
Object? previousValue,
Object? newValue,
ProviderContainer container,
) {
if (newValue is StateController<int>) {
print(
'[${provider.name ?? provider.runtimeType}] value: ${newValue.state}');
}
}
}
ProviderObserver
是多功能的,我们可以配置我们的日志记录器,只记录与特定类型或提供者名称匹配的值。或者,我们可以使用嵌套的 ProviderScope
,只记录特定小部件子树中的值。
通过这种方式,我们可以评估状态变化和监视小部件重建,而不必在各个地方放置 print
语句。
ProviderObserver
类似于BlocObserver
小部件,它来自flutter_bloc
包。
Riverpod应用架构的快速说明
在构建复杂的应用程序时,选择一个能够支持代码库随着增长而增加的良好应用程序架构至关重要。
正如我之前所说:
- 将应用程序状态和逻辑保持在小部件内会导致关注点分离不佳。
- 将其移至提供者内会使我们的代码更具可测试性和可维护性。
事实证明,Riverpod非常适合解决架构问题,而不会妨碍开发过程。
那么,一个强大的Riverpod应用架构是什么样的呢?
经过大量研究,我制定了一个由四个层次(数据、领域、应用、展示)组成的架构:
使用数据、领域、应用和演示层的Flutter应用架构。箭头显示了层之间的依赖关系。我在自己的应用中广泛使用这种架构,并撰写了一整套相关文章。
要了解更多信息,你可以从这里开始: 👇
结论
Riverpod借鉴了Provider的最佳特点,并添加了许多优点,使在我们的应用中更容易且更安全地管理状态。
除了本指南中涵盖的所有内容外,我建议查看官方文档以及官方示例应用: