延后2Flutter

Flutter Riverpod 使用

2022-03-21  本文已影响0人  不思进取的码农

背景

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 有三种依赖方式

通常我们使用 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

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

还有一些比较有用的 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

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

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

测试组件间无状态保存

我们知道 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

上一篇 下一篇

猜你喜欢

热点阅读