flutter状态管理:Riverpod 高级
2022-11-08 20:24:18
Riverpod 是一个 响应式缓存和数据绑定框架 的演变而诞生的。 ,它是作为Provider 包
根据 官方文档 :
许多人仍然将其视为“状态管理”框架。
但远不止于此。
事实上,Riverpod 2.0 从 React Query 中借鉴了许多有价值的概念,并将它们带到了 Flutter 世界。
Riverpod 用途广泛,您可以使用它来:
Riverpod 为检索和缓存数据实现了定义明确的模式,因此您不必重新实现它们。
它还可以帮助您建立 良好的应用程序架构 (如果您正确使用它),因此您可以专注于以最小的摩擦构建功能。
Riverpod 入门很容易。
但是,如果您想充分利用它,则有一点学习曲线,我创建了本指南以涵盖所有 基本概念和 API 。
为了更容易理解,我将本指南分为三个主要部分:
本指南内容 广泛 且 最新 ,除了 官方文档 外,您还可以将其用作参考。
我们将 使用简单的示例来 探索主要的 Riverpod API 和概念。
在适当的情况下,我已经包含了指向 不同 文章的链接,这些文章涵盖了不适合此处的更复杂的真实示例。
一个新的 riverpod_generator 作为 Riverpod 2.0 版本的一部分,已发布了 包。 这引入了一个新的
@riverpod
注释 API,您可以使用它自动为代码中的类和方法生成提供程序(使用代码生成)。 要了解它,请阅读: 如何使用 Flutter Riverpod Generator 自动生成您的提供程序 。
准备好? 让我们开始吧! ???
要理解为什么我们甚至需要 Riverpod,让我们看看 Provider 包的主要缺点。
按照设计,Provider 是对 InheritedWidget
,因此,它 取决于小部件树 。
这是一个不幸的设计决策,可能导致常见的 ProviderNotFoundException
:
示例:在小部件树中访问 Providers
另一方面,Riverpod 是 编译安全的 ,因为所有提供程序都是 全局 声明的,并且可以在任何地方访问。
这意味着您可以创建提供程序来 在小部件树之外 保存您的应用程序状态和业务逻辑。
而且由于 Riverpod 是一个 反应式 框架,它使得仅在需要时重建您的提供程序和小部件变得更加容易。
那么让我们看看如何安装和使用它。 ????
第一步是添加最新版本的 flutter_riverpod
作为对我们的依赖 pubspec.yaml
文件:
dependencies:
flutter:
sdk: flutter
flutter_riverpod: ^2.0.2
注意 :如果您的应用程序已经使用 了 flutter_hooks ,您可以安装 hooks_riverpod 包。 这包括一些额外的功能,可以更轻松地将 Hooks 与 Riverpod 集成。 在本教程中,为了简单起见,我们将重点关注 flutter_riverpod 。
如果你想使用新的 Riverpod Generator ,你需要安装一些额外的包。 有关所有详细信息,请阅读: 如何使用 Flutter Riverpod Generator 自动生成您的提供程序 。
重要提示 :要更轻松地在代码中添加 Riverpod 提供程序,请安装适用于 VSCode 或 Android Studio / IntelliJ 的 Flutter Riverpod Snippets 扩展。
有关更多信息,请阅读 Riverpod.dev 上的入门页面 。
一旦安装了 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
或者直接使用。 要了解更多信息,请阅读 Flutter Riverpod:如何在 App 启动期间注册监听器 。
完成初始设置后,我们可以开始了解 Providers。
Riverpod 文档 将 Provider 定义为 封装状态并允许监听该状态的对象 。
对于 Riverpod,提供者是一切的核心:
那么让我们看看如何使用它们。 ????
让我们从创建一个基本的“Hello world”提供者开始:
// provider that returns a string value
final helloWorldProvider = Provider<String>((ref) {
return 'Hello world';
});
This is made of three things:
final helloWorldProvider
是我们将用来读取提供者状态的全局变量Provider<String>
告诉我们 哪种类型 我们正在使用 类型。 的提供者(下面会详细介绍),以及它所持有的状态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 对象。 以下是三种不同的获取方式。 ????
最简单的方法是使用 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
方法获取一个额外的 ref 对象(类型 WidgetRef
) 我们可以用来 监视 我们的提供者。
使用 ConsumerWidget
是最常见的选项,也是您大部分时间应该选择的选项。
作为替代方案,我们可以包装我们的 Text
小部件 Consumer
:
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);
},
),
);
}
}
,我们 只包装 在这种情况下 Text
与 Consumer
小部件,但 不是 父级 Scaffold
:
Scaffold
├─ AppBar
└─ Consumer
└─ Text
结果,只有 Text
如果提供者值发生变化,将重建(更多内容见下文)。
这可能看起来像一个小细节,但如果你有一个具有复杂布局的大部件类,你可以使用 Consumer
重建 仅 于提供程序的小部件 依赖 。 虽然正如我在 上一篇文章 中所说:
创建小型且可重用的小部件有利于组合,从而使代码更简洁、性能更高且更易于推理。
如果你遵循这个原则并创建小的、可重复使用的小部件,那么你自然会使用 ConsumerWidget
大多数时候。
ConsumerWidget
是一个很好的替代品 StatelessWidget
并为我们提供了一种使用最少代码访问提供者的便捷方式。
但是如果我们有一个 StatefulWidget
反而?
这是相同的 hello world 示例:
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
,我们可以调用 ref.watch()
在里面 build
方法就像我们之前做的那样。
如果我们需要 在任何其他小部件生命周期方法中读取 提供者值,我们可以使用 ref.read()
.
当我们从
ConsumerState
,我们可以访问ref
所有小部件生命周期方法中的对象。 这是因为ConsumerState
宣布WidgetRef
作为一个属性 ,很像 FlutterState
类声明BuildContext
直接访问的属性。 作为可以在所有小部件生命周期方法中
如果你使用 hooks_riverpod 包,你也可以使用
HookConsumerWidget
和StatefulHookConsumerWidget
. 官方文档 更详细地介绍了这些小部件 。
正如我们所见,我们可以 通过 使用 ref
类型的对象 WidgetRef
. 这可以 作为参数使用 当我们使用时, 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 自动生成您的提供程序 。
我们已经了解了这一点:
// 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()));
}
}
Provider
非常适合访问不会更改的依赖项,例如我们应用程序中的存储库。 有关更多信息,请阅读: Flutter 应用架构:存储库模式 。
更多信息在这里:
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
非常适合存储 简单 的状态变量,例如 enums 、 strings 、 booleans 和 numbers 。Notifier
也可以用于相同的目的,并且更灵活。 对于更复杂或异步的状态,使用AsyncNotifierProvider
,FutureProvider
, 或者StreamProvider
如下所述。
更多信息和示例在这里:
使用它来聆听和展示 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();
});
然后,我们 观看 可以 clockProvider
在一个里面 ConsumerWidget
获取当前时间并将其显示在 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
班级。 有关更多详细信息,请阅读: 如何将 Notifier 和 AsyncNotifier 与新的 Flutter Riverpod Generator 一起使用 。
请注意,使用 StateNotifierProvider
如果您只需要 读取一些异步数据 ,那就大材小用了。 就是这样 FutureProvider
是为了。 ????
想要从返回的 API 调用中获取结果 Future
?
然后只需创建一个 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
(数据、加载、错误)到您的 UI:
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<T>
或者StreamProvider<T>
,返回类型是AsyncValue<T>
. AsyncValue 是用于处理 Riverpod 中的异步数据的实用程序类。 有关更多详细信息,请阅读: Flutter Riverpod 提示:使用 AsyncValue 而不是 FutureBuilder 或 StreamBuilder
FutureProvider
非常强大,你可以用它来:
更多信息在这里:
利用 StreamProvider
看一个 Stream
来自实时 API 的结果并 反应性 地重建 UI。
For example, here is how to create a StreamProvider
for the authStateChanges method of the FirebaseAuth class:
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;
});
And here's how to use it inside a widget:
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 ,这些都在此处列出:
这 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 状态管理:使用 Provider 从 setState 到 Freezed 和 StateNotifier
更多信息在这里:
Riverpod 2.0 引入了新的 Notifier 和 AsyncNotifier 类,以及它们相应的提供程序。
我在本文中分别介绍了它们:
在上面的例子中,我们遇到了两种读取提供者的方式: ref.read
and 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++,
);
}
}
根据经验,我们应该:
ref.watch(provider)
提供者 观察 的状态 build
方法并 重建一个小部件 在它更改时ref.read(provider)
只 读取 提供者的状态 一次 (这在 initState
或其他生命周期方法)但是,在上面的代码中,我们调用了 ref.read(provider.notifier)
并用它来 修改 它的状态。
这 .notifier
语法可用于 StateProvider
和 StateNotifierProvider
只有 并且按以下方式工作:
ref.read(provider.notifier)
在一个 StateProvider<T>
返回底层证券 StateController<T>
我们可以用来修改状态ref.read(provider.notifier)
在一个 StateNotifierProvider<T>
返回底层证券 StateNotifier<T>
所以我们可以在上面调用方法除了使用
ref.watch
和ref.read
在我们的小部件中,我们也可以 在我们的提供者 中使用它们。 阅读下面关于 将 Providers 与 Riverpod 结合以 获取更多信息。
旁边 ref.read
和 ref.watch
, 我们还有 ref.listen
. ????
有时我们想显示一个警告对话框或 SnackBar
当提供者状态改变时。
我们可以通过调用来做到这一点 ref.listen()
在 - 的里面 build
方法:
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 的实际项目中经常需要的一些附加功能。
如果我们正在与 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;
});
阅读 autoDispose 文档页面 了解更多详细信息。
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);
});
然后,我们可以在调用时将我们想要的值传递给提供者 ref.watch
在里面 build
方法:
final movieAsync = ref.watch(movieProvider(550));
当用户从 ListView
的电影,我们推 MovieDetailsScreen
这需要 movieId
作为论据:
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 包并传递 任意数量的命名或位置参数 。 阅读本文以了解所有详细信息:
有时我们想创建一个 Provider
的值或对象 存储不立即可用 。
例如,我们只能得到一个 SharedPreferences
实例: 具有基于 Future 的 API 的
final sharedPreferences = await SharedPreferences.getInstance();
但是我们不能在 同步 Provider 中返回这个:
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(),
));
}
初始化的好处 sharedPreferences
打电话之前 runApp()
是我们可以观看 sharedPreferencesProvider
任何地方的对象,而无需使用任何基于 Future 的 API。
这个例子使用了
ProviderScope
在小部件树的根部,但我们也可以创建 嵌套ProviderScope
如果需要小部件。 更多关于这 下面 。
有关异步应用程序初始化的更复杂示例,请阅读本文:
提供者可以 依赖于 其他提供者。
例如,这里我们定义一个 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
:
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
}
}
在这种情况下:
Provider
抛出 UnimplementedError
默认。ProviderScope
到 ProductItem
小部件。ProductItem
的 build
方法。这对性能更好,因为我们可以创建 ProductItem
作为一个 const
中的小部件 ListView.builder
. 所以即使 ListView
重建,我们的 ProductItem
不会 重建。 除非其索引已更改,否则
有时您有一个具有 多个属性 的模型类,并且您希望 仅在特定属性更改时 重建一个小部件。
例如,考虑这个 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 文档中的使用“选择”过滤重建 。
正如我们所见,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 进行测试 的页面,但它不是很广泛。 要了解更多信息,您可以参加我的 Flutter Foundations 课程 ,其中包含 超过三个小时的关于单独测试的内容 。
监控状态变化在许多应用程序中都是有益的。
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()),
);
}
为了改善记录器的输出,我们可以为我们的提供者添加一个名称:
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 应用程序架构是什么样的呢?
经过大量研究,我正式确定了一个由四层( 数据 、 域 、 应用程序 、 表示 )组成的架构:
使用数据、域、应用程序和表示层的 Flutter 应用架构。 箭头显示层之间的依赖关系
我在自己的应用程序中广泛使用了这种架构,并写了一系列关于它的文章。
要了解更多信息,您可以从这里开始:????
Riverpod 借鉴了 Provider 的最佳功能,并增加了许多好处,使 变得更容易 、 更安全。 在我们的应用程序中管理状态
除了我们在本指南中介绍的所有内容之外,我建议您查看 官方文档 以及官方示例应用程序:
如果你想构建大中型应用程序,我想你会喜欢我关于 Riverpod 应用程序架构的整个系列:
如果您想更深入,请查看我最新的 Flutter 课程,您将在其中学习如何使用 Riverpod 2.0 构建完整的电子商务应用程序。 ????
我开设了一门全新的课程,深入介绍了 Riverpod 的状态管理,以及其他重要主题,如应用程序架构、路由、测试等等: