点击上方蓝字关注我,知识会给你力量
Riverpod 是 Flutter 功能强大的反应式缓存和数据绑定框架。
它为我们提供了许多不同类型的provider,我们可以用它们来:
访问代码中的依赖关系(使用 Provider)
缓存来自网络的异步数据(使用 FutureProvider 和 StreamProvider)
管理本地应用程序状态(使用 StateProvider、StateNotifierProvider 和 ChangeNotifierProvider)
但是,手工编写大量provider很容易出错,而且选择使用哪个provider也并非易事。🥵
如果我告诉你,你不再需要这样做呢?
如果您只需在代码中注释 @riverpod,然后让 build_runner 即时生成所有provider,会怎么样?
事实证明,这就是新的 riverpod_generator 的作用(它能让我们的生活变得更轻松)。
我们准备介绍什么
要介绍的内容很多,因此我将分成两篇文章来介绍。
在第一篇文章中,我们将学习如何使用新的 @riverpod 语法从函数生成providers。
作为其中的一部分,我将向你展示如何:
使用 @riverpod 语法声明providers
将 FutureProvider 转换为新语法
克服旧family修饰符的限制,将参数传递给providers
在下一篇文章中,我们将学习如何从类中生成providers,并了解如何用新的 Notifier 和 AsyncNotifier 类完全替换 StateNotifierProvider 和 StateProvider。
我们还将介绍一些折衷方法,以便您决定是否在自己的应用程序中使用新语法。
准备好了吗?开始吧 👇
❝本文假定你已经熟悉 Riverpod。
我们真的需要手写providers吗?
这是一个很好的问题。
一方面,您可能有这样的简单providers:
// a provider for the Dio client to be used by the rest of the app
final dioProvider = Provider<Dio>((ref) {
return Dio();
});
另一方面,有些providers具有依赖性,可能会使用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);
});
如果你的 StateNotifierProvider 带有family修饰符,语法就会变得更加复杂,因为你必须指定三个类型注解:
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 annotation
让我们再来看看这个 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);
}
这与我们定义函数的方法一致:
先确定返回类型
然后是函数名
然后是参数列表
然后是函数体
这比声明一个 FutureProvider.family<TMDBMovie, int>(返回类型紧挨着参数类型)更直观。
新的Riverpod syntax?
当 Remi 在 Flutter Vikings 大会上介绍新的 Riverpod 语法时,我有点困惑。
但在我的一些项目中试用后,我越来越喜欢它的简洁性。
新的应用程序接口更加精简,并带来了两个显著的可用性改进:
您不必再为使用哪个provider而烦恼
您可以随心所欲地将命名参数或位置参数传递给provider(就像使用任何函数一样)
这是 Riverpod 本身的一大飞跃,学习新的 API 将使你的生活更加轻松。
让我向你展示这一切是如何工作的。
我们不是从零开始,而是从现有应用程序中提取一些provider,并将它们转换为新语法。
从riverpod_generator开始
正如 pub.dev 上的 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。
通过"watch"方式来实现代码生成
然后,我们需要在终端上运行此命令:
dart run build_runner watch -d
-d 标志是可选的,与 --delete-conflicting-outputs 相同。正如命名所暗示的,它确保我们覆盖先前构建中的任何冲突输出(这通常是我们想要的)。
这将监视我们项目中的所有 Dart 文件,并在我们进行修改时自动更新生成的代码。
因此,让我们开始创建一些provider。👇
创建第一个annotated 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:
新的 .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 class创建一个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。
创建和使用一个annotated 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);
}
在我们的Widget中进行watch:
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。
事实上,我们甚至根本不在乎是否使用了family参数。
我们只需定义一个带有 “ref ”对象和任意多个命名或位置参数的函数,剩下的就交给 riverpod_generator。
它是如何生成families参数实现的?
如果我们好奇地看一看 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。
事实上,如果我们有一个返回streams的方法,就可以像这样创建相应的provider:
@riverpod
Stream<int> values(ValuesRef ref) {
return Stream.fromIterable([1, 2, 3]);
}
这要归功于 Riverpod 2.3 中新引入的 StreamNotifier 类。
如果我们使用实时数据库(如 Cloud Firestore),或者与支持 Web Socket的自定义后端通信,Streams 和 StreamProvider 将非常有用,因此 Riverpod Generator 支持它们是件好事。👍
新的生成器不支持 StateNotifier 和 ChangeNotifier,因此你还不能将使用 StateNotifierProvider 和 ChangeNotifierProvider 的现有代码转换为新语法。不过,你可以根据 Notifier 和 AsyncNotifier 类生成provider,我在本文中已经解释过了。
新旧操作符混合使用
让我们再次重温这个功能:
@riverpod
Future<TMDBMovie> movie(
MovieRef ref, {
required int movieId,
}) {
return ref
.watch(moviesRepositoryProvider)
.movie(movieId: movieId);
}
请注意,我们在其中调用了 ref.watch(moviesRepositoryProvider)。
但是,我们可以在自动生成的provider中使用基于旧语法的provider吗?
事实证明,新的 Riverpod Lint 软件包引入了一条新的 lint 规则,名为 avoid_manual_providers_as_generated_provider_depenency。如果我们不遵守这条规则,就会收到这样的警告:
❝生成的provider只能依赖于其他生成的provider。否则可能会破坏 “provider_dependencies ”等规则。
因此,如果我们计划迁移代码,最好先从不依赖其他provider的provider开始,然后逐步更新provider树,直到所有provider都更新完毕。👍
使用autoDispose vs keepAlive
一个常见的需求是在不再使用provider时销毁其状态。
在旧语法中,这是通过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,那么provider就会一直 “活着”:
// 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);
}
Riverpod Generator的利弊
现在,我们已经了解了新语法和生成器的工作原理,让我们来总结一下利弊。
优势: 自动选择正确类型的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的有状态热重载
软件包文档是这样说的:
❝当修改provider的源代码时,Riverpod 会在热重载时重新执行该provider,并且只执行该provider。
这是一个值得欢迎的改进。
劣势: code generation
Riverpod Generator 的缺点归结为一点:代码生成。
即使是最简单的provider,也会在一个单独的文件中生成 15 行代码,这不仅会减慢构建过程,还会让我们的项目充满额外的文件。
如果我们将生成的文件添加到版本控制中,每当它们发生变化时,就会出现在拉取请求中:
如果不希望这样,我们可以在 .gitignore 中添加 *.g.dart 来排除版本库中所有生成的文件。
这样做有两个好处:
在开发过程中,其他团队成员需要始终运行 dart run build_runner watch -d
CI 构建工作流需要在编译应用程序之前运行代码生成器(导致构建时间延长,花费更多的构建时间)
在实践中,我发现 dart run build_runner watch -d 运行速度很快(至少在小型项目中是如此),第一次构建后会产生亚秒级的更新:
[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 (new in Riverpod 2.0)
AsyncNotifierProvider (new in Riverpod 2.0)
不支持 StateProvider、StateNotifierProvider 和 ChangeNotifierProvider 等传统provider,我已经在关于如何在新的 Flutter Riverpod Generator 中使用 Notifier 和 AsyncNotifier 的文章中介绍了如何替换它们。
随着 Riverpod Lint 软件包的推出,采用新的 @riverpod 语法变得更加容易。
因此,无论您的应用程序是使用实时数据库并严重依赖流,还是使用期货与 REST API 对话,您都可以从新的生成器中获益。
总结
正如我们所看到的,riverpod_generator 软件包有很多功能。以下是使用它的几个理由:
自动生成合适的provider
克服了 “旧 ”family修饰符语法的限制,更容易创建带参数的provider
提高了类型安全性,减少了运行时的类型错误
默认情况下autoDispose
不过,某些传统的provider类型不受支持。
由于新软件包依赖于代码生成,因此必须:
处理项目中额外的自动生成文件
决定是否将生成的文件添加到 git,并制定相应的计划
如果您对代码生成持观望态度,请考虑一下 Remi Rousselet 的观点:
❝生成的代码不是 “模板”。您并不关心生成的代码。它并不是用来阅读或编辑的。它是为编译器准备的,而不是为开发人员准备的。事实上,你可以将其从集成开发环境资源管理器中隐藏起来,而且通常不会提交生成的文件。
总的来说,最显著的优势是提高了开发人员的工作效率。
使用新语法意味着你需要学习和使用一个更小、更熟悉的 API。这让那些对旧 API 感到困惑的开发人员更容易接受 Riverpod。
但需要明确的是:riverpod_generator是构建在riverpod之上的一个可选包,“旧 ”语法不会很快消失。
因为新的Riverpod语法与旧的兼容,所以你可以在迁移代码库中的提供者时逐步采用新的语法。
原文翻译链接:https://codewithandrea.com/articles/flutter-riverpod-generator/
向大家推荐下我的网站 https://www.yuque.com/xuyisheng 点击原文一键直达
专注 Android-Kotlin-Flutter 欢迎大家访问
往期推荐
本文原创公众号:群英传,授权转载请联系微信(Tomcat_xu),授权后,请在原创发表24小时后转载。
< END >
作者:徐宜生
更文不易,点个“三连”支持一下👇