Flutter Riverpod 使用

背景

Flutter 中一直有一个很有争议的话题,就是有太多的状态管理框架可以用,开发者不知道如何选择,这是一个非常大的挑战。

我们现在在代码里使用的是 Provider 框架,但是它有很多限制和设计上的缺陷,这很难被移除。

Riverpod 就是一个 rearranged 的 Provider,就如字面意思,它的所有字母就是通过 Provider 重新排列的。

它不仅仅是一个重新排列的 Provider,它还是一个改进版本。

Provider 的缺陷

众所周知,Provider 依赖于 inherited widgets,但随着代码量的增加,Provider 的缺陷越来越暴露出来。

Provider 高度依赖 widget tree 和 build contexts,这本身不是大问题但是它还是带来一些麻烦。

多个 Provider 一起使用是个噩梦,有很多样板代码,把简单的事情变得复杂。

让我们看下面这段代码:

image

一个 Credentials 类,以及一个依赖它的 Authenticate 类。

我们需要使用 ProxyProvider 来实现,这里有太多的样板代码。

image

我们经常遇到运行时找不到 Provider 的问题,虽然很容易解决,但是随着代码量越来越多,变得越来越困难。

[图片上传失败...(image-82897a-1647511422989)]

Providers 会从 widget tree 上去找第一个 instance,但是如果有多个相同类型的 provider 时,就会出现预料之外的结果。

image

上面这些是 Provider 的缺陷。

Riverpod

从设计之初就是想摆脱对 Flutter 的依赖,所以它也可以被用于其他 UI 框架比如 angular_dart。

因为它把 UI 和业务逻辑剥离,所以更容易进行测试。

Riverpod 有三种依赖方式

  • riverpod
  • flutter_riverpod
  • hooks_riverpod

通常我们使用 flutter_riverpod。

ProviderScope

因为 riverpod 是不依赖 flutter 的,那么就需要一个实际的类来和 widget 及 build context 关联,这个类就是 flutter_riverpod 提供的 ProviderScope,通常我们把它包在 App 最外层,这样我们在 App 里只需要顶层这一个,当然你也可以包在任何地方。

image

Provider

然后我们就可以创建一个最简单的 Provider,这里的概念和我们之前使用的 Provider 框架完全是两个东西,就理解为致敬吧。

image

Provider<T> 会暴露一个只读的 value。

因为 greetingsProvider 是一个具体的实例,而不是一个类,这样就确保了它的 compile safe

这样我们就能有多个相同类型的实例而不出现冲突。

声明一个全局变量的方式真的好吗?当然,因为 provider 里是不保存状态的,状态保存在 Scope 里,也就是我们包在顶部的 ProviderScope。

WidgetRef

那么我们如何访问它呢?和 Provider 框架一样,我们通过一个叫 WidgetRef 的东西来访问。

context.read() -> ref.read()

context.watch() -> ref.watch()

context.listen() -> ref.listen()

Consumer

这个 ref 是哪里来的呢?通过一个叫 Consumer 的东西(这个 Consumer 和 Provider 框架里的也不是一个东西)

image

或者直接通过集成 ConsumerWidget,它是继承自 StatelessWidget 的。

image

当然也有 ConsumerStatefulWidget。

我们现在知道如何注入一个 provider,但是如果要监听改变,就需要用到这些继承自 AlwaysAliveProviderBase 的 Providers。

我们先从 StateProvider 开始。

假如我们有一个文本按钮,上面显示点击次数。

我们可以这样写:

image
image

这看上去和我们之前使用 Provider 框架时用 context 的扩展方法没什么两样,简直可以无缝迁移。

一般来讲,状态总是一个 model/class,这时我们可以和 ChangeNotifier 结合来用了。

image
image
image

StateNotifier

ChangeNotifier 里面还要 notifyListeners,等于还是有样板代码,我们更进一步,使用 StateNotifier 来做:

[图片上传失败...(image-365893-1647511422988)]

image
image

更重要的是使用 StateNotifier 是不可变的,也更容易测试。

这里有个稍微不太一样的地方:

ref.read(clicksChangeProvider.notifier).incrementClicks();

这是为了让我们能够使用它暴露出来的方法。

Ref method

接下来讲一下这几个东西的区别:

  • Read - ref.read() 表示只读。
  • Watch - ref.watch() 表示监听,同时会 rebuild。
  • Listen - ref.listen() 表示监听,但不会 rebuild,比如导航这里可能有用。
  • Select - provider.select() 表示监听部分,减少 rebuild 次数。

还有一些比较有用的 Provider。

FutureProvider

它其实就是 FutureBuilder 和 Provider 的结合。

image
image

可以看到,这里已经把 loading 和 error 状态都自动做了,非常简便。

这里的 tokenValue 是 AsyncValue<T> 类型。

StreamProvider

它的用法和 FutureProvider 差不多。

之前我们一直说 ref,实际上 ref 有两种:

  1. WidgetRef - Consumer 里的 ref。

  2. ProviderReference - Provider 创建时的 ref。

第二种的用法实际上和 Provider 框架里的 ProxyProvider 差不多。

image
image

我们用 ProviderReference 来获取其他 provider,简单直接。

Provider.family

Provider 构造还有不少方式,其中一个就是使用 family。

它通常是用来创建带参数的 provider 的。

image

这里有个 typo,应该是 revokeAuthenticationProvider

image

如果我们要创建多个值,只要后面多加泛型就可以了,也可以使用 turple 插件。

Provider.autoDispose

有些情况下,在 FutureProvider 里要做一些 dispose 清理工作。

image

这里通过 autoDispose 构造方法,配合 ref.onDispose() 来处理。

Test

任何中型到大型的应用,对应用程序的测试环节都非常关键。

要达成测试目的,我们通常需要做到以下几点:

  • test/testWidgets 里面没有任何的状态保存。
  • 能够通过 mock 或者手动操作让 provider 达到一种特定的状态。

测试组件间无状态保存

我们知道 provider 通常是定义成全局的,全局变量会让测试变得很困难。

因为我们需要一些诸如 setUp/tearDown 之类的方法。

事实上,虽然 provider 是全局的,但是 provider 的状态确不是。

实际上,状态保存在一个叫 ProviderContainer 的类中,这个类是被 ProviderScope 隐式创建的。

具体例子:

// A Counter implemented and tested using Flutter

// We declared a provider globally, and we will use it in two tests, to see
// if the state correctly resets to `0` between tests.

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

// Renders the current state and a button that allows incrementing the state
class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Consumer(builder: (context, ref, _) {
        final counter = ref.watch(counterProvider);
        return ElevatedButton(
          onPressed: () => ref.read(counterProvider.notifier).state++,
          child: Text('$counter'),
        );
      }),
    );
  }
}

void main() {
  testWidgets('update the UI when incrementing the state', (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);
  });
}

可以看到两个测试方法是完全隔离的,没有任何状态上的耦合。

覆写 provider 行为

final todoListProvider = FutureProvider((ref) async => <Todo>[]);
// ...
ProviderScope(
  overrides: [
    /// Allows overriding a FutureProvider to return a fixed value
    todoListProvider.overrideWithValue(
      AsyncValue.data([Todo(id: '42', label: 'Hello', completed: true)]),
    ),
  ],
  child: const MyApp(),
);


参考文档

Riverpod: A deep dive “on the surface”

Flutter Riverpod 全面深入解析,为什么官方推荐它?

Testing | Riverpod

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 160,108评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,699评论 1 296
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,812评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,236评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,583评论 3 288
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,739评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,957评论 2 315
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,704评论 0 204
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,447评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,643评论 2 249
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,133评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,486评论 3 256
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,151评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,108评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,889评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,782评论 2 277
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,681评论 2 272

推荐阅读更多精彩内容