使用 Flutter Riverpod Generator 自动生成Provider

来源:codewithandrea.com 更新时间:2023-11-09 01:35

Riverpod 是用于 Flutter 的强大的响应式缓存和数据绑定框架。

它提供了许多不同类型的提供者,我们可以使用这些提供者来:

  • 在我们的代码中访问依赖项(使用 Provider
  • 缓存来自网络的异步数据(使用 FutureProviderStreamProvider
  • 管理本地应用程序状态(使用 StateProviderStateNotifierProviderChangeNotifierProvider

但手动编写大量提供者可能容易出错,而选择要使用的提供者也不总是容易的。 🥵

如果我告诉你,你不再需要这样做了呢?

如果你只需在代码中加上 @riverpod 注释,然后让 build_runner 动态生成所有提供者,会怎么样?

事实证明,这就是新的 riverpod_generator 包的用途(它可以让我们的生活变得更加轻松)。

我们将涵盖的内容

有很多内容要涵盖,所以我将把它分为两篇文章。

在第一篇文章中,我们将学习如何使用新的 @riverpod 语法从函数生成提供者。

作为其中的一部分,我将向你展示如何:

  • 使用 @riverpod 语法声明提供者
  • FutureProvider 转换为新的语法
  • 传递参数给提供者,克服旧的 family 修饰符的限制

在下一篇文章中,我们将学习如何从类生成提供者,并了解如何完全替代 StateNotifierProviderStateProvider,使用新的 NotifierAsyncNotifier 类。

你可以在这里找到第二篇文章:如何使用新的 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();
});

另一方面,一些提供者可能有依赖项,并可以使用 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);
});

这个提供者的要点在于,我们可以通过调用以下方法来获取电影:


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

但如果我们不创建上面的提供者,而是像这样写呢?


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

这符合我们定义函数的方式:

  • 首先是返回类型
  • 然后是函数名
  • 然后是参数列表
  • 最后是函数体 A function definition. 1: return type, 2: function name, 3: list of arguments, 4: function body 一个函数定义。1:返回类型,2:函数名,3:参数列表,4:函数体。这比声明一个FutureProvider.family,将返回类型放在参数类型旁边更加直观。

一种新的Riverpod语法?

当Remi在Flutter Vikings期间介绍了新的Riverpod语法时,我有点困惑。

但在我的一些项目中尝试过后,我开始喜欢它的简单性。

新的API更加精简,并带来了两个重要的可用性改进:

  • 你不再需要担心要使用哪个提供者
  • 你可以随意传递命名参数或位置参数给提供者(就像你对待任何函数一样)

这对Riverpod本身来说是一大进步,学习新的API将让你的生活更加轻松。

所以让我来向你展示它是如何工作的。

与其从零开始,我们将从一个现有应用程序中获取一些提供者,并将它们转换为新的语法。最后,我会分享一个包含完整源代码的示例存储库。

使用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_lintcustom_lint这两个包。要了解更多关于riverpod_lint的功能,请阅读:如何通过 Riverpod Lint 和 Riverpod Snippets 提升开发工作流

在 "watch" 模式下启动代码生成器

接下来,我们需要在终端上运行以下命令:


flutter pub run build_runner watch -d

-d标志是可选的,等同于--delete-conflicting-outputs。如其名称所示,它确保我们覆盖以前构建中的任何冲突输出(通常是我们想要的)。

这将监视项目中的所有Dart文件,并在我们进行更改时自动更新生成的代码。

现在让我们开始创建一些提供者。👇

创建第一个带注解的提供者

作为第一步,让我们考虑这个简单的提供者:


// 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.dartA dio_provider.g.dart file is generated alongside our Dart file
一个 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 创建的提供者默认使用 autoDispose 修饰符。如果您不熟悉此内容,请阅读有关 autoDispose 修饰符 的信息。

为 Repository 类创建提供者

现在我们有了一个 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});
}

为了为这个仓库创建一个提供者,我们可以这样写:


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 将为我们创建一个 moviesRepositoryProviderMoviesRepositoryRef 类型。

当为一个 Repository 创建提供者时,请不要在 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);
}

这意味着我们不再受限于仅使用一个位置参数来定义提供者家族。

事实上,我们甚至不需要关心我们是否在使用家族。

我们只需定义一个带有"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的方法,我们可以像这样创建相应的提供者:


@riverpod
Stream<int> values(ValuesRef ref) {
  return Stream.fromIterable([1, 2, 3]);
}

这是由 Riverpod 2.3 中引入的新 StreamNotifier 类实现的,我在这里更详细地介绍了它:


如果我们使用实时数据库,如 Cloud Firestore,或者与支持 WebSockets 的自定义后端进行通信,那么流和 StreamProvider 就会非常有用,因此它们现在受到 Riverpod Generator 的支持,非常棒!👍

新生成器不支持 StateNotifierChangeNotifier,因此目前无法将现有代码转换为新的语法,使用 StateNotifierProviderChangeNotifierProvider。但你可以根据 NotifierAsyncNotifier 类生成提供程序,正如我在这篇文章中所解释的:

让我们再次回顾这个函数:


@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以实现一些自定义缓存行为,您仍然可以在提供者内部执行这样的操作:


@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 vs FutureProvider vs StreamProvider 等),因为代码生成器将从函数签名中找出。

新的 @riverpod 语法还使得声明复杂的提供者(如我们在上面看到的 FutureProvider.family)变得容易。

另一个好处是生成的代码为每个 "ref" 对象创建了一个新的专用类型,这可以很容易从函数名中推断出:

  • moviesRepository()moviesRepositoryProviderMoviesRepositoryRef
  • movie()movieProviderMovieRef

这使得在首次使用时,如果我们不使用正确的类型,我们的代码将不会编译,从而降低了运行时类型错误的可能性。

优点:默认自动释放资源(autoDispose)

使用新的语法,所有生成的提供者默认使用 autoDispose

这是一个明智的选择,因为我们不应该保留不再使用的提供者的状态。

正如我在我的 Riverpod 2.0 指南 中所解释的,我们可以通过调用 ref.keepAlive() 来调整释放行为,甚至在需要时实现基于超时的缓存策略。

优点:提供者的有状态热重载

包的文档中提到:

当修改提供程序的源代码并进行热加载时,Riverpod 将重新执行该提供程序,仅重新执行该提供程序。

这是一个受欢迎的改进。 🙂

缺点:代码生成

Riverpod Generator的缺点归结为一点:代码生成。

即使是最简单的提供程序也会在单独的文件中生成 15 行代码,这可能会减慢构建过程并在项目中添加额外的文件。

如果我们将生成的文件添加到版本控制中,它们将在每次更改时显示在 Pull Requests 中: GitHub diff showing the auto-generated provider file GitHub差异显示了自动生成的提供者文件。如果我们不希望显示这些文件,可以将*.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文件,以确保每个人都使用相同的软件包版本)。

不足之处:尚不支持所有提供者类型

八种不同类型的提供者中,riverpod_generator仅支持以下几种:

  1. Provider
  2. FutureProvider
  3. StreamProvider
  4. NotifierProvider(在Riverpod 2.0中新增)
  5. AsyncNotifierProvider(在Riverpod 2.0中新增)

不支持旧的提供者类型,如 StateProviderStateNotifierProviderChangeNotifierProvider,我已经在我的文章中解释了如何用新的Flutter Riverpod Generator中的NotifierAsyncNotifier来替代它们,您可以在这里找到详细信息:如何在新的Flutter Riverpod Generator中使用NotifierAsyncNotifier

而且,引入了Riverpod Lint包之后,采用新的@riverpod语法变得更加容易。

因此,无论您的应用是否使用实时数据库并且大量依赖流,还是使用期货与REST API通信,您都可以从新的生成器中受益。

带源代码的示例应用程序

到目前为止,我们已经看到了如何使用新的@riverpod语法创建提供者。

如果您想知道这在真实世界的应用程序中是如何组合在一起的,我有个好消息告诉您。

事实上,我的两个开源Flutter应用程序已经在使用新的Riverpod Generator。👇

1. TMDB电影应用

第一个是基于TMDB API的电影应用:
TMDB Movies app with Riverpod

TMDB电影应用与Riverpod

该应用支持以下功能:

  • 使用分页的无限滚动
  • 下拉刷新
  • 搜索功能

所有这些功能都是使用Riverpod本地构建的,没有使用外部包。

而且,由于该应用已经使用了Freezed进行JSON序列化,添加riverpod_generator包似乎非常合适。

源代码中还包括了我们没有在这里介绍的内容,例如如何使用dio包CancelToken取消网络请求。

这个应用仍然在开发中,我将尽力在将来添加更多功能。

但你已经可以在这里查看它:👇

2. 基于Firebase的时间追踪应用

第二个应用是使用Flutter和Firebase构建的时间追踪应用: Time tracking app with Flutter & Firebase 关于使用Flutter和Firebase创建的时间跟踪应用,源代码已经更新到了最新的Riverpod包,你可以在以下链接找到:

如果你愿意,你甚至可以查看我如何将代码迁移到新的Riverpod语法,可以在以下PR链接中找到: 此PR。👍

结论

正如我们所看到的,riverpod_generator包提供了许多功能。以下是使用它的几个原因:

  • 自动生成适当类型的提供程序
  • 更容易创建带参数的提供程序,克服了“旧” family 修改符语法的限制
  • 提高了类型安全性,在运行时减少了类型错误
  • 默认启用autoDispose

然而,一些传统提供程序类型不受支持。

由于新包依赖于代码生成,你需要:

  • 处理项目中额外生成的文件
  • 决定生成的文件是否应添加到git并做相应的计划

如果你对代码生成有所疑虑,请考虑Remi Rousselet的观点:

生成的代码不是“样板”。你并不真的关心生成的代码。它并不是为了供开发人员阅读或编辑的。它是为了编译器而存在的,而不是为开发人员。事实上,你可以将其从你的IDE资源管理器中隐藏,通常不提交生成的文件。

总的来说,最显著的优点是提高了开发人员的生产力。

使用新语法意味着你需要学习并使用一个更小且更熟悉的API。这使得Riverpod更容易接近那些对旧API感到困惑的开发人员。

但要明确一点:riverpod_generator是建立在riverpod之上的可选包,而“旧”语法不会很快消失。 由于新的Riverpod语法与旧语法兼容,您可以在代码库中迁移提供程序时逐步采用它。

那么,为什么不试一试呢? 😎