Riverpod 是用于 Flutter 的强大的响应式缓存和数据绑定框架。
它提供了许多不同类型的Provider,我们可以使用这些Provider来:
- 在我们的代码中访问依赖项(使用
Provider
) - 缓存来自网络的异步数据(使用
FutureProvider
和StreamProvider
) - 管理本地应用程序状态(使用
StateProvider
、StateNotifierProvider
和ChangeNotifierProvider
)
但手动编写大量Provider可能容易出错,而选择要使用的Provider也不总是容易的。 🥵
如果我告诉你,你不再需要这样做了呢?
如果你只需在代码中加上 @riverpod
注释,然后让 build_runner
动态生成所有Provider,会怎么样?
事实证明,这就是新的 riverpod_generator 包的用途(它可以让我们的生活变得更加轻松)。
我们将涵盖的内容
有很多内容要涵盖,所以我将把它分为两篇文章。
在第一篇文章中,我们将学习如何使用新的 @riverpod
语法从函数生成Provider。
作为其中的一部分,我将向你展示如何:
- 使用
@riverpod
语法声明Provider - 将
FutureProvider
转换为新的语法 - 传递参数给Provider,克服旧的
family
修饰符的限制
在下一篇文章中,我们将学习如何从类生成Provider,并了解如何完全替代 StateNotifierProvider
和 StateProvider
,使用新的 Notifier 和 AsyncNotifier 类。
你可以在这里找到第二篇文章:如何使用 Flutter Riverpod Generator 重写新的 Notifier 和 AsyncNotifier。
更新:还有第三篇文章可用,展示如何使用 Riverpod Lint & Riverpod Snippets 来提高你的工作流程。 我们还将涵盖一些权衡,以便您可以决定是否应在自己的应用程序中使用新的语法。
准备好了吗?开始吧!👇
本文假定您已经熟悉了 Riverpod。如果您对 Riverpod 还不熟悉,请阅读:Flutter Riverpod 2.0:终极指南
我们是否应该手动编写 providers?
这是一个很好的问题。
一方面,您可能有像这样的简单 providers:
// a provider for the Dio client to be used by the rest of the app
final dioProvider = Provider<Dio>((ref) {
return Dio();
});
另一方面,一些Provider可能有依赖项,并可以使用 family
修饰符来接受参数:
// a provider to fetch the movie data for a given movie id
final movieProvider = FutureProvider.autoDispose
.family<TMDBMovie, int>((ref, movieId) {
return ref
.watch(moviesRepositoryProvider)
.movie(movieId: movieId);
});
如果你有一个带有family
修饰符的StateNotifierProvider
,语法会变得更加复杂,因为你需要指定三个类型注释:
final emailPasswordSignInControllerProvider = StateNotifierProvider.autoDispose
.family<
EmailPasswordSignInController, // the StateNotifier subclass
EmailPasswordSignInState, // the type of the underlying state class
EmailPasswordSignInFormType // the argument type passed to the family
>((ref, formType) {
return EmailPasswordSignInController(
authRepository: ref.watch(authRepositoryProvider),
formType: formType,
);
});
静态分析工具可以帮助我们确定需要多少种类型,但上面的代码不够易读。
有没有更简单的方法? 🧐
Riverpod 注解
再次考虑一下这个 FutureProvider
:
// a provider to fetch the movie data for a given movie id
final movieProvider = FutureProvider.autoDispose
.family<TMDBMovie, int>((ref, movieId) {
return ref
.watch(moviesRepositoryProvider)
.movie(movieId: movieId);
});
这个Provider的要点在于,我们可以通过调用以下方法来获取电影:
// declared inside a MoviesRepository class
Future<TMDBMovie> movie({required int movieId});
但如果我们不创建上面的Provider,而是像这样写呢?
@riverpod
Future<TMDBMovie> movie(
MovieRef ref, {
required int movieId,
}) {
return ref
.watch(moviesRepositoryProvider)
.movie(movieId: movieId);
}
这符合我们定义函数的方式:
- 首先是返回类型
- 然后是函数名
- 然后是参数列表
- 最后是函数体
一个函数定义。1:返回类型,2:函数名,3:参数列表,4:函数体。这比声明一个FutureProvider.family
,将返回类型放在参数类型旁边更加直观。
一种新的Riverpod语法?
当Remi在Flutter Vikings期间介绍了新的Riverpod语法时,我有点困惑。
但在我的一些项目中尝试过后,我开始喜欢它的简单性。
新的API更加精简,并带来了两个重要的可用性改进:
- 你不再需要担心要使用哪个Provider
- 你可以随意传递命名参数或位置参数给Provider(就像你对待任何函数一样)
这对Riverpod本身来说是一大进步,学习新的API将让你的生活更加轻松。
所以让我来向你展示它是如何工作的。
与其从零开始,我们将从一个现有应用程序中获取一些Provider,并将它们转换为新的语法。最后,我会分享一个包含完整源代码的示例存储库。
使用riverpod_generator入门
正如在riverpod_generator页面上所解释的,我们需要将这些包添加到pubspec.yaml
中:
dependencies:
# or flutter_riverpod/hooks_riverpod as per https://riverpod.dev/docs/getting_started
riverpod:
# the annotation package containing @riverpod
riverpod_annotation:
dev_dependencies:
# a tool for running code generators
build_runner:
# the code generator
riverpod_generator:
# riverpod_lint makes it easier to work with Riverpod
riverpod_lint:
# import custom_lint too as riverpod_lint depends on it
custom_lint:
注意我还添加了riverpod_lint
和custom_lint
这两个包。要了解更多关于riverpod_lint
的功能,请阅读:如何通过 Riverpod Lint 和 Riverpod Snippets 提升开发工作流。
在 "watch" 模式下启动代码生成器
接下来,我们需要在终端上运行以下命令:
flutter pub run build_runner watch -d
-d
标志是可选的,等同于--delete-conflicting-outputs
。如其名称所示,它确保我们覆盖以前构建中的任何冲突输出(通常是我们想要的)。
这将监视项目中的所有Dart文件,并在我们进行更改时自动更新生成的代码。
现在让我们开始创建一些Provider。👇
创建第一个带注解的Provider
作为第一步,让我们考虑这个简单的Provider:
// dio_provider.dart
import 'package:dio/dio.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
// a provider for the Dio client to be used by the rest of the app
final dioProvider = Provider<Dio>((ref) {
return Dio();
});
以下是我们应该如何修改这个文件以使用新的语法:
import 'package:dio/dio.dart';
// 1. import the riverpod_annotation package
import 'package:riverpod_annotation/riverpod_annotation.dart';
// 2. add a part file
part 'dio_provider.g.dart';
// 3. use the @riverpod annotation
@riverpod
// 4. update the declaration
Dio dio(DioRef ref) {
return Dio();
}
一旦我们保存这个文件,build_runner
将开始工作,并在相同的文件夹中生成 dio_provider.g.dart
:
一个 dio_provider.g.dart
文件会与我们的 Dart 文件一起生成。新的 .g.dart
文件会与现有的文件一起生成,所以你无需改变文件夹结构。
如果我们打开生成的文件,我们会看到以下内容:
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'dio_provider.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
String _$dioHash() => r'26723d20a4ee2d05c3b01acad1196ed96cece567';
/// See also [dio].
@ProviderFor(dio)
final dioProvider = AutoDisposeProvider<Dio>.internal(
dio,
name: r'dioProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product') ? null : _$dioHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef DioRef = AutoDisposeProviderRef<Dio>;
// ignore_for_file: unnecessary_raw_strings, subtype_of_sealed_class, invalid_use_of_internal_member, do_not_use_environment, prefer_const_constructors, public_member_api_docs, avoid_private_typedef_functions
最重要的是,这个文件包含了我们需要的 dioProvider
(其中还包含用于调试的额外属性),并且将 DioRef
类型定义为 AutoDisposeProviderRef
。
这意味着我们只需要编写以下代码:
part 'dio_provider.g.dart';
@riverpod
Dio dio(DioRef ref) {
return Dio();
}
riverpod_generator 会生成相应的 dioProvider
和我们作为参数传递给函数的 DioRef
类型。
所有由 riverpod_generator 创建的Provider默认使用
autoDispose
修饰符。如果您不熟悉此内容,请阅读有关 autoDispose 修饰符 的信息。
为 Repository 类创建Provider
现在我们有了一个 dioProvider
,让我们尝试在某个地方使用它。
例如,假设我们有一个 MoviesRepository
类,该类定义了一些用于获取电影数据的方法:
class MoviesRepository {
MoviesRepository({required this.client, required this.apiKey});
final Dio client;
final String apiKey;
// search for movies that match a given query (paginated)
Future<List<TMDBMovie>> searchMovies({required int page, String query = ''});
// get the "now playing" movies (paginated)
Future<List<TMDBMovie>> nowPlayingMovies({required int page});
// get the movie for a given id
Future<TMDBMovie> movie({required int movieId});
}
为了为这个仓库创建一个Provider,我们可以这样写:
part 'movies_repository.g.dart';
@riverpod
MoviesRepository moviesRepository(MoviesRepositoryRef ref) => MoviesRepository(
client: ref.watch(dioProvider), // the provider we defined above
apiKey: Env.tmdbApiKey, // a constant defined elsewhere
);
因此,riverpod_generator 将为我们创建一个 moviesRepositoryProvider
和 MoviesRepositoryRef
类型。
当为一个
Repository
创建Provider时,请不要在Repository
类本身上添加@riverpod
注解。相反,请创建一个单独的全局函数,该函数返回该Repository
的实例,并对其进行注解。在下一篇文章中,我们将学习如何在类中使用@riverpod
。
创建和读取带注解的 FutureProvider
正如我们所见,给定一个如下所示的 FutureProvider
:
// a provider to fetch the movie data for a given movie id
final movieProvider = FutureProvider.autoDispose
.family<TMDBMovie, int>((ref, movieId) {
return ref
.watch(moviesRepositoryProvider)
.movie(movieId: movieId);
});
我们可以将其转换为使用@riverpod
注解:
@riverpod
Future<TMDBMovie> movie(
MovieRef ref, {
required int movieId,
}) {
return ref
.watch(moviesRepositoryProvider)
.movie(movieId: movieId);
}
并在我们的小部件内观看它:
class MovieDetailsScreen extends ConsumerWidget {
const MovieDetailsScreen({super.key, required this.movieId});
final int movieId;
@override
Widget build(BuildContext context, WidgetRef ref) {
// movieId is a *named* argument
final movieAsync = ref.watch(movieProvider(movieId: movieId));
return movieAsync.when(
error: (e, st) => Text(e.toString()),
loading: () => CircularProgressIndicator(),
data: (movie) => SomeMovieWidget(movie),
);
}
}
这是最重要的部分:
// movieId is a *named* argument
final movieAsync = ref.watch(movieProvider(movieId: movieId));
正如我们所看到的,movieId
是一个命名参数,因为我们在 movie
函数中明确定义了它。
@riverpod
Future<TMDBMovie> movie(
MovieRef ref, {
required int movieId,
}) {
return ref
.watch(moviesRepositoryProvider)
.movie(movieId: movieId);
}
这意味着我们不再受限于仅使用一个位置参数来定义Provider家族。
事实上,我们甚至不需要关心我们是否在使用家族。
我们只需定义一个带有"ref"对象和尽可能多的命名或位置参数的函数,然后由riverpod_generator
来处理其余的事情。
生成的家族是如何实现的?
如果我们好奇地查看一下movieProvider
是如何生成的,我们可以找到以下内容:
typedef MovieRef = AutoDisposeFutureProviderRef<TMDBMovie>;
@ProviderFor(movie)
const movieProvider = MovieFamily();
class MovieFamily extends Family<AsyncValue<TMDBMovie>> {
const MovieFamily();
MovieProvider call({
required int movieId,
}) {
return MovieProvider(
movieId: movieId,
);
}
...
}
这个使用了可调用类,这是Dart语言的一个很棒的特性,允许我们使用 movieProvider(movieId: movieId)
这种方式来调用,而不是 movieProvider.call(movieId: movieId)
。
这对StreamProvider也适用吗?
正如我们所见,使用@riverpod
很容易生成一个FutureProvider
。
而且,自从Riverpod Generator 2.0.0发布以来,它也支持流(streams)。
实际上,如果我们有一个返回Stream
的方法,我们可以像这样创建相应的Provider:
@riverpod
Stream<int> values(ValuesRef ref) {
return Stream.fromIterable([1, 2, 3]);
}
这是由 Riverpod 2.3 中引入的新 StreamNotifier
类实现的,我在这里更详细地介绍了它:
如果我们使用实时数据库,如 Cloud Firestore,或者与支持 WebSockets 的自定义后端进行通信,那么流和 StreamProvider
就会非常有用,因此它们现在受到 Riverpod Generator 的支持,非常棒!👍
新生成器不支持
StateNotifier
和ChangeNotifier
,因此目前无法将现有代码转换为新的语法,使用StateNotifierProvider
和ChangeNotifierProvider
。但你可以根据Notifier
和AsyncNotifier
类生成提供程序,正如我在这篇文章中所解释的:
让我们再次回顾这个函数:
@riverpod
Future<TMDBMovie> movie(
MovieRef ref, {
required int movieId,
}) {
return ref
.watch(moviesRepositoryProvider)
.movie(movieId: movieId);
}
请注意,在上述内容中,我们在其中调用了 ref.watch(moviesRepositoryProvider)
。
但是,我们是否允许在自动生成的提供程序中使用基于旧语法的提供程序?
事实证明,新的 Riverpod Lint 软件包引入了一个名为 avoid_manual_providers_as_generated_provider_depenency
的新 lint 规则。如果我们不遵循此规则,将会收到以下警告:
生成的提供程序应仅依赖于其他生成的提供程序。不这样做可能会违反"provider_dependencies"等规则。
因此,如果我们计划迁移我们的代码,最好从不依赖于其他提供程序的提供程序开始,并逐步遍历提供程序树,直到所有提供程序都得到更新。👍
使用 autoDispose 与 keepAlive
常见的要求是在不再使用时销毁提供程序的状态。
在旧的语法中,这是通过 autoDispose
修饰符来实现的(默认情况下禁用)。
如果我们使用新的 @riverpod
语法,autoDispose
现在默认启用,并已重命名为 keepAlive
。
这意味着我们可以这样编写:
// keepAlive is false by default
@riverpod
Future<TMDBMovie> movie(MovieRef ref, {required int movieId}) {
...
}
等同于这个:
// keepAlive: false is the same as using autoDispose
@Riverpod(keepAlive: false)
Future<TMDBMovie> movie(MovieRef ref, {required int movieId}) {
...
}
生成的 movieProvider
将在不再使用时被销毁。
另一方面,如果我们将 keepAlive
设置为 true
,该提供程序将保持"活跃"状态:
// keepAlive: true is the same as *NOT* using autoDispose
@Riverpod(keepAlive: true)
Future<TMDBMovie> movie(MovieRef ref, {required int movieId}) {
...
}
请注意,如果您想要获得一个KeepAliveLink
以实现一些自定义缓存行为,您仍然可以在Provider内部执行这样的操作:
@riverpod
Future<TMDBMovie> movie(MovieRef ref, {required int movieId}) {
// get the [KeepAliveLink]
final link = ref.keepAlive();
// start a 60 second timer
final timer = Timer(const Duration(seconds: 60), () {
// dispose on timeout
link.close();
});
// make sure to cancel the timer when the provider state is disposed
ref.onDispose(() => timer.cancel());
return ref
.watch(moviesRepositoryProvider)
.movie(movieId: movieId);
}
要了解如何使用
keepAlive
自定义缓存行为的更多细节,请阅读:超时缓存。
Riverpod 生成器:权衡利弊
现在我们已经了解了新的语法和生成器的工作原理,让我们总结一下其优点和缺点。
优点:自动生成正确类型的Provider
最大的优点是,我们不再需要弄清楚我们需要哪种类型的Provider(Provider
vs FutureProvider
vs StreamProvider
等),因为代码生成器将从函数签名中找出。
新的 @riverpod
语法还使得声明复杂的Provider(如我们在上面看到的 FutureProvider.family
)变得容易。
另一个好处是生成的代码为每个 "ref" 对象创建了一个新的专用类型,这可以很容易从函数名中推断出:
moviesRepository()
→moviesRepositoryProvider
和MoviesRepositoryRef
movie()
→movieProvider
和MovieRef
这使得在首次使用时,如果我们不使用正确的类型,我们的代码将不会编译,从而降低了运行时类型错误的可能性。
优点:默认自动释放资源(autoDispose)
使用新的语法,所有生成的Provider默认使用 autoDispose
。
这是一个明智的选择,因为我们不应该保留不再使用的Provider的状态。
正如我在我的 Riverpod 2.0 指南 中所解释的,我们可以通过调用 ref.keepAlive()
来调整释放行为,甚至在需要时实现基于超时的缓存策略。
优点:Provider的有状态热重载
包的文档中提到:
当修改提供程序的源代码并进行热加载时,Riverpod 将重新执行该提供程序,仅重新执行该提供程序。
这是一个受欢迎的改进。 🙂
缺点:代码生成
Riverpod Generator的缺点归结为一点:代码生成。
即使是最简单的提供程序也会在单独的文件中生成 15 行代码,这可能会减慢构建过程并在项目中添加额外的文件。
如果我们将生成的文件添加到版本控制中,它们将在每次更改时显示在 Pull Requests 中:
GitHub差异显示了自动生成的Provider文件。如果我们不希望显示这些文件,可以将*.g.dart
添加到.gitignore
中,以将所有生成的文件排除在我们的代码库之外。
这有两个影响:
- 其他团队成员在开发过程中需要始终运行
flutter pub run build_runner watch
。 - CI构建工作流需要在编译应用程序之前运行代码生成器(这会导致更长的构建时间,从而增加构建分钟数的消耗)。
在实际使用中,我观察到flutter pub run build_runner watch
很快(至少在小型项目上),在第一次构建后产生亚秒级的更新:
[INFO] ------------------------------------------------------------------------
[INFO] Starting Build
[INFO] Updating asset graph completed, took 0ms
[INFO] Running build completed, took 309ms
[INFO] Caching finalized dependency graph completed, took 12ms
[INFO] Succeeded after 323ms with 4 outputs (16 actions)
这与热重载的响应时间相符,使开发工作流非常顺畅。👍
然而,如果您想在较大的项目上使用 build_runner
,您需要一台性能强大的开发机器。
由于CI构建分钟并不免费,我建议将所有生成的文件都添加到版本控制中(连同.lock
文件,以确保每个人都使用相同的软件包版本)。
不足之处:尚不支持所有Provider类型
在八种不同类型的Provider中,riverpod_generator仅支持以下几种:
Provider
FutureProvider
StreamProvider
NotifierProvider
(在Riverpod 2.0中新增)AsyncNotifierProvider
(在Riverpod 2.0中新增)
不支持旧的Provider类型,如 StateProvider
、StateNotifierProvider
和 ChangeNotifierProvider
,我已经在我的文章中解释了如何用新的Flutter Riverpod Generator中的Notifier
和AsyncNotifier
来替代它们,您可以在这里找到详细信息:如何在新的Flutter Riverpod Generator中使用Notifier
和AsyncNotifier
。
而且,引入了Riverpod Lint包之后,采用新的@riverpod
语法变得更加容易。
因此,无论您的应用是否使用实时数据库并且大量依赖流,还是使用期货与REST API通信,您都可以从新的生成器中受益。
带源代码的示例应用程序
到目前为止,我们已经看到了如何使用新的@riverpod
语法创建Provider。
如果您想知道这在真实世界的应用程序中是如何组合在一起的,我有个好消息告诉您。
事实上,我的两个开源Flutter应用程序已经在使用新的Riverpod Generator。👇
1. TMDB电影应用
第一个是基于TMDB API的电影应用:
TMDB电影应用与Riverpod
该应用支持以下功能:
- 使用分页的无限滚动
- 下拉刷新
- 搜索功能
所有这些功能都是使用Riverpod本地构建的,没有使用外部包。
而且,由于该应用已经使用了Freezed进行JSON序列化,添加riverpod_generator包似乎非常合适。
源代码中还包括了我们没有在这里介绍的内容,例如如何使用dio包的CancelToken
取消网络请求。
这个应用仍然在开发中,我将尽力在将来添加更多功能。
但你已经可以在这里查看它:👇
2. 基于Firebase的时间追踪应用
第二个应用是使用Flutter和Firebase构建的时间追踪应用:
关于使用Flutter和Firebase创建的时间跟踪应用,源代码已经更新到了最新的Riverpod包,你可以在以下链接找到:
如果你愿意,你甚至可以查看我如何将代码迁移到新的Riverpod语法,可以在以下PR链接中找到: 此PR。👍
结论
正如我们所看到的,riverpod_generator包提供了许多功能。以下是使用它的几个原因:
- 自动生成适当类型的提供程序
- 更容易创建带参数的提供程序,克服了“旧”
family
修改符语法的限制 - 提高了类型安全性,在运行时减少了类型错误
- 默认启用
autoDispose
然而,一些传统提供程序类型不受支持。
由于新包依赖于代码生成,你需要:
- 处理项目中额外生成的文件
- 决定生成的文件是否应添加到git并做相应的计划
如果你对代码生成有所疑虑,请考虑Remi Rousselet的观点:
生成的代码不是“样板”。你并不真的关心生成的代码。它并不是为了供开发人员阅读或编辑的。它是为了编译器而存在的,而不是为开发人员。事实上,你可以将其从你的IDE资源管理器中隐藏,通常不提交生成的文件。
总的来说,最显著的优点是提高了开发人员的生产力。
使用新语法意味着你需要学习并使用一个更小且更熟悉的API。这使得Riverpod更容易接近那些对旧API感到困惑的开发人员。
但要明确一点:riverpod_generator是建立在riverpod之上的可选包,而“旧”语法不会很快消失。 由于新的Riverpod语法与旧语法兼容,您可以在代码库中迁移提供程序时逐步采用它。
转自How to Auto-Generate your Providers with Flutter Riverpod Generator
评论区