点击上方蓝字关注我,知识会给你力量

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
      
 ((ref) {    return Dio(); });

另一方面,有些providers具有依赖性,可能会使用family修饰符接收一个参数:

// a provider to fetch the movie data for a given movie id 
final movieProvider = FutureProvider.autoDispose
    .family
      
 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
      
 int>((ref, movieId) {    return ref       .watch(moviesRepositoryProvider)       .movie(movieId: movieId); });

该provider的精髓在于,我们可以通过调用该方法来获取影片:

// declared inside a MoviesRepository class 
Future
      
  movie({required  int movieId});

但是,如果我们不创建上述provider,而是编写这样的程序呢?

@riverpod
Future
      
  movie(   MovieRef ref, {   required  int movieId, }) {    return ref       .watch(moviesRepositoryProvider)       .movie(movieId: movieId); }

这与我们定义函数的方法一致:

  • 先确定返回类型

  • 然后是函数名

  • 然后是参数列表

  • 然后是函数体

打开网易新闻 查看精彩图片

这比声明一个 FutureProvider.family (返回类型紧挨着参数类型)更直观。

新的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
      
 ((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
      
 .internal(   dio,   name:  r'dioProvider',   debugGetCreateSourceHash:        const  bool.fromEnvironment( 'dart.vm.product') ?  null : _$dioHash,   dependencies:  null,   allTransitiveDependencies:  null, ); typedef DioRef = AutoDisposeProviderRef ; // 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

  searchMovies({required  int page,  String query =  ''});    // get the "now playing" movies (paginated)   Future< List > nowPlayingMovies({required  int page});    // get the movie for a given id   Future  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
      
 int>((ref, movieId) {    return ref       .watch(moviesRepositoryProvider)       .movie(movieId: movieId); });

我们可以将其转换为使用 @riverpod 注解:

@riverpod
Future
      
  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
      
  movie(   MovieRef ref, {   required  int movieId, }) {    return ref       .watch(moviesRepositoryProvider)       .movie(movieId: movieId); }

这意味着我们不再受限于只定义一个位置参数的provider。

事实上,我们甚至根本不在乎是否使用了family参数。

我们只需定义一个带有 “ref ”对象和任意多个命名或位置参数的函数,剩下的就交给 riverpod_generator。

它是如何生成families参数实现的?

如果我们好奇地看一看 movieProvider 是如何生成的,我们就会发现这一点:

typedef MovieRef = AutoDisposeFutureProviderRef
      
 ; @ProviderFor(movie) const movieProvider = MovieFamily(); class MovieFamily extends Family

 > {    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

  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
      
  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
      
  movie(MovieRef ref, {required  int movieId}) {   ... }

相当于这样:

// keepAlive: false is the same as using autoDispose 
@Riverpod(keepAlive: false)
Future
      
  movie(MovieRef ref, {required  int movieId}) {   ... }

当不再使用时,生成的 movieProvider 将被废弃。

另一方面,如果我们将 keepAlive 设置为 true,那么provider就会一直 “活着”:

// keepAlive: true is the same as *NOT* using autoDispose 
@Riverpod(keepAlive: true)
Future
      
  movie(MovieRef ref, {required  int movieId}) {   ... }

请注意,如果您想获取 KeepAliveLink 以实现某些自定义缓存行为,您仍然可以在provider中这样做:

@riverpod
Future
      
  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小时后转载。

作者:徐宜生

更文不易,点个“三连”支持一下