将 Notifier 和 AsyncNotifier 与新的 Flutter Riverpod Generator 结合使用

来源:codewithandrea.com 更新时间:2023-11-06 17:30

使用Riverpod编写Flutter应用程序在引入riverpod_generator包后变得更加简便。

在新的Riverpod语法中,我们使用@riverpod注解,并让build_runner动态生成所有的提供程序。

我已经在这篇文章中涵盖了所有的基础知识:

在这篇文章中,我们将深入学习Riverpod 2.0中新增的NotifierAsyncNotifier类。

2023年3月更新:我们还将介绍新增的StreamNotifier类,它已经添加到Riverpod 2.3中。

这些类旨在替代StateNotifier并带来一些新的优势:

  • 更容易执行复杂的异步初始化
  • 更符合人体工程学的API:不再需要在各处传递ref
  • 不再需要手动声明提供程序(如果我们使用Riverpod Generator)

最终,您将知道如何轻松创建自定义状态类,并快速生成复杂的提供程序,使用riverpod_generator

准备好了吗?让我们开始吧!

本文假设您已经熟悉Riverpod。如果您对Riverpod还不熟悉,请阅读:Flutter Riverpod 2.0:终极指南

为了使本教程更容易理解,我们将使用两个示例。

1. 简单计数器

第一个示例将是一个基于StateProvider的简单计数器。

我们将把它转换为新的Notifier并学习它的语法。 接下来,我们将加入Riverpod Generator并了解如何自动生成相应的NotifierProvider

2. 鉴权控制器

然后,我们将研究一个更复杂的示例,其中包含一些基于StateNotifier的异步逻辑。

我们将把它转换为使用新的AsyncNotifier类,并学习有关异步初始化的一些细微差别。

然后,我们还将将其转换为使用Riverpod Generator并生成相应的AsyncNotifierProvider


最后,我们将总结NotifierAsyncNotifier的优势,以便您可以决定是否要在您的应用程序中使用它们。

我还会分享一些展示所有内容如何组合在一起的源代码。

让我们开始吧!

一个简单的计数器与StateProvider

作为第一步,让我们考虑一个简单的StateProvider,以及一个使用它的CounterWidget

// 1. declare a [StateProvider]
final counterProvider = StateProvider<int>((ref) {
  return 0;
});
// 2. create a [ConsumerWidget] subclass
class CounterWidget extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    // 3. watch the provider and rebuild when the value changes
    final counter = ref.watch(counterProvider);
    return ElevatedButton(
      // 4. use the value
      child: Text('Value: $counter'),
      // 5. change the state inside a button callback
      onPressed: () => ref.read(counterProvider.notifier).state++,
    );
  }
}

这里没有什么花里胡哨的东西:

  • 我们可以在 build 方法中观察计数器的值
  • 我们可以在按钮回调中递增它

正如我们所看到的,StateProvider 很容易声明:

final counterProvider = StateProvider<int>((ref) {
  return 0;
});

这个适合存储和更新简单的变量,比如上面的计数器。

但是如果您的状态需要一些验证逻辑,或者您需要表示更复杂的对象,StateProvider 就不适用了。

而且,虽然 StateNotifier 对于更高级的情况是一个合适的替代方案,但现在推荐使用新的 Notifier 类。

Notifier 是如何工作的?

以下是我们如何声明一个基于 Notifier 类的 Counter 类。

// counter.dart
import 'package:flutter_riverpod/flutter_riverpod.dart';
class Counter extends Notifier<int> {
  @override
  int build() {
    return 0;
  }
  void increment() {
    state++;
  }
}

有两点需要注意:

  • 我们有一个build方法,返回一个int(初始值)
  • 我们可以(可选地)添加一个方法来增加状态(我们的计数器值)

如果我们想为这个类创建一个提供者,我们可以这样做:

final counterProvider = NotifierProvider<Counter, int>(() {
  return Counter();
});

另外,我们可以使用Counter.new作为构造函数引用:

final counterProvider = NotifierProvider<Counter, int>(Counter.new);

在小部件中使用 NotifierProvider

事实证明,只要我们导入 counter.dart 文件,我们就可以在 CounterWidget 中使用 counterProvider,而无需进行任何更改:

import 'counter.dart';
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(counterProvider);
    return ElevatedButton(
      // 2. use the value
      child: Text('Value: $counter'),
      // 3. change the state inside a button callback
      onPressed: () => ref.read(counterProvider.notifier).state++,
    );
  }
}

由于我们还拥有一个 increment 方法,如果我们希望的话,我们可以这样做:

onPressed: () => ref.read(counterProvider.notifier).increment(),

increment 方法使我们的代码更富表现力。但它是可选的,因为如果需要,我们仍然可以直接修改状态。

StateProvider 与 NotifierProvider

到目前为止,我们已经了解到,当我们需要修改简单变量时,StateProvider 表现得很好。

但是,如果我们的状态(以及更新状态的逻辑)更为复杂,NotifierNotifierProvider 是一个良好的选择,仍然容易实现:

// counter.dart
import 'package:flutter_riverpod/flutter_riverpod.dart';
class Counter extends Notifier<int> {
  @override
  int build() {
    return 0;
  }
  void increment() {
    state++;
  }
}
final counterProvider = NotifierProvider<Counter, int>(Counter.new);

如果我们愿意的话,我们可以自动化生成提供程序。

使用RiverpodGenerator的Notifier

以下是如何使用新的 @riverpod 语法声明相同的 Counter 类:

import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'counter.g.dart';
@riverpod
class Counter extends _$Counter {
  @override
  int build() {
    return 0;
  }
  void increment() {
    state++;
  }
}

请注意,在这种情况下,我们扩展_$Counter而不是Notifier

如果我们运行flutter pub run build_runner watchcounter.g.dart文件将为我们生成,并包含以下代码:

/// See also [Counter].
final counterProvider = AutoDisposeNotifierProvider<Counter, int>(
  Counter.new,
  name: r'counterProvider',
  debugGetCreateSourceHash:
      const bool.fromEnvironment('dart.vm.product') ? null : $CounterHash,
);
typedef CounterRef = AutoDisposeNotifierProviderRef<int>;
abstract class _$Counter extends AutoDisposeNotifier<int> {
  @override
  int build();
}

两个主要需要注意的事情是:

  • 为我们创建了一个 counterProvider
  • _$Counter 继承自 AutoDisposeNotifier

AutoDisposeNotifier 在 Riverpod 包内定义如下:

/// {@template riverpod.notifier}
abstract class AutoDisposeNotifier<State>
    extends BuildlessAutoDisposeNotifier<State> {
  /// {@macro riverpod.asyncnotifier.build}
  @visibleForOverriding
  State build();
}

正如我们所看到的,build 方法返回了一个通用的 State 类型。

但这与我们的 Counter 类有什么关系,Riverpod Generator 又是如何知道要使用哪种类型呢? counter-state-int.png build 方法的返回类型决定了 state 属性的类型。答案是 _$Counter 继承自 AutoDisposeNotifier,而 state 属性也是一个 int,因为我们定义了 build 方法返回 int

一旦我们决定使用哪种类型,如果我们想避免编译时错误,就需要一致使用它。

在我们的小部件类内部,只要我们导入生成的 counter.g.dart 文件,所有的代码都将继续工作:

import 'counter.g.dart';
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(counterProvider);
    return ElevatedButton(
      // 2. use the value
      child: Text('Value: $counter'),
      // 3. change the state inside a button callback
      onPressed: () => ref.read(counterProvider.notifier).state++,
    );
  }
}

StateProvider还是Notifier?

我们已经涵盖了一些重要的概念,让我们在继续之前做一个简要的总结。

StateProvider 仍然是存储简单状态的最简单方式:

final counterProvider = StateProvider<int>((ref) {
  return 0;
});

但我们也可以通过创建一个 Notifier 的子类和一个 NotifierProvider 来实现相同的效果:

// counter.dart
import 'package:flutter_riverpod/flutter_riverpod.dart';
class Counter extends Notifier<int> {
  @override
  int build() {
    return 0;
  }
  void increment() {
    state++;
  }
}
final counterProvider = NotifierProvider<Counter, int>(Counter.new);

这个表述更加详细,同时也更加灵活,因为我们可以为我们的 Notifier 子类添加具有复杂逻辑的方法(就像我们在 StateNotifier 中所做的那样)。

而且,如果我们愿意,我们可以使用新的 @riverpod 语法,并自动生成 counterProvider

import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'counter.g.dart';
@riverpod
class Counter extends _$Counter {
  @override
  int build() {
    return 0;
  }
  void increment() {
    state++;
  }
}
// counterProvider will be generated by build_runner

我们创建的Counter类是存储同步状态的一个简单示例。

正如我们即将看到的,我们可以使用AsyncNotifier创建异步状态类,完全替代StateNotifierStateNotifierProvider

使用StateNotifier进行更复杂示例

如果您已经使用Riverpod一段时间,您可能已经习惯于编写StateNotifier子类,以存储一些不可变状态,供您的小部件监听。

例如,我们可能希望使用自定义按钮类登录用户: sign-in-sign-out.gif 一个按钮类,用于进行登录操作。为了实现登录逻辑,我们可以创建以下的StateNotifier子类:

dart
class SignInButton {
  // Your button class implementation here
}

class SignInNotifier extends StateNotifier {
  // Implementation for sign-in logic
}
class AuthController extends StateNotifier<AsyncValue<void>> {
  AuthController(this.ref)
      // set the initial state (synchronously)
      : super(const AsyncData(null));
  final Ref ref;
  Future<void> signInAnonymously() async {
    // read the repository using ref
    final authRepository = ref.read(authRepositoryProvider);
    // set the loading state
    state = const AsyncLoading();
    // sign in and update the state (data or error)
    state = await AsyncValue.guard(authRepository.signInAnonymously);
  }
}

我们可以通过调用AuthRepositorysignInAnonymously方法来进行匿名登录。

当我们在Notifier内进行异步工作时,可以多次设置状态。这样,小部件可以重建并显示每种可能的状态(数据、加载和错误)的正确UI。

如果您对存储库模式不熟悉,请阅读这篇文章:Flutter应用程序架构:存储库模式

如果您对AsyncValue.guard的语法不熟悉,请阅读这篇文章:在StateNotifier子类内使用AsyncValue.guard而不是try/catch

此外,我们还需要创建相应的StateNotifierProvider,以便我们可以在小部件内调用watchreadlisten

final authControllerProvider = StateNotifierProvider<
    AccountScreenController, AsyncValue<void>>((ref) {
  return AuthController(ref);
});

您可以使用此提供程序在按钮回调中获取控制器并调用 signInAnonymously()

onPressed: () => ref.read(authControllerProvider.notifier).signInAnonymously(),

虽然采用这种方法没有问题,但 StateNotifier 不能进行异步初始化。

而且声明 StateNotifierProvider 的语法有点笨拙,因为它需要两个类型注解。

但请稍等!在先前的文章中,我们已经学到Riverpod Generator可以为我们生成提供程序!

那么我们可以使用它来做类似这样的事情吗?

@riverpod
class AuthController extends StateNotifier<AsyncValue<void>> {
  ...
}

如果我们尝试执行以下命令并运行 flutter pub run build_runner watch,会出现如下错误:

Provider classes must contain a method named `build`.

事实证明,我们不能在 StateNotifier 中使用 @riverpod 语法。相反,我们应该使用新的 AsyncNotifier 类。

AsyncNotifier 的工作原理

Riverpod 文档将 AsyncNotifier 定义为一种异步初始化的 Notifier 实现。

下面是如何使用它来转换我们的 AuthController 类:

// 1. add the necessary imports
import 'dart:async';
import 'package:flutter_riverpod/flutter_riverpod.dart';
// 2. extend [AsyncNotifier]
class AuthController extends AsyncNotifier<void> {
  // 3. override the [build] method to return a [FutureOr]
  @override
  FutureOr<void> build() {
    // 4. return a value (or do nothing if the return type is void)
  }
  Future<void> signInAnonymously() async {
    // 5. read the repository using ref
    final authRepository = ref.read(authRepositoryProvider);
    // 6. set the loading state
    state = const AsyncLoading();
    // 7. sign in and update the state (data or error)
    state = await AsyncValue.guard(authRepository.signInAnonymously);
  }
}
  • 基类为 AsyncNotifier 而不是 StateNotifier>
  • 我们需要覆盖 build 方法,并返回初始值(如果返回类型为 void,则返回空)。
  • signInAnonymously 方法中,我们使用 ref 对象读取了另一个提供程序,尽管我们没有明确将 ref 声明为属性(下面会详细解释)。

还请注意使用 FutureOr:这是一个表示既可以是 Future 也可以是 T 的值的类型。在我们的示例中很有用,因为底层类型是 void,我们没有什么可返回的。

AsyncNotifier 相对于 StateNotifier 的一个优势是它允许我们异步初始化状态。有关更多详细信息,请参见下面的示例(具有异步初始化)

声明 AsyncNotifierProvider

在我们可以使用更新后的 AuthController 之前,我们需要声明相应的 AsyncNotifierProvider

final authControllerProvider = AsyncNotifierProvider<AuthController, void>(() {
  return AuthController();
});

或者,使用构造函数的引用:

final authControllerProvider =
    AsyncNotifierProvider<AuthController, void>(AuthController.new);

请注意创建提供程序的函数没有 ref 参数。

然而,ref 始终作为 NotifierAsyncNotifier 子类内部的属性可访问,这使得很容易读取其他提供程序。

这与 StateNotifier 不同,对于 StateNotifier,如果我们想要使用它,需要将 ref 显式传递为构造函数参数。

关于自动释放的注意事项

请注意,如果您像这样声明 AsyncNotifier 和相应的 AsyncNotifierProvider,并使用 autoDispose

class AuthController extends AsyncNotifier<void> {
  ...
}
// note: this will produce a runtime error
final authControllerProvider =
    AsyncNotifierProvider.autoDispose<AuthController, void>(AuthController.new);

那么您将会收到一个运行时错误:

Error: Type argument 'AuthController' doesn't conform to the bound 'AutoDisposeAsyncNotifier<T>' of the type variable 'NotifierT' on 'AutoDisposeAsyncNotifierProviderBuilder.call'.

使用AsyncNotifierautoDispose的正确方式是扩展AutoDisposeAsyncNotifier类:

// using AutoDisposeAsyncNotifier
class AuthController extends AutoDisposeAsyncNotifier<int> {
  ...
}
// using AsyncNotifierProvider.autoDispose
final authControllerProvider =
    AsyncNotifierProvider.autoDispose<AuthController, void>(AuthController.new);

.autoDispose 修饰符可用于在所有侦听器被移除时重置提供程序的状态。有关更多信息,请阅读:autoDispose 修饰符

好消息是,如果我们使用 Riverpod Generator,就无需担心正确的语法。

使用 Riverpod Generator 的 AsyncNotifier

就像我们使用 @riverpod 语法与 Notifier 一样,我们也可以在 AsyncNotifier 中执行相同的操作。

以下是如何将 AuthController 转换为使用它:

// 1. import this
import 'package:riverpod_annotation/riverpod_annotation.dart';
// 2. declare a part file
part 'auth_controller.g.dart';
// 3. annotate
@riverpod
// 4. extend like this
class AuthController extends _$AuthController {
  // 5. override the [build] method to return a [FutureOr]
  @override
  FutureOr<void> build() {
    // 6. return a value (or do nothing if the return type is void)
  }
  Future<void> signInAnonymously() async {
    // 7. read the repository using ref
    final authRepository = ref.read(authRepositoryProvider);
    // 8. set the loading state
    state = const AsyncLoading();
    // 9. sign in and update the state (data or error)
    state = await AsyncValue.guard(authRepository.signInAnonymously);
  }
}

因此,基类为 _$AuthController,并且是自动生成的。

如果我们查看生成的代码,会发现以下内容:

/// See also [AuthController].
final authControllerProvider =
    AutoDisposeAsyncNotifierProvider<AuthController, void>(
  AuthController.new,
  name: r'authControllerProvider',
  debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
      ? null
      : $AuthControllerHash,
);
typedef AuthControllerRef = AutoDisposeAsyncNotifierProviderRef<void>;
abstract class _$AuthController extends AutoDisposeAsyncNotifier<void> {
  @override
  FutureOr<void> build();
}

需要注意的两个主要点是:

  • 我们创建了一个名为authControllerProvider的提供者。
  • _$AuthController 继承了 AutoDisposeAsyncNotifier

另外,这个类在Riverpod包内部定义如下:

/// {@macro riverpod.asyncnotifier}
abstract class AutoDisposeAsyncNotifier<State>
    extends BuildlessAutoDisposeAsyncNotifier<State> {
  /// {@macro riverpod.asyncnotifier.build}
  @visibleForOverriding
  FutureOr<State> build();
}

这次,build 方法返回一个 FutureOr

以下是我们的 AuthController 类,再次提供: async-notifier-void.png

AsyncNotifier子类:如果`build`方法返回一个Future,状态将是一个AsyncValue。从上面的图表可以看出,我们处理`void`、`FutureOr`和`AsyncValue`。

那么,这些类型之间有什么关系呢?

嗯,状态属性的类型是`AsyncValue`,因为`build`方法的返回类型是`FutureOr`。

这意味着我们可以在`signInAnonymously`方法中将状态设置为`AsyncData`、`AsyncLoading`或`AsyncError`。

StateNotifier还是AsyncNotifier?

为了比较,这里是基于`StateNotifier`的先前实现:
class AuthController extends StateNotifier<AsyncValue<void>> {
  AuthController(this.ref) : super(const AsyncData(null));
  final Ref ref;
  Future<void> signInAnonymously() async {
    final authRepository = ref.read(authRepositoryProvider);
    state = const AsyncLoading();
    state = await AsyncValue.guard(authRepository.signInAnonymously);
  }
}
final authControllerProvider =
    StateNotifierProvider<AuthController, AsyncValue<void>>((ref) {
  return AuthController(ref);
});

 

@riverpod
class AuthController extends _$AuthController {
  @override
  FutureOr<void> build() {
    // return a value (or do nothing if the return type is void)
  }
  Future<void> signInAnonymously() async {
    final authRepository = ref.read(authRepositoryProvider);
    state = const AsyncLoading();
    state = await AsyncValue.guard(authRepository.signInAnonymously);
  }
}

使用@riverpod语法,代码量更少,因为我们不再需要手动声明提供程序。

而且,由于ref作为所有Notifier子类的属性可用,我们无需再传递它。

我们的小部件中的代码保持不变,因为我们可以像以前一样监视、读取或监听authControllerProvider

带有异步初始化的示例

由于AsyncNotifier支持异步初始化,我们可以编写如下代码:

@riverpod
class SomeOtherController extends _$SomeOtherController {
  @override
  // note the [Future] return type and the async keyword
  Future<String> build() async {
    final someString = await someFutureThatReturnsAString();
    return anotherFutureThatReturnsAString(someString);
  }
  // other methods here
}

在这种情况下,build方法是真正异步的,只有当future完成时才会返回。

但是,任何监听器小部件的build方法需要同步返回,不能等待future完成:

class SomeWidget extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    // returns AsyncLoading on first load,
    // rebuilds with the new value when the initialization is complete
    final valueAsync = ref.watch(someOtherControllerProvider);
    return valueAsync.when(...);
  }
}

为了处理这种情况,控制器将发出两个状态,而小部件将重建两次:

  • 首次加载时,使用临时的AsyncLoading值进行一次重建
  • 初始化完成后,再次使用新的AsyncData值(或AsyncError)进行重建

另一方面,如果使用同步的Notifier或带有build方法的AsyncNotifier,该方法返回FutureOr并且未标记为async,那么初始状态将立即可用,并且小部件只会在首次加载时进行一次重建。

示例:向AsyncNotifier传递参数

有时,您可能需要向AsyncNotifier传递附加参数。

这可以通过在build方法中声明它们为命名参数或位置参数来完成:

@riverpod
class SomeOtherController extends _$SomeOtherController {
  @override
  // you can add named or positional parameters to the build method
  Future<String> build(int someValue) async {
    final someString = await someFutureThatReturnsAString(someValue);
    return anotherFutureThatReturnsAString(someString);
  }
  // other methods here
}

随后,当您观看、阅读或收听提供者时,您可以简单地将它们作为参数传递:

// this provider takes a positional argument of type int
final state = ref.watch(someOtherControllerProvider(42));

在声明和传递参数给 `AsyncNotifier` 或其他提供者时,语法是相同的。毕竟,它们只是常规的函数参数,而 Riverpod Generator 会为我们处理一切。要获取更多细节,请查看我的之前文章中的创建和读取带注释的 FutureProvider

Riverpod 2.3 中的新特性:StreamNotifier

随着Riverpod Generator 2.0.0的发布,现在可以生成一个返回 `Stream` 的提供者:

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

如果我们使用Riverpod Lint包,我们可以将上面的提供程序转换为一个“有状态”的变体: convert-stateful-provider.png 将一个使用Riverpod Lint包的函数型提供程序转换为类型提供程序后,以下是结果:

使用Riverpod Lint包将提供程序从函数型转换为类型后的结果如下:

@riverpod
class Values extends _$Values {
  @override
  Stream<int> build() {
    return Stream.fromIterable([1, 2, 3]);
  }
}

在底层,build_runner 将生成一个 StreamNotifier 以及相应的 AutoDisposeStreamNotifierProvider

AsyncNotifierStreamNotifier 是旧的 FutureProviderStreamProvider 的类变体。如果您需要监视 FutureStream,同时还要添加执行某些数据变更操作的方法,类变体是一种不错的选择。

Notifier 和 AsyncNotifier:是否值得使用?

长时间以来,StateNotifier 一直在为我们提供服务,提供了一个存储复杂状态和修改状态逻辑的地方,使其不再依赖于小部件树。

NotifierAsyncNotifier 旨在取代 StateNotifier 并带来一些新的好处:

  • 更容易执行复杂的异步初始化
  • 更符合人体工程学的 API:不再需要传递 ref
  • 不再需要手动声明提供者(如果使用 Riverpod Generator)

对于新项目来说,这些好处是值得的,因为新的类可以帮助您用更少的代码实现更多的功能。

但如果您有很多现有代码使用 StateNotifier,则由您决定是否(或何时)迁移到新的语法。

无论如何,StateNotifier 还会存在一段时间,如果您愿意,可以逐个迁移您的提供者。

如何测试 AsyncNotifier 子类?

我们在这里没有涵盖的一个方面是如何编写 NotifierAsyncNotifier 子类的单元测试。

这是一个有趣的话题,我在这篇文章中详细介绍了它:

结论

自从引入以来,Riverpod 从一个简单的状态管理解决方案演变为一种响应式缓存和数据绑定框架。

Riverpod 可以通过类似 FutureProviderStreamProviderAsyncValue 的类轻松处理异步数据。 同样,新的 NotifierAsyncNotifierStreamNotifier 类使得使用人性化的API轻松创建自定义状态类。

在Riverpod中,找出所有提供者和修饰符(autoDisposefamily)的正确语法组合是一个主要的痛点。

但是有了新的 riverpod_generator 包,所有这些问题都会消失,因为您可以利用 build_runner 动态生成所有提供者。

而新的 riverpod_lint 包,则提供了Riverpod特定的Lint规则和代码辅助,帮助我们获得正确的语法。


通过我所有的 Riverpod 文章,我想为您详细介绍Riverpod的功能。