侧边栏壁纸
博主头像
Gstory's Blog博主等级

每天进步一点点!

  • 累计撰写 108 篇文章
  • 累计创建 23 个标签
  • 累计收到 11 条评论

目 录CONTENT

文章目录

Flutter Riverpod 2.0:终极指南

gstory
2023-11-03 / 0 评论 / 0 点赞 / 73 阅读 / 51789 字 / 正在检测是否收录...
温馨提示:
本文最后更新于 2023-11-03,若内容或图片失效,请留言反馈。部分素材来自网络,若不小心影响到您的利益,请联系我们删除。

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

根据官方文档的描述:

Riverpod 是对Provider包的完全重写,以进行否则无法实现的改进。

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

但它不仅如此。

事实上,Riverpod 2.0 借鉴了React Query中的许多有价值的概念,并将其引入了Flutter世界。

Riverpod非常灵活,您可以使用它来:

  • 在编译时捕捉编程错误,而不是在运行时
  • 轻松获取、缓存和更新来自远程源的数据
  • 执行响应式缓存并轻松更新您的UI
  • 依赖于异步或计算状态
  • 创建、使用和组合Provider,减少样板代码
  • 在不再使用时释放Provider的状态
  • 编写可测试的代码,并将逻辑保持在小部件树之外

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

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

开始使用Riverpod很容易。

但如果要充分发挥其功能,还需要一些学习曲线,因此我创建了这个指南,以涵盖所有基本概念和API。

本指南的组织结构

为了更容易跟随,我将这个指南分为三个主要部分:

  1. 为什么使用Riverpod,如何安装它以及核心概念
  2. 八种不同类型的Provider的概述(以及何时使用它们)
  3. 附加的Riverpod功能(修饰符、Provider覆盖、过滤、测试支持、日志记录等) 这份指南内容详尽且更新,您可以将其作为参考,除了官方文档之外。

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

在适当的情况下,我还包含了链接到单独文章的链接,这些文章涵盖了这里无法涵盖的更复杂的实际示例。

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

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

为什么使用Riverpod?

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

从设计上来说,Provider是对InheritedWidget的改进,因此它依赖于小部件树。

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

2023-11-03-ubskmp.webp

访问小部件树中的Provider 然而,Riverpod是编译安全的,因为所有Provider都vider和小部件变得更容易。

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

Riverpod 安装

第一步是将最新版本的 flutter_riverpod 添加为依赖项到我们的 pubspec.yaml 文件中:


dependencies:
    flutter:
      sdk: flutter  
    flutter_riverpod: ^2.3.6

注意:如果您的应用程序已经使用flutter_hooks,您可以安装hooks_riverpod包。这个包包括一些额外功能,使得将Hooks与Riverpod集成更加容易。在本教程中,为了简化起见,我们将仅关注flutter_riverpod

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

顶部提示:为了更轻松地在您的代码中添加Riverpod提供程序,您可以安装VSCodeAndroid Studio / IntelliJ的Flutter Riverpod Snippets扩展。

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

ProviderScope

一旦安装了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 或直接使用它。有关 ProviderContainerUncontrolledProviderScope 的更多详细信息,请阅读 Flutter Riverpod: 如何在应用启动时注册监听器

完成了初始设置后,我们可以开始学习有关提供程序的内容。

什么是 Riverpod 提供程序?

Riverpod 文档 定义提供程序为封装状态片段并允许监听该状态的对象。

在 Riverpod 中,提供程序是一切的核心:

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

因此,让我们看看如何使用它们。👇

创建和读取提供程序

让我们从创建一个基本的 "Hello world" 提供程序开始:


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

这由三部分组成:

  1. 声明:final helloWorldProvider 是我们将用于读取提供程序状态的全局变量。
  2. 提供程序:Provider 告诉我们我们正在使用什么类型的提供程序(下文将详细介绍),以及它所持有的状态的类型。
  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))。

但是,RiverpodProvider存在于小部件树之外,要读取它们,我们需要一个额外的ref对象。以下是三种不同获取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 方法会获得一个额外的引用对象(类型为 WidgetRef),我们可以使用它来监视我们的提供程序。

使用 ConsumerWidget 是最常见的选择,大多数情况下,您应该选择它。

2. 使用 Consumer

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


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”的一个构建器参数之一,我们可以使用它来监视Provider的值。

这种方法是有效的,但相较于以前的解决方案更加冗长。

那么,什么情况下应该使用“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);
          },
        ),
      );
    }
  }

在这种情况下,我们只是用一个 Consumer 小部件包裹 Text,而没有包裹父级的 Scaffold


Scaffold
  ├─ AppBar
  └─ Consumer
      └─ Text

因此,只有在Provider的值发生变化时,Text 部分将重新构建(更多细节请参考下文)。

这可能看起来像一个小细节,但如果您有一个包含复杂布局的大型小部件类,您可以使用 Consumer 来仅重新构建依赖于Provider的小部件。正如我在之前的文章中所提到的:

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

如果您遵循这一原则并创建小而可重用的小部件,那么大部分时间您将自然而然地使用 ConsumerWidget

3. 使用 ConsumerStatefulWidget 和 ConsumerState

ConsumerWidgetStatelessWidget 的良好替代品,为我们提供了一种方便的方式来以最少的代码访问Provider。

但如果我们有一个 StatefulWidget 呢?

这里是相同的示例,你可以看到:


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进行子类化,我们可以在build方法中调用ref.watch(),就像我们以前所做的那样。

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

当我们从ConsumerState进行子类化时,我们可以在所有小部件生命周期方法中访问ref对象。这是因为ConsumerState声明WidgetRef作为属性,类似于Flutter的State类声明BuildContext作为可以在所有小部件生命周期方法中直接访问的属性。

如果您使用hooks_riverpod包,还可以使用HookConsumerWidgetStatefulHookConsumerWidget。官方文档在这些小部件上有更详细的介绍。

什么是WidgetRef?

正如我们所看到的,我们可以使用类型为WidgetRefref对象来监视提供程序的值。当我们使用ConsumerConsumerWidget时,这将作为参数提供,当我们从ConsumerState进行子类化时,它将作为属性提供。

Riverpod文档将WidgetRef定义为一个允许小部件与提供程序进行交互的对象。

请注意,BuildContextWidgetRef之间存在一些相似之处:

  • BuildContext允许我们访问小部件树中的祖先小部件(例如Theme.of(context)MediaQuery.of(context))。
  • WidgetRef 允许我们在应用程序中访问任何Provider。

换句话说,WidgetRef 允许我们在我们的代码库中访问任何Provider(只要我们导入相应的文件)。这是有意设计的,因为所有 Riverpod Provider都是全局的。

这很重要,因为将应用程序状态和逻辑保留在我们的小部件内会导致关注点分离不足。将其移到Provider内部使我们的代码更具可测试性和可维护性。👍

八种不同类型的Provider

到目前为止,我们已经学习了如何创建一个简单的 Provider,并使用 ref 对象在小部件内部进行观察。

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

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

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

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

1. Provider

我们已经学习了关于这个:


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

Provider 用于访问不会改变的依赖项和对象。

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

例如,这是一个返回 DateFormat 的Provider:


// 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 App Architecture: The Repository Pattern

更多信息请查看以下链接:

StateProvider

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适用于存储简单的状态变量,如枚举、字符串、布尔值和数字。Notifier也可用于相同的目的,且更加灵活。对于更复杂或异步的状态,请使用AsyncNotifierProviderFutureProviderStreamProvider,如下所述。

更多信息和示例请参考:

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();
  });

然后,我们可以在一个ConsumerWidget内观察clockProvider,以获取当前时间并将其显示在一个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 类替代。有关更多详细信息,请阅读:如何在新的 Flutter Riverpod 生成器中使用 Notifier 和 AsyncNotifier(英文)。

请注意,如果您只需要读取一些异步数据,则使用 StateNotifierProvider 可能有点过于复杂。这就是 FutureProvider 的用途。👇

4. FutureProvider

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

只需创建一个类似这样的 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(数据、加载、错误)映射到您的用户界面:


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()),
    );
  }

注意:当您观察 FutureProviderStreamProvider 时,返回类型是 AsyncValueAsyncValue 是 Riverpod 中用于处理异步数据的实用类。更多详细信息请查看:Flutter Riverpod Tip: 使用 AsyncValue 而不是 FutureBuilder 或 StreamBuilder

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

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

更多信息请查看:

5. StreamProvider

使用 StreamProvider 来监视来自实时 API 的结果流,并以响应式方式重新构建 UI。

例如,以下是如何为 FirebaseAuth 类的 authStateChanges 方法创建一个 StreamProvider


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;
  });

这是如何在小部件中使用它的方式:


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 widget 具有许多优点,这些优点在以下链接中都有详细列出:

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'),
    );
  }

ChangeNotifier API让我们违反了两个重要的规则:不可变状态和单向数据流。

因此,不鼓励使用ChangeNotifier,而应该使用StateNotifier

当不正确使用ChangeNotifier时,会导致可变状态,并使我们的代码更难维护。StateNotifier为我们提供了处理不可变状态的简单API。要了解更详细的信息,请阅读:Flutter状态管理:从setStateFreezedStateNotifierProvider

更多信息请参考以下链接:

Riverpod 2.0中的新功能:NotifierProvider和AsyncNotifierProvider

Riverpod 2.0引入了新的NotifierAsyncNotifier类,以及相应的Provider。

我在这篇文章中单独介绍了它们:

何时使用ref.watchref.read

在上面的示例中,我们遇到了两种读取Provider的方法:ref.readref.watch

build方法内获取Provider的值时,我们一直使用ref.watch。这确保了如果Provider的值发生更改,我们会重建依赖它的小部件。

但也有情况下,我们不应使用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++,
      );
    }
  }

根据经验,我们应该:

  • build方法中调用ref.watch(provider)来观察提供程序的状态,并在其发生变化时重建小部件。
  • initState或其他生命周期方法中调用ref.read(provider)来仅一次读取提供程序的状态。

然而,在上述代码中,我们调用了ref.read(provider.notifier)并用它来修改其状态。

.notifier语法仅适用于StateProviderStateNotifierProvider,其工作方式如下:

  • StateProvider上调用ref.read(provider.notifier),以返回底层的StateController,我们可以使用它来修改状态。
  • StateNotifierProvider上调用ref.read(provider.notifier),以返回底层的StateNotifier,以便我们可以调用其方法。

除了在小部件内部使用ref.watchref.read之外,我们还可以在提供程序内部使用它们。

除了ref.readref.watch之外,我们还有ref.listen。👇

监听提供程序状态变化

有时,当提供程序的状态发生变化时,我们希望显示警报对话框或SnackBar

我们可以通过在build方法中调用ref.listen()来实现这一点:


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中的副作用的文章。

除了 watchreadlisten,Riverpod 2.0 还引入了一些新方法,我们可以使用它们来明确地刷新或使供应商失效。我将在单独的文章中介绍它们。

Riverpod的附加功能

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

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

autoDispose修饰符

如果我们使用FutureProviderStreamProvider,当我们的供应商不再使用时,我们将希望处理任何监听器的释放。

我们可以通过将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();
  });

这将确保在我们离开观看Provider的页面时,流连接会被立即关闭。

在底层,Riverpod会跟踪附加到任何给定Provider的所有监听器(小部件或其他Provider)(通过ref.watchref.listen)。如果我们使用autoDispose,Provider将在所有监听器被移除时被处置(也就是在小部件被卸载时)。


另一个使用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;
  });

Riverpod帮助我们用简单的代码解决复杂的问题,特别在数据缓存方面表现出色。要充分发挥其优势,请阅读:Riverpod数据缓存和Provider生命周期:完整指南

家族修改器

family是一个修饰符,我们可以使用它来向Provider传递参数。

它通过添加第二个类型注释和一个额外的参数来工作,在Provider体内我们可以使用这个参数:


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);
  });

然后,在build方法中调用ref.watch时,我们只需将我们想要传递的值传递给Provider:


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

当用户从电影的ListView中选择一个项目,并且我们推送一个带有movieId作为参数的MovieDetailsScreen时,我们可以在这种情况下使用这段代码。


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 来存储当前不可立即获取的值或对象。

例如,我们只能使用基于 Future 的 API 获取 SharedPreferences 实例:


final sharedPreferences = await SharedPreferences.getInstance();

但我们不能在同步提供程序内部返回这个。


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(),
    ));
  }

在调用runApp()之前初始化sharedPreferences的优点是,我们可以在任何地方观察sharedPreferencesProvider对象,而无需使用基于Future的API。

此示例在小部件树的根部使用了ProviderScope,但如果需要,我们也可以创建嵌套的ProviderScope小部件。更多信息请参阅#scoping-providers。

要了解更复杂的异步应用程序初始化示例,请阅读以下文章:

将Providers与Riverpod结合使用

Providers可以依赖于其他Providers。

例如,这里我们定义了一个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 的Provider,它依赖于我们上面创建的 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作为参数传递

作为一种替代方法,我们可以在创建 SettingsRepository 时将 Ref 作为参数传递:


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的Provider,而这些Provider都在小部件树之外。这是与Provider包相比的一个巨大优势,使得编写仅包含UI代码的小部件变得更加容易。

要了解如何在复杂应用程序中结合Provider并处理多个依赖关系的实际示例,请阅读以下内容:

作用域Provider

使用Riverpod,我们可以对Provider进行作用域设置,以便它们在应用程序的特定部分表现出不同行为。

一个示例是,当我们有一个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内部覆盖Provider的值:


// 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
    }
  }

在这种情况下:

  • 我们创建一个默认抛出UnimplementedErrorProvider
  • 通过将父ProviderScope添加到ProductItem小部件来覆盖其值。
  • 我们在ProductItembuild方法中监视索引。

这对性能更有益,因为我们可以将ProductItem作为const小部件创建在ListView.builder中。因此,即使ListView重新构建,除非其索引发生更改,否则我们的ProductItem将不会重新构建。

使用 "select" 过滤小部件的重新构建

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

例如,考虑这个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文档中的"使用“select”来筛选重建"

使用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进行测试的更多信息

上面的示例相当基本。如果您想要认真进行测试,您将需要一些更多的资源。👇

官方的 Riverpod 测试指南 包括一些更详细的信息(包括如何使用 ProviderContainer 编写单元测试),但不太全面。

如果您有一些带有依赖关系的自定义通知器并希望对它们进行单元测试,请阅读以下内容:

并且,如果您想要了解有关使用Riverpod(以及一般的Flutter)进行测试的更全面指南,您可以参加我的最新课程,其中包括超过三个小时关于测试的内容:

使用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()),
    );
  }

为了提升日志记录的输出,我们可以给我们的Provider(providers)添加一个名称:


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 是多功能的,我们可以配置我们的日志记录器,只记录与特定类型或Provider名称匹配的值。或者,我们可以使用嵌套的 ProviderScope,只记录特定小部件子树中的值。

通过这种方式,我们可以评估状态变化和监视小部件重建,而不必在各个地方放置 print 语句。

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

Riverpod应用架构的快速说明

在构建复杂的应用程序时,选择一个能够支持代码库随着增长而增加的良好应用程序架构至关重要。

正如我之前所说:

  • 将应用程序状态和逻辑保持在小部件内会导致关注点分离不佳。
  • 将其移至Provider内会使我们的代码更具可测试性和可维护性。

事实证明,Riverpod非常适合解决架构问题,而不会妨碍开发过程。

那么,一个强大的Riverpod应用架构是什么样的呢?

经过大量研究,我制定了一个由四个层次(数据、领域、应用、展示)组成的架构:

2023-11-03-irrlvdfq.png
使用数据、领域、应用和演示层的Flutter应用架构。箭头显示了层之间的依赖关系。我在自己的应用中广泛使用这种架构,并撰写了一整套相关文章。

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

结论

Riverpod借鉴了Provider的最佳特点,并添加了许多优点,使在我们的应用中更容易且更安全地管理状态。

除了本指南中涵盖的所有内容外,我建议查看官方文档以及官方示例应用:

转自Andrea Bizzotto

0

评论区