Flutter Riverpod 2.0:终极指南

来源:codewithandrea.com 时间:2022-11-08 23:20

Riverpod 是一个 响应式缓存和数据绑定框架 的演变而诞生的。 ,它是作为Provider

根据 官方文档

Riverpod 是对 Provider 包的完全重写,以进行原本不可能的改进。

许多人仍然将其视为“状态管理”框架。

但远不止于此。

事实上,Riverpod 2.0 从 React Query 中借鉴了许多有价值的概念,并将它们带到了 Flutter 世界。

Riverpod 用途广泛,您可以使用它来:

  • 捕获编程错误 在编译时而 不是在运行时
  • 轻松地 获取缓存更新数据 从远程源
  • 执行 响应式缓存 并轻松更新您的 UI
  • 取决于 异步或计算状态
  • 使用最少的样板代码创建使用组合 提供程序
  • 处理它的状态 不再使用提供程序时
  • 编写 可测试的代码 并将您的逻辑保留在小部件树之外

Riverpod 为检索和缓存数据实现了定义明确的模式,因此您不必重新实现它们。

它还可以帮助您建立 良好的应用程序架构 (如果您正确使用它),因此您可以专注于以最小的摩擦构建功能。

 

Riverpod 入门很容易。

但是,如果您想充分利用它,则有一点学习曲线,我创建了本指南以涵盖所有 基本概念和 API

本指南的组织方式

为了更容易理解,我将本指南分为三个主要部分:

  1. 为什么使用 Riverpod、如何安装以及 核心概念
  2. 的概述 八种不同类型提供程序 (以及何时使用它们)
  3. 其他 Riverpod 功能(修改器、提供程序覆盖、过滤、测试支持、日志记录等)

本指南内容 广泛最新 ,除了 官方文档 外,您还可以将其用作参考。

我们将 使用简单的示例来 探索主要的 Riverpod API 和概念。

在适当的情况下,我已经包含了指向 不同 文章的链接,这些文章涵盖了不适合此处的更复杂的真实示例。

一个新的 riverpod_generator 作为 Riverpod 2.0 版本的一部分,已发布了 包。 这引入了一个新的 @riverpod注释 API,您可以使用它自动为代码中的类和方法生成提供程序(使用代码生成)。 要了解它,请阅读: 如何使用 Flutter Riverpod Generator 自动生成您的提供程序

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

为什么使用 Riverpod?

要理解为什么我们甚至需要 Riverpod,让我们看看 Provider 包的主要缺点。

按照设计,Provider 是对 InheritedWidget,因此,它 取决于小部件树

这是一个不幸的设计决策,可能导致常见的 ProviderNotFoundException:

示例:在小部件树中访问 Providers


 

另一方面,Riverpod 是 编译安全的 ,因为所有提供程序都是 全局 声明的,并且可以在任何地方访问。

这意味着您可以创建提供程序来 在小部件树之外 保存您的应用程序状态和业务逻辑。

而且由于 Riverpod 是一个 反应式 框架,它使得仅在需要时重建您的提供程序和小部件变得更加容易。

那么让我们看看如何安装和使用它。 ????

Riverpod 安装

第一步是添加最新版本的 flutter_riverpod作为对我们的依赖 pubspec.yaml文件:

dependencies:
  flutter:
    sdk: flutter  
  flutter_riverpod: ^2.0.2

注意 :如果您的应用程序已经使用 了 flutter_hooks ,您可以安装 hooks_riverpod 包。 这包括一些额外的功能,可以更轻松地将 Hooks 与 Riverpod 集成。 在本教程中,为了简单起见,我们将重点关注 flutter_riverpod

如果你想使用新的 Riverpod Generator ,你需要安装一些额外的包。 有关所有详细信息,请阅读: 如何使用 Flutter Riverpod Generator 自动生成您的提供程序

重要提示 :要更轻松地在代码中添加 Riverpod 提供程序,请安装适用于 VSCodeAndroid Studio / IntelliJ 的 Flutter Riverpod Snippets 扩展。

有关更多信息,请阅读 Riverpod.dev 上的入门页面

提供者范围

一旦安装了 Riverpod,我们就可以用一个 ProviderScope:

void main() {
  // wrap the entire app with a ProviderScope so that widgets
  // will be able to read providers
  runApp(ProviderScope(
    child: MyApp(),
  ));
}

ProviderScope是一个小部件,用于存储我们创建的所有提供者的状态。

在引擎盖下, ProviderScope创建一个 ProviderContainer实例。 大多数时候,你不需要关心 ProviderContainer或者直接使用。 要了解更多信息,请阅读 Flutter Riverpod:如何在 App 启动期间注册监听器

完成初始设置后,我们可以开始了解 Providers。

什么是 Riverpod 提供商?

Riverpod 文档 将 Provider 定义为 封装状态并允许监听该状态的对象

对于 Riverpod,提供者是一切的核心:

  • 它们完全取代了 单例服务定位器依赖注入InheritedWidgets 等设计模式。
  • 它们允许您存储一些状态并在多个位置轻松访问它。
  • 它们允许您通过过滤小部件重建或缓存昂贵的状态计算来优化性能。
  • 它们使您的代码更具可测试性,因为可以覆盖每个提供程序以在测试期间表现不同。

那么让我们看看如何使用它们。 ????

创建和读取提供程序

让我们从创建一个基本的“Hello world”提供者开始:

// provider that returns a string value
final helloWorldProvider = Provider<String>((ref) {
  return 'Hello world';
});

This is made of three things:

  1. 声明final helloWorldProvider是我们将用来读取提供者状态的全局变量
  2. 提供者Provider<String>告诉我们 哪种类型 我们正在使用 类型。 的提供者(下面会详细介绍),以及它所持有的状态
  3. 的函数 创建状态 。 这给了我们一个 ref我们可以使用该参数来读取其他提供程序、执行一些自定义处置逻辑等等。

一旦我们有了提供者,我们如何在小部件中使用它?

class HelloWorldWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Text(
      /* how to read the provider value here? */,
    );
  }
}

所有 Flutter 小部件都有一个 BuildContext我们可以用来访问 小部件树中的东西的 对象(例如 Theme.of(context)).

但是 Riverpod 提供程序 位于小部件树之外 ,要读取它们,我们需要一个额外的 ref 对象。 以下是三种不同的获取方式。 ????

1. 使用 ConsumerWidget

最简单的方法是使用 ConsumerWidget:

final helloWorldProvider = Provider<String>((_) => 'Hello world');

// 1. widget class now extends [ConsumerWidget]
class HelloWorldWidget extends ConsumerWidget {
  @override
  // 2. build method has an extra [WidgetRef] argument
  Widget build(BuildContext context, WidgetRef ref) {
    // 3. use ref.watch() to get the value of the provider
    final helloWorld = ref.watch(helloWorldProvider);
    return Text(helloWorld);
  }
}

通过子类化 ConsumerWidget代替 StatelessWidget, 我们的小部件 build方法获取一个额外的 ref 对象(类型 WidgetRef) 我们可以用来 监视 我们的提供者。

使用 ConsumerWidget是最常见的选项,也是您大部分时间应该选择的选项。

2. 使用消费者

作为替代方案,我们可以包装我们的 Text小部件 Consumer:

final helloWorldProvider = Provider<String>((_) => 'Hello world');

class HelloWorldWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    // 1. Add a Consumer
    return Consumer(
      // 2. specify the builder and obtain a WidgetRef
      builder: (_, WidgetRef ref, __) {
        // 3. use ref.watch() to get the value of the provider
        final helloWorld = ref.watch(helloWorldProvider);
        return Text(helloWorld);
      },
    );
  }
}

在这种情况下,“ref”对象是 Consumer的构建器参数,我们可以使用它来 观察 提供者的值。

这可行,但它比以前的解决方案更冗长。

那么我们什么时候应该使用 Consumer在...之上 ConsumerWidget?

这是一个例子:

final helloWorldProvider = Provider<String>((_) => 'Hello world');

class HelloWorldWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(),
      // 1. Add a Consumer
      body: Consumer(
        // 2. specify the builder and obtain a WidgetRef
        builder: (_, WidgetRef ref, __) {
          // 3. use ref.watch() to get the value of the provider
          final helloWorld = ref.watch(helloWorldProvider);
          return Text(helloWorld);
        },
      ),
    );
  }
}

,我们 只包装 在这种情况下 TextConsumer小部件,但 不是 父级 Scaffold:

Scaffold
├─ AppBar
└─ Consumer
    └─ Text

结果,只有 Text如果提供者值发生变化,将重建(更多内容见下文)。

这可能看起来像一个小细节,但如果你有一个具有复杂布局的大部件类,你可以使用 Consumer重建 于提供程序的小部件 依赖 。 虽然正如我在 上一篇文章 中所说:

创建小型且可重用的小部件有利于组合,从而使代码更简洁、性能更高且更易于推理。

如果你遵循这个原则并创建小的、可重复使用的小部件,那么你自然会使用 ConsumerWidget大多数时候。

3. 使用 ConsumerStatefulWidget & ConsumerState

ConsumerWidget是一个很好的替代品 StatelessWidget并为我们提供了一种使用最少代码访问提供者的便捷方式。

但是如果我们有一个 StatefulWidget反而?

这是相同的 hello world 示例:

final helloWorldProvider = Provider<String>((_) => 'Hello world');

// 1. extend [ConsumerStatefulWidget]
class HelloWorldWidget extends ConsumerStatefulWidget {
  @override
  ConsumerState<HelloWorldWidget> createState() => _HelloWorldWidgetState();
}

// 2. extend [ConsumerState]
class _HelloWorldWidgetState extends ConsumerState<HelloWorldWidget> {
  @override
  void initState() {
    super.initState();
    // 3. if needed, we can read the provider inside initState
    final helloWorld = ref.read(helloWorldProvider);
    print(helloWorld); // "Hello world"
  }

  @override
  Widget build(BuildContext context) {
    // 4. use ref.watch() to get the value of the provider
    final helloWorld = ref.watch(helloWorldProvider);
    return Text(helloWorld);
  }
}

通过子类化 ConsumerStatefulWidgetConsumerState,我们可以调用 ref.watch()在里面 build方法就像我们之前做的那样。

如果我们需要 在任何其他小部件生命周期方法中读取 提供者值,我们可以使用 ref.read().

当我们从 ConsumerState,我们可以访问 ref所有小部件生命周期方法中的对象。 这是因为 ConsumerState宣布 WidgetRef 作为一个属性 ,很像 Flutter State类声明 BuildContext 直接访问的属性。 作为可以在所有小部件生命周期方法中

如果你使用 hooks_riverpod 包,你也可以使用 HookConsumerWidgetStatefulHookConsumerWidget. 官方文档 更详细地介绍了这些小部件

什么是 WidgetRef?

正如我们所见,我们可以 通过 使用 ref类型的对象 WidgetRef. 这可以 作为参数使用 当我们使用时, Consumer或者 ConsumerWidget,并且 作为一个属性 当我们从子类化时 ConsumerState.

Riverpod 文档定义 WidgetRef作为 允许小部件与提供者交互的对象

请注意,两者之间有一些相似之处 BuildContextWidgetRef:

  • BuildContext让我们可以访问 小部件树中的祖先 小部件(例如 Theme.of(context)MediaQuery.of(context))
  • WidgetRef让我们访问 我们应用程序中的任何 提供程序

换句话说, WidgetRef让我们访问代码库中的任何提供程序 (只要我们导入相应的文件)。 这是 设计 使然,因为所有 Riverpod 提供商都是 全球性 的。

这很重要,因为将 应用程序 状态和逻辑保留在我们的小部件中会导致 关注点分离不佳 。 将其移入 我们的提供程序 使我们的代码更易于测试和维护。 ????

八种不同的供应商

到目前为止,我们已经学会了如何创建一个简单的 Provider并使用 ref目的。

但是 Riverpod 提供了 八种不同类型的提供程序 ,都适用于不同的用例:

  1. Provider
  2. StateProvider(遗产)
  3. StateNotifierProvider(遗产)
  4. FutureProvider
  5. StreamProvider
  6. ChangeNotifierProvider(遗产)
  7. NotifierProvider (Riverpod 2.0 中的新功能)
  8. AsyncNotifierProvider (Riverpod 2.0 中的新功能)

因此,让我们回顾一下它们并了解何时使用它们。

如果您使用新的 riverpod_generator 包,您不再需要手动声明您的提供程序(尽管我仍然建议您熟悉所有六种提供程序)。 要了解更多信息,请阅读: 如何使用 Flutter Riverpod Generator 自动生成您的提供程序

1. 提供者

我们已经了解了这一点:

// provider that returns a string value
final helloWorldProvider = Provider<String>((ref) {
  return 'Hello world';
});

Provider非常适合访问不会更改的依赖项和对象。

您可以使用它来访问存储库、记录器或其他 不包含可变状态 的类。

例如,这里有一个提供者返回一个 DateFormat:

// declare the provider
final dateFormatterProvider = Provider<DateFormat>((ref) {
  return DateFormat.MMMEd();
});

class SomeWidget extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    // retrieve the formatter
    final formatter = ref.watch(dateFormatterProvider);
    // use it
    return Text(formatter.format(DateTime.now()));
  }
}

Provider非常适合访问不会更改的依赖项,例如我们应用程序中的存储库。 有关更多信息,请阅读: Flutter 应用架构:存储库模式

更多信息在这里:

2. 状态提供者

StateProvider非常适合存储可以更改的简单状态对象,例如计数器值:

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

如果你 在里面 build方法,小部件将在状态更改时重建。

您可以通过调用在按钮回调中更新其状态 ref.read():

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

StateProvider非常适合存储 简单 的状态变量,例如 enumsstringsbooleansnumbersNotifier也可以用于相同的目的,并且更灵活。 对于更复杂或异步的状态,使用 AsyncNotifierProvider, FutureProvider, 或者 StreamProvider如下所述。

更多信息和示例在这里:

3.StateNotifierProvider

使用它来聆听和展示 StateNotifier.

StateNotifierProviderStateNotifier非常适合管理可能因事件或用户交互而改变的状态。

例如,这里有一个简单的 Clock班级:

import 'dart:async';

class Clock extends StateNotifier<DateTime> {
  // 1. initialize with current time
  Clock() : super(DateTime.now()) {
    // 2. create a timer that fires every second
    _timer = Timer.periodic(Duration(seconds: 1), (_) {
      // 3. update the state with the current time
      state = DateTime.now();
    });
  }

  late final Timer _timer;

  // 4. cancel the timer when finished
  @override
  void dispose() {
    _timer.cancel();
    super.dispose();
  }
}

此类通过调用设置初始状态 super(DateTime.now())在构造函数中,并使用周期性计时器每秒更新一次。

一旦我们有了这个,我们就可以创建一个新的提供者:

// Note: StateNotifierProvider has *two* type annotations
final clockProvider = StateNotifierProvider<Clock, DateTime>((ref) {
  return Clock();
});

然后,我们 观看 可以 clockProvider在一个里面 ConsumerWidget获取当前时间并将其显示在 Text小部件:

import 'package:intl/intl.dart';

class ClockWidget extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    // watch the StateNotifierProvider to return a DateTime (the state)
    final currentTime = ref.watch(clockProvider);
    // format the time as `hh:mm:ss`
    final timeFormatted = DateFormat.Hms().format(currentTime);
    return Text(timeFormatted);
  }
}

由于我们使用 ref.watch(clockProvider),我们的小部件将在每次状态更改时重建(每秒一次)并显示更新时间。

注意ref.watch(clockProvider)返回提供者的 状态 。 要访问底层状态通知对象,请调用 ref.read(clockProvider.notifier)反而。

有关如何以及何时使用的完整示例 StateNotifierProvider,阅读这篇文章:

更多信息在这里:

从 Riverpod 2.0 开始, StateNotifier被认为是遗留的,可以被新的取代 AsyncNotifier班级。 有关更多详细信息,请阅读: 如何将 Notifier 和 AsyncNotifier 与新的 Flutter Riverpod Generator 一起使用

请注意,使用 StateNotifierProvider如果您只需要 读取一些异步数据 ,那就大材小用了。 就是这样 FutureProvider是为了。 ????

4.未来提供者

想要从返回的 API 调用中获取结果 Future?

然后只需创建一个 FutureProvider像这样:

final weatherFutureProvider = FutureProvider.autoDispose<Weather>((ref) {
  // get repository from the provider below
  final weatherRepository = ref.watch(weatherRepositoryProvider);
  // call method that returns a Future<Weather>
  return weatherRepository.getWeather(city: 'London');
});

// example weather repository provider
final weatherRepositoryProvider = Provider<WeatherRepository>((ref) {
  return WeatherRepository(); // declared elsewhere
});

FutureProvider经常与 autoDispose修饰符。 阅读 下文 以了解更多信息。

然后你可以在 build方法并使用 模式匹配 来映射结果 AsyncValue(数据、加载、错误)到您的 UI:

Widget build(BuildContext context, WidgetRef ref) {
  // watch the FutureProvider and get an AsyncValue<Weather>
  final weatherAsync = ref.watch(weatherFutureProvider);
  // use pattern matching to map the state to the UI
  return weatherAsync.when(
    loading: () => const CircularProgressIndicator(),
    error: (err, stack) => Text('Error: $err'),
    data: (weather) => Text(weather.toString()),
  );
}

注意 :当您观看 FutureProvider<T>或者 StreamProvider<T>,返回类型是 AsyncValue<T>. AsyncValue 是用于处理 Riverpod 中的异步数据的实用程序类。 有关更多详细信息,请阅读: Flutter Riverpod 提示:使用 AsyncValue 而不是 FutureBuilder 或 StreamBuilder

FutureProvider非常强大,你可以用它来:

  • 执行和缓存异步操作(例如网络请求)
  • 处理异步操作的错误和加载状态
  • 将多个异步值组合成另一个值
  • 重新获取和刷新数据(对于拉动刷新操作很有用)

更多信息在这里:

5. 流提供者

利用 StreamProvider看一个 Stream来自实时 API 的结果并 反应性 地重建 UI。

For example, here is how to create a StreamProvider for the authStateChanges method of the FirebaseAuth class:

final authStateChangesProvider = StreamProvider.autoDispose<User?>((ref) {
  // get FirebaseAuth from the provider below
  final firebaseAuth = ref.watch(firebaseAuthProvider);
  // call a method that returns a Stream<User?>
  return firebaseAuth.authStateChanges();
});

// provider to access the FirebaseAuth instance
final firebaseAuthProvider = Provider<FirebaseAuth>((ref) {
  return FirebaseAuth.instance;
});

And here's how to use it inside a widget:

Widget build(BuildContext context, WidgetRef ref) {
  // watch the StreamProvider and get an AsyncValue<User?>
  final authStateAsync = ref.watch(authStateChangesProvider);
  // use pattern matching to map the state to the UI
  return authStateAsync.when(
    data: (user) => user != null ? HomePage() : SignInPage(),
    loading: () => const CircularProgressIndicator(),
    error: (err, stack) => Text('Error: $err'),
  );
}

StreamProvider小部件相比有很多好处 与StreamBuilder ,这些都在此处列出:

6.ChangeNotifierProvider

ChangeNotifier类是 Flutter SDK 的一部分。

我们可以使用它来存储一些状态并在它发生变化时通知监听器。

例如,这里有一个 ChangeNotifier子类以及相应的 ChangeNotifierProvider:

class AuthController extends ChangeNotifier {
  // mutable state
  User? user;
  // computed state
  bool get isSignedIn => user != null;

  Future<void> signOut() {
    // update state
    user = null;
    // and notify any listeners
    notifyListeners();
  }
}

final authControllerProvider = ChangeNotifierProvider<AuthController>((ref) {
  return AuthController();
});

这是小部件 build显示如何使用它的方法:

Widget build(BuildContext context, WidgetRef ref) {
  return ElevatedButton(
    onPressed: () => ref.read(authControllerProvider).signOut(),
    child: const Text('Logout'),
  );
}

ChangeNotifierAPI 可以轻松打破两个重要规则: 不可变状态单向数据流

因此, ChangeNotifier不鼓励,我们应该使用 StateNotifier反而。

使用不当时, ChangeNotifier导致 可变状态 并使我们的代码更难维护。 StateNotifier为我们提供了一个简单的 API 来处理 不可变状态 。 如需更深入的概述,请阅读: Flutter 状态管理:使用 Provider 从 setState 到 Freezed 和 StateNotifier

更多信息在这里:

Riverpod 2.0 中的新功能:NotifierProvider 和 AsyncNotifierProvider

Riverpod 2.0 引入了新的 NotifierAsyncNotifier 类,以及它们相应的提供程序。

我在本文中分别介绍了它们:

When to use ref.watch vs ref.read?

在上面的例子中,我们遇到了两种读取提供者的方式: ref.read and ref.watch.

获取提供者的价值 build方法,我们一直使用 ref.watch. 这确保了如果提供者值发生变化,我们会重建依赖它的小部件。

但是有些情况我们不应该使用 ref.watch.

例如,在内部 onPressed按钮的回调,我们应该使用 ref.read反而:

final counterStateProvider = StateProvider<int>((_) => 0);

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

根据经验,我们应该:

  • 称呼 ref.watch(provider)提供者 观察 的状态 build方法并 重建一个小部件 在它更改时
  • 称呼 ref.read(provider)读取 提供者的状态 一次 (这在 initState或其他生命周期方法)

但是,在上面的代码中,我们调用了 ref.read(provider.notifier)并用它来 修改 它的状态。

.notifier语法可用于 StateProviderStateNotifierProvider 只有 并且按以下方式工作:

  • 称呼 ref.read(provider.notifier)在一个 StateProvider<T>返回底层证券 StateController<T>我们可以用来修改状态
  • 称呼 ref.read(provider.notifier)在一个 StateNotifierProvider<T>返回底层证券 StateNotifier<T>所以我们可以在上面调用方法

除了使用 ref.watchref.read在我们的小部件中,我们也可以 在我们的提供者 中使用它们。 阅读下面关于 将 Providers 与 Riverpod 结合以 获取更多信息。

旁边 ref.readref.watch, 我们还有 ref.listen. ????

监听提供者状态变化

有时我们想显示一个警告对话框或 SnackBar当提供者状态改变时。

我们可以通过调用来做到这一点 ref.listen()在 - 的里面 build方法:

final counterStateProvider = StateProvider<int>((_) => 0);

class CounterWidget extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    // if we use a StateProvider<T>, the type of the previous and current 
    // values is StateController<T>
    ref.listen<StateController<int>>(counterStateProvider.state, (previous, current) {
      // note: this callback executes when the provider value changes,
      // not when the build method is called
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(content: Text('Value is ${current.state}')),
      );
    });
    // watch the provider and rebuild when the value changes
    final counter = ref.watch(counterStateProvider);
    return ElevatedButton(
      // use the value
      child: Text('Value: $counter'),
      // change the state inside a button callback
      onPressed: () => ref.read(counterStateProvider.notifier).state++,
    );
  }
}

在这种情况下,回调为我们提供了提供者的先前和当前状态,我们可以使用它来显示 SnackBar.

ref.listen()给我们一个回调, 当提供者值改变 时执行,而不是当 build方法被调用。 因此,我们可以使用它来运行任何 异步 代码(例如显示对话框),就像我们在按钮回调中所做的那样。 有关在 Flutter 小部件中运行异步代码的更多信息,请阅读我关于 Flutter 中的副作用的 文章。

此外 watch, read, 和 listen,Riverpod 2.0 引入了新方法,我们可以使用这些方法显式 刷新使 提供程序无效。 我将在另一篇文章中介绍它们。

其他 Riverpod 功能

到目前为止,我们已经涵盖了大部分核心概念和六种主要的提供程序。

接下来,让我们看看使用 Riverpod 的实际项目中经常需要的一些附加功能。

autoDispose 修饰符

如果我们正在与 FutureProvider或者 StreamProvider,当我们的提供程序不再使用时,我们将要处理所有侦听器。

我们可以通过添加一个 autoDispose 我们提供者的修饰符

final authStateChangesProvider = StreamProvider.autoDispose<User?>((ref) {
  // get FirebaseAuth from another provider
  final firebaseAuth = ref.watch(firebaseAuthProvider);
  // call method that returns a Stream<User?>
  return firebaseAuth.authStateChanges();
});

这将确保在我们离开我们正在观看提供者的页面后立即关闭流连接。

在后台,Riverpod 会跟踪连接到任何给定提供程序的所有侦听器(小部件或其他提供程序)(通过 ref.watch或者 ref.listen)。 如果我们使用 autoDispose,一旦所有侦听器被移除(即,当小部件被卸载时),提供者将被释放。


另一个用例 autoDispose是我们使用的时候 FutureProvider作为用户打开新屏幕时触发的 HTTP 请求的包装器。

如果我们想在请求完成之前用户离开屏幕时取消 HTTP 请求,我们可以使用 ref.onDispose()执行一些自定义取消逻辑:

final movieProvider = FutureProvider.autoDispose<TMDBMovieBasic>((ref) async {
  // get the repository
  final moviesRepo = ref.watch(fetchMoviesRepositoryProvider);
  // an object from package:dio that allows cancelling http requests
  final cancelToken = CancelToken();
  // when the provider is destroyed, cancel the http request
  ref.onDispose(() => cancelToken.cancel());
  // call method that returns a Future<TMDBMovieBasic>
  return moviesRepo.movie(movieId: 550, cancelToken: cancelToken);
});

超时缓存

如果需要,我们可以致电 ref.keepAlive()保留状态,以便在用户离开并重新进入同一屏幕时请求不会再次触发:

final movieProvider = FutureProvider.autoDispose<TMDBMovieBasic>((ref) async {
  // get the repository
  final moviesRepo = ref.watch(fetchMoviesRepositoryProvider);
  // an object from package:dio that allows cancelling http requests
  final cancelToken = CancelToken();
  // when the provider is destroyed, cancel the http request
  ref.onDispose(() => cancelToken.cancel());
  // if the request is successful, keep the response
  ref.keepAlive();
  // call method that returns a Future<TMDBMovieBasic>
  return moviesRepo.movie(movieId: 550, cancelToken: cancelToken);
});

keepAlive方法将告诉提供者无限期地保持其状态,使其仅在我们 刷新使其无效 时才更新。

我们甚至可以使用 KeepAliveLink实现基于超时的缓存策略以在给定持续时间后处理提供者的状态:

  // get the [KeepAliveLink]
  final link = ref.keepAlive();
  // start a 30 second timer
  final timer = Timer(const Duration(seconds: 30), () {
    // dispose on timeout
    link.close();
  });
  // make sure to cancel the timer when the provider state is disposed
  // (prevents undesired test failures)
  ref.onDispose(() => timer.cancel());

如果你想让这段代码更可重用,你可以创建一个 AutoDisposeRef扩展名(如此 所述):

extension AutoDisposeRefCache on AutoDisposeRef {
  // keeps the provider alive for [duration] since when it was first created
  // (even if all the listeners are removed before then)
  void cacheFor(Duration duration) {
    final link = keepAlive();
    final timer = Timer(duration, () => link.close());
    onDispose(() => timer.cancel());
  }
}

final myProvider = Provider.autoDispose<int>((ref) {
  // use like this:
  ref.cacheFor(const Duration(minutes: 5));
  return 42;
});

阅读 autoDispose 文档页面 了解更多详细信息。

家庭修饰符

family是一个修饰符,我们可以使用它来 将参数传递给提供者

它通过添加第二个 类型注释的附加参数来工作: 我们可以在提供程序主体中使用

final movieProvider = FutureProvider.autoDispose
    // additional movieId argument of type int
    .family<TMDBMovieBasic, int>((ref, movieId) async {
  // get the repository
  final moviesRepo = ref.watch(fetchMoviesRepositoryProvider);
  // call method that returns a Future<TMDBMovieBasic>, passing the movieId as an argument
  return moviesRepo.movie(movieId: movieId, cancelToken: cancelToken);
});

然后,我们可以在调用时将我们想要的值传递给提供者 ref.watch在里面 build方法:

final movieAsync = ref.watch(movieProvider(550));

当用户从 ListView的电影,我们推 MovieDetailsScreen这需要 movieId作为论据:

class MovieDetailsScreen extends ConsumerWidget {
  const MovieDetailsScreen({super.key, required this.movieId});
  // pass this as a property
  final int movieId;

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    // fetch the movie data for the given movieId
    final movieAsync = ref.watch(movieProvider(movieId));
    // map to the UI using pattern matching
    return movieAsync.when(
      data: (movie) => MovieWidget(movie: movie),
      loading: (_) => Center(child: CircularProgressIndicator()),
      error: (e, __) => Center(child: Text(e.toString())),
    );
  }
}

将多个参数传递给族

在某些情况下,您可能需要将 多个值 传递给家庭。

虽然 Riverpod 不 支持 这一点,但您可以传递任何实现的自定义对象 hashCode和相等运算符(例如使用 Freezed 生成的对象 使用 equatable 的对象)。

有关更多详细信息,请阅读:

为了克服这个限制,您可以使用新的 riverpod_generator 包并传递 任意数量的命名或位置参数 。 阅读本文以了解所有详细信息:

Riverpod 的依赖覆盖

有时我们想创建一个 Provider的值或对象 存储不立即可用

例如,我们只能得到一个 SharedPreferences 实例: 具有基于 Future 的 API 的

final sharedPreferences = await SharedPreferences.getInstance();

但是我们不能在 同步 Provider 中返回这个:

final sharedPreferencesProvider = Provider<SharedPreferences>((ref) {
  return SharedPreferences.getInstance();
  // The return type Future<SharedPreferences> isn't a 'SharedPreferences',
  // as required by the closure's context.
});

相反,我们必须通过抛出一个 UnimplementedError:

final sharedPreferencesProvider = Provider<SharedPreferences>((ref) {
  throw UnimplementedError();
});

当我们需要的对象可用时,我们 可以ProviderScope小部件:

// asynchronous initialization can be performed in the main method
Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  final sharedPreferences = await SharedPreferences.getInstance();
  runApp(ProviderScope(
    overrides: [
      // override the previous value with the new object
      sharedPreferencesProvider.overrideWithValue(sharedPreferences),
    ],
    child: MyApp(),
  ));
}

初始化的好处 sharedPreferences打电话之前 runApp()是我们可以观看 sharedPreferencesProvider任何地方的对象,而无需使用任何基于 Future 的 API。

这个例子使用了 ProviderScope在小部件树的根部,但我们也可以创建 嵌套 ProviderScope如果需要小部件。 更多关于这 下面

有关异步应用程序初始化的更复杂示例,请阅读本文:

将 Provider 与 Riverpod 相结合

提供者可以 依赖于 其他提供者。

例如,这里我们定义一个 SettingsRepository需要 显式的类 SharedPreferences争论:

class SettingsRepository {
  const SettingsRepository(this.sharedPreferences);
  final SharedPreferences sharedPreferences;

  // synchronous read
  bool onboardingComplete() {
    return sharedPreferences.getBool('onboardingComplete') ?? false;
  }

  // asynchronous write
  Future<void> setOnboardingComplete(bool complete) {
    return sharedPreferences.setBool('onboardingComplete', complete);
  }
}

然后我们创建一个 settingsRepositoryProvider提供 取决于sharedPreferencesProvider我们在上面创建。

final settingsRepositoryProvider = Provider<SettingsRepository>((ref) {
  // watch another provider to obtain a dependency
  final sharedPreferences = ref.watch(sharedPreferencesProvider);
  // pass it as an argument to the object we need to return
  return SettingsRepository(sharedPreferences);
});

使用 ref.watch()确保在 我们依赖 的提供者发生变化时更新提供者。 结果,任何 依赖 的小部件和提供者也将重建。

将 Ref 作为参数传递

作为替代方案,我们可以通过 Ref作为创建时的参数 SettingsRepository:

class SettingsRepository {
  const SettingsRepository(this.ref);
  final Ref ref;

  // synchronous read
  bool onboardingComplete() {
    final sharedPreferences = ref.read(sharedPreferencesProvider);
    return sharedPreferences.getBool('onboardingComplete') ?? false;
  }

  // asynchronous write
  Future<void> setOnboardingComplete(bool complete) {
    final sharedPreferences = ref.read(sharedPreferencesProvider);
    return sharedPreferences.setBool('onboardingComplete', complete);
  }
}

这样一来, sharedPreferencesProvider成为一个 隐式 依赖,我们可以通过调用来访问它 ref.read().

这使我们的 settingsRepositoryProvider声明更简单:

final settingsRepositoryProvider = Provider<SettingsRepository>((ref) {
  return SettingsRepository(ref);
});

使用 Riverpod,我们可以声明包含复杂逻辑或依赖于其他提供程序的提供程序,所有这些都 之外 在小部件树 。 与 Provider 包相比,这是一个很大的优势,并且可以更轻松地编写仅包含 UI 代码的小部件。

有关如何在复杂应用程序中组合提供程序和处理多个依赖项的真实示例,请阅读以下内容:

范围提供者

使用 Riverpod,我们可以确定 提供程序的范围 ,以便它们 针对应用程序的特定部分表现不同

这方面的一个例子是当我们有一个 ListView显示产品列表,每个项目都需要知道正确的产品 ID 或索引:

class ProductList extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return ListView.builder(
      itemBuilder: (_, index) => ProductItem(index: index),
    );
  }
}

在上面的代码中,我们将构建器的索引作为构造函数参数传递给 ProductItem小部件:

class ProductItem extends StatelessWidget {
  const ProductItem({super.key, required this.index});
  final int index;

  @override
  Widget build(BuildContext context) {
    // do something with the index
  }
}

这有效,但如果 ListView重建,它的所有孩子也将重建。


作为替代方案,我们可以覆盖 嵌套中的提供程序值 ProviderScope:

// 1. Declare a Provider
final currentProductIndex = Provider<int>((_) => throw UnimplementedError());

class ProductList extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return ListView.builder(itemBuilder: (context, index) {
      // 2. Add a parent ProviderScope
      return ProviderScope(
        overrides: [
          // 3. Add a dependency override on the index
          currentProductIndex.overrideWithValue(index),
        ],
        // 4. return a **const** ProductItem with no constructor arguments
        child: const ProductItem(),
      );
    });
  }
}

class ProductItem extends ConsumerWidget {
  const ProductItem({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    // 5. Access the index via WidgetRef
    final index = ref.watch(currentProductIndex);
    // do something with the index
  }
}

在这种情况下:

  • 我们创建一个 Provider抛出 UnimplementedError默认。
  • 我们通过添加一个父级来覆盖它的值 ProviderScopeProductItem小部件。
  • 我们 里面的索引 ProductItembuild方法。

这对性能更好,因为我们可以创建 ProductItem作为一个 const中的小部件 ListView.builder. 所以即使 ListView重建,我们的 ProductItem不会 重建。 除非其索引已更改,否则

使用“选择”过滤小部件重建

有时您有一个具有 多个属性 的模型类,并且您希望 仅在特定属性更改时 重建一个小部件。

例如,考虑这个 Connection类,以及提供程序和读取它的小部件类:

class Connection {
  Connection({this.bytesSent = 0, this.bytesReceived = 0});
  final int bytesSent;
  final int bytesReceived;
}

// Using [StateProvider] for simplicity.
// This would be a [FutureProvider] or [StreamProvider] in real-world usage.
final connectionProvider = StateProvider<Connection>((ref) {
  return Connection();
});

class BytesReceivedText extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    // rebuild when bytesSent OR bytesReceived changes
    final counter = ref.watch(connectionProvider).state;
    return Text('${counter.bytesReceived}');
  }
}

如果我们打电话 ref.watch(connectionProvider),我们的小部件将(错误地)重建时 bytesSent值变化。

相反,我们可以使用 select()只听一个特定的属性:

class BytesReceivedText extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    // only rebuild when bytesReceived changes
    final bytesReceived = ref.watch(connectionProvider.select(
      (connection) => connection.state.bytesReceived
    ));
    return Text('$bytesReceived');
  }
}

那么,每当 Connection变化,Riverpod 将比较我们返回的值 ( connection.state.bytesReceived) 并且仅在它与前一个不同时才重建小部件。

select方法适用于所有 Riverpod 提供程序,并且可以在我们调用时使用 ref.watch()或者 ref.listen(). 有关更多信息,请阅读 Riverpod 文档中的使用“选择”过滤重建

使用 Riverpod 进行测试

正如我们所见,Riverpod 提供者是 全球性 的,但 他们的状态不是 .

提供者的状态存储在一个 ProviderContainer, 隐式创建的对象 ProviderScope.

这意味着单独的小部件测试永远不会共享任何状态,因此不需要 setUptearDown方法。

例如,这是一个简单的计数器应用程序,它使用 StateProvider存储计数器值:

final counterProvider = StateProvider((ref) => 0);

void main() {
  runApp(ProviderScope(child: MyApp()));
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Consumer(builder: (_, ref, __) {
        final counter = ref.watch(counterProvider);
        return ElevatedButton(
          onPressed: () => ref.read(counterProvider.notifier).state++,
          child: Text('${counter.state}'),
        );
      }),
    );
  }
}

上面的代码使用 ElevatedButton显示计数器值并通过 onPressed打回来。

在编写小部件测试时,我们只需要这样:

await tester.pumpWidget(ProviderScope(child: MyApp()));

使用此设置,多个测试不会共享任何状态,因为每个测试都有不同的 ProviderScope:

void main() {
  testWidgets('incrementing the state updates the UI', (tester) async {
    await tester.pumpWidget(ProviderScope(child: MyApp()));

    // The default value is `0`, as declared in our provider
    expect(find.text('0'), findsOneWidget);
    expect(find.text('1'), findsNothing);

    // Increment the state and re-render
    await tester.tap(find.byType(ElevatedButton));
    await tester.pump();

    // The state have properly incremented
    expect(find.text('1'), findsOneWidget);
    expect(find.text('0'), findsNothing);
  });

  testWidgets('the counter state is not shared between tests', (tester) async {
    await tester.pumpWidget(ProviderScope(child: MyApp()));

    // The state is `0` once again, with no tearDown/setUp needed
    expect(find.text('0'), findsOneWidget);
    expect(find.text('1'), findsNothing);
  });
}

如何在测试中模拟和覆盖依赖项

许多应用程序需要调用 REST API 或与外部服务进行通信。

例如,这里有一个 MoviesRepository我们可以使用它来获取最喜欢的电影列表:

class MoviesRepository {
  Future<List<Movie>> favouriteMovies() async {
    // get data from the network or local database
  }
}

我们可以创建一个 moviesProvider获取我们需要的数据:

final moviesRepositoryProvider = Provider((ref) => MoviesRepository());

final moviesProvider = FutureProvider<List<Movie>>((ref) {
  // access the provider above
  final repository = ref.watch(moviesRepositoryProvider);
  // use it to return a Future
  return repository.favouriteMovies();
});

在编写小部件测试时,我们想要替换我们的 MoviesRepository使用返回预设响应而不是进行网络调用的模拟。

正如我们所看到的,我们可以使用 依赖覆盖 来改变提供者的行为,方法是用不同的实现替换它。

所以我们可以实现一个 MockMoviesRepository:

class MockMoviesRepository implements MoviesRepository {
  @override
  Future<List<Movie>> favouriteMovies() {
    return Future.value([
      Movie(id: 1, title: 'Rick & Morty', posterUrl: 'https://nnbd.me/1.png'),
      Movie(id: 2, title: 'Seinfeld', posterUrl: 'https://nnbd.me/2.png'),
    ]);
  }
}

在我们的小部件测试中,我们可以覆盖存储库提供程序:

void main() {
  testWidgets('Override moviesRepositoryProvider', (tester) async {
    await tester.pumpWidget(
      ProviderScope(
        overrides: [
          moviesRepositoryProvider
              .overrideWithValue(MockMoviesRepository())
        ],
        child: MoviesApp(),
      ),
    );
  });
}

结果, MoviesApp小部件将从 MockMoviesRepository测试运行时。

如果您 使用mocktail 在测试中 ,此设置也适用。 您可以存根模拟方法以返回值或抛出异常并验证它们是否被调用。

官方文档包含一个关于 使用 Riverpod 进行测试 的页面,但它不是很广泛。 要了解更多信息,您可以参加我的 Flutter Foundations 课程 ,其中包含 超过三个小时的关于单独测试的内容

使用 ProviderObserver 进行日志记录

监控状态变化在许多应用程序中都是有益的。

Riverpod 包括一个 ProviderObserver我们可以子类化的类来实现一个 Logger:

class Logger extends ProviderObserver {
  @override
  void didUpdateProvider(
    ProviderBase provider,
    Object? previousValue,
    Object? newValue,
    ProviderContainer container,
  ) {
    print('[${provider.name ?? provider.runtimeType}] value: $newValue');
  }
}

这使我们可以访问 以前 的值和 的值。

我们可以通过添加 Logger到里面的观察者名单 ProviderScope:

void main() {
  runApp(
    ProviderScope(observers: [Logger()], child: MyApp()),
  );
}

为了改善记录器的输出,我们可以为我们的提供者添加一个名称:

final counterStateProvider = StateProvider<int>((ref) {
  return 0;
}, name: 'counter');

如果需要,我们可以根据观察到的值调整记录器的输出:

class Logger extends ProviderObserver {
  @override
  void didUpdateProvider(
    ProviderBase provider,
    Object? previousValue,
    Object? newValue,
    ProviderContainer container,
  ) {
    if (newValue is StateController<int>) {
      print(
          '[${provider.name ?? provider.runtimeType}] value: ${newValue.state}');
    }
  }
}

ProviderObserver用途广泛,我们可以将记录器配置为仅记录与特定 类型提供者名称 匹配的值。 或者我们可以使用嵌套 ProviderScope并且仅记录特定小部件子树内的值。

这样,我们可以评估状态变化并监控小部件重建,而无需放置 print到处都是声明。

ProviderObserver类似于 BlocObserver小部件来自 flutter_bloc包裹。

 

 

 

关于使用 Riverpod 的应用程序架构的快速说明

在构建复杂的应用程序时, 至关重要。 选择一个可以支持您的代码库增长的良好应用程序架构

正如我之前所说:

  • 状态和逻辑保留 将应用程序 在我们的小部件中会导致 关注点分离不佳
  • 将其移入 我们的提供程序 使我们的代码更易于测试和维护。

事实证明,Riverpod 非常适合解决架构问题, 而且不会碍事

但是一个健壮的 Riverpod 应用程序架构是什么样的呢?

经过大量研究,我正式确定了一个由四层( 数据应用程序表示 )组成的架构:

使用数据、域、应用程序和表示层的 Flutter 应用架构。 箭头显示层之间的依赖关系

我在自己的应用程序中广泛使用了这种架构,并写了一系列关于它的文章。

要了解更多信息,您可以从这里开始:????

结论

Riverpod 借鉴了 Provider 的最佳功能,并增加了许多好处,使 变得更容易更安全。 在我们的应用程序中管理状态

除了我们在本指南中介绍的所有内容之外,我建议您查看 官方文档 以及官方示例应用程序:

如果你想构建大中型应用程序,我想你会喜欢我关于 Riverpod 应用程序架构的整个系列:

如果您想更深入,请查看我最新的 Flutter 课程,您将在其中学习如何使用 Riverpod 2.0 构建完整的电子商务应用程序。 ????

新的 Flutter 课程现已推出

我开设了一门全新的课程,深入介绍了 Riverpod 的状态管理,以及其他重要主题,如应用程序架构、路由、测试等等: