likes
comments
collection
share

跟🤡杰哥一起学Flutter (十五、玩转状态管理之——Riverpod使用详解)

作者站长头像
站长
· 阅读数 4

😑 搜了圈 Riverpod详解 的文章,大多 浅尝辄止,写个Hello Riverpod就完了,写得好些的又 年代久远,基于 0.x、1.x 进行的讲解,最新版都更新到 2.5.1 了...

跟🤡杰哥一起学Flutter (十五、玩转状态管理之——Riverpod使用详解)

🙃 然后《官方文档》写得是 一言难尽,以致于我花了好些时间也无法 一窥Riverpod的全貌。查阅了大量文章 + 硬撸官方文档 + 自己实践 + 阅读源码,按照自己觉得比较合适的学习曲线,输出成一篇 Riverpod用法详解 的文章。希望能帮到在做 Flutter状态管理框架选型 的铁子,能够快速参透 Riverpod的使用方法

1. Provider vs Riverpod

这两个库的作者都是 Remi Rousselet,新库命名其实就是旧库的 字母重排,估计是想表达 Provider重构升级版 的寓意😂。之所以要搞这个库,是因为 Provider 存在一些 局限,主要有这几点:

① 依赖BuildContext

Provider 是基于 InheritedWidget 封装,读取状态需要 BuildContxt,所以 只能在Widget树中声明使用。而在有些场景不不一定能直接拿到 BuildContext,如在 非UI层 (如业务逻辑层) 访问状态,只能通过某种方式传递 BuildContext实例,繁琐之余还增加了代码的耦合度。使用不当,还可能导致 ProviderNotFoundException

跟🤡杰哥一起学Flutter (十五、玩转状态管理之——Riverpod使用详解)

多个相同类型的Provider需要自己维护一个Key进行区分

如:Widget树的同一层级,为相同类型的状态创建多个同类型的Provider,子Widget无法确定使用哪个Provider的数据,需要指定一个特定的Key来进行区分。😳 这种手动维护的东西,多了就容易乱...

void main() {
  runApp(
    MultiProvider(
      providers: [
        ChangeNotifierProvider(create: (_) => Counter(1), key: ValueKey(1)),
        ChangeNotifierProvider(create: (_) => Counter(2), key: ValueKey(2)),
      ],
      child: MyApp(),
    ),
  );
}

// 调用出,通过key指定使用哪个Counter实例
Provider.of<Counter>(context, listen: false, key: ValueKey(1)).increment();

③ 如果需要跨Widget共享状态,Provider就没法弄成局部私有的,只能是全局可访问的。

比如:同一层级的Widget → A、B、C、D,状态虽然只有A和C用到,但是为了共享,需要在 更高层级 注册这个状态,然后B和D也能访问到这个没用到的状态。另外,如果是涉及到跨多个层级共享状态的修改,复杂的多层嵌套,可能会改得你想骂人🤬

RiverpodProvider 的基础上进行重构,解决上述问题之余,提供了 更灵活/精细的状态管理机制状态不可变编译时类型安全易于测试 等特性,更清晰的代码组织和维护方式 (注解代码生成),可以帮助我们有效地 组织和管理大规模的状态

😄 当然,不是说 Provider库 就一无是处,它的优点是 简单易用上手难度低,适用于应用规模较小,状态管理不太复杂的场景。适合就好 ❗️ 接着说下 Riverpod库 的基本使用~

2. 基本使用

2.1. 依赖添加

😮 官方文档 上来就让你唰唰唰在 终端 使用下述命令安装依赖包:

flutter pub add flutter_riverpod
flutter pub add riverpod_annotation
flutter pub add dev:riverpod_generator
flutter pub add dev:build_runner
flutter pub add dev:custom_lint
flutter pub add dev:riverpod_lint

或者在 pubspec.yaml 中添加依赖,然后执行 flutter pub get 安装依赖包:

name: my_app_name
environment:
  sdk: ">=3.0.0 <4.0.0"
  flutter: ">=3.0.0"

dependencies:
  flutter:
    sdk: flutter
  # Riverpod核心库
  flutter_riverpod: ^2.5.1
  # Riverpod注解
  riverpod_annotation: ^2.3.5

dev_dependencies:
  # Dart代码生成文件
  build_runner:
  # 为Dart和Flutter项目自定义lint规则,Lint有助于捕获潜在错误,并强制执行一致的编程风格
  custom_lint:
  # Riverpod代码生成器
  riverpod_generator: ^2.4.0
  # 专为Riverpod设计的一套lint规则,有助于再使用Riverpod时执行最佳实践
  riverpod_lint: ^2.3.10

跟🤡杰哥一起学Flutter (十五、玩转状态管理之——Riverpod使用详解)

啊?不是,这些库真的都是必须的吗

答:如果不需要 不需要注解代码生成和Lint,只添加一个 flutter_riverpod 就能正常使用 Riverpod了。然后,如果你项目有在用 flutter_hooks (支持React状态管理玩法的库,在不创建 StatefulWidget 和 State 的情况下,直接在函数组件中声明和管理状态),可以添加 hooks_riverpod 依赖,其中包含一些额外功能来使得Hooks与Riverpod的集成更加容易。⚠️ 本节不讨论 hooks_riverpod 相关内容~

😳 官方 推荐使用注解代码生成,更好的可读性和灵活性,如:方便的参数传递,生成代码时 Riverpod会自动选择最合适的Provider类型。

2.2. 补全插件-Flutter Riverpod Snippets

😄 然后,为了简化 Riverpod 的代码编写,官方建议安装一个 Flutter Riverpod Snippets 的IDE插件:

跟🤡杰哥一起学Flutter (十五、玩转状态管理之——Riverpod使用详解)

对,就 补全,类似于 Live Template 那一套,输入 触发补全的字母组合,选中后回车补全:

跟🤡杰哥一起学Flutter (十五、玩转状态管理之——Riverpod使用详解)

触发字母组合 & 对应生成的代码 可到 《Flutter Riverpod Snippets 插件主页》自行查询,😏 杰哥有 Github Copilot 加持,表示不太需要介个,嘿嘿~

2.3. 简单代码示例

😄 前面说了,可以只添加一个 flutter_riverpod 依赖就可以使用Riverpod来管理状态,写个简单的计数器例子来验证,顺便熟悉Riverpod库的使用方法:

  • 主页面:显示当前计数的 Text + 点击跳转计数页的Button。
  • 计数页:显示当前计数的 Text + 点击计数+1的Button。
  • 测试流程:点击主页面按钮跳转计数页,点几下计数Button,关闭页面,看主页面计数是否刷新。

具体实现代码如下

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

// ① 创建一个状态提供者,StateProvider会观察一个值,并再改变时得到通知
final clickCountProvider = StateProvider<int>((ref) => 0);

void main() {
  // ② 想使用Riverpod 的 Provider 必须用 ProviderScope 包裹MyApp!
  runApp(const ProviderScope(child: MyApp()));
}

// ③ 继承ConsumerWidget,它是可以提供监听Provider的Widget
class MyApp extends ConsumerWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    // ④ 通过ref.watch() 来监听Provider的值,当Provider的值改变时,会自动刷新UI
    final int count = ref.watch(clickCountProvider);
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(title: const Text('Riverpod Demo')),
        body: Center(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: <Widget>[
              Text('点击计数:$count'),
              Builder(
                builder: (context) => ElevatedButton(
                  onPressed: () {
                    Navigator.push(
                      context,
                      MaterialPageRoute(builder: (context) => const CountPage()),
                    );
                  },
                  child: const Text('跳转到增加计数页面'),
                ),
              ),
            ],
          )),
      ),
    );
  }
}

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

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final int count = ref.watch(clickCountProvider);

    return Scaffold(
      appBar: AppBar(
        title: const Text('增加计数'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text('点击计数:$count'),
            Builder(
              builder: (context) => ElevatedButton(
                onPressed: () {
                  // ⑤ 获取Provider的通知器修改状态值(自增)
                  ref.read(clickCountProvider.notifier).state++;
                },
                child: const Text('点击计数+1'),
              ),
            ),
          ],
        ))
    );
  }
}

运行结果如下

跟🤡杰哥一起学Flutter (十五、玩转状态管理之——Riverpod使用详解)

😄 阔以正常使用,接着归纳下Riverpod的基本使用流程:

  • ① 创建一个 全局finalProvider实例 来存储 状态/数据,传入一个 初始化状态的方法
  • ② 使用 ProviderScope 包裹 MyApp 实例。
  • ③ 需要用到状态的 Widget 继承 ConsumerWidget,它的 build() 会提供一个 WidgetRef 类型的参数。
  • ④ 需要 读取状态值,调用 ref.watch(xxxProvider) 来获取,状态值改变,会触发UI更新。
  • ⑤ 需要 修改状态值, 调用 ref.read(xxxProvider.notifier).state = xxx。

😁 还算简单,接着添加 注解相关的依赖,试下 代码生成 的玩法,先注释掉定义clickCountProvider变量的那一行,添加下述代码:

@Riverpod(keepAlive: true)
int clickCount(ClickCountRef ref) => 0;

然后打开 终端,可以执行 flutter pub run build_runner build 生成对应的Provider代码,也可以执行 flutter pub run build_runner watch 监听相关文件改动触发代码文件的重新生成。😣 然后代码报错了:

跟🤡杰哥一起学Flutter (十五、玩转状态管理之——Riverpod使用详解)

提示找不到这个 notifier,点进去看下这个生成的 clickCountProvider 实例:

跟🤡杰哥一起学Flutter (十五、玩转状态管理之——Riverpod使用详解)

😮 咦,生成的代码用的是 Provider 类,上面没用注解的写法,用的是 StateProvider。在解决报错前,先来过下 Riverpod 中都有哪几种 Provider 吧 ❗️

2.4. 各种 Provider

Provider (状态提供者)Riverpod状态管理的核心,负责创建和存储管理状态,通知UI组建状态更新等功能,Riverpod 提供了下述这些不同类型的 Provider,以满足不同的需求:

  • Provider:只存储 不可变 的值或对象,最简单的状态提供者,只对外提供访问状态值的接口,外部无法对状态值进行修改。
  • FutureProvider:处理 异步操作,如:从网络请求数据数据,它会再Future完成时通知其观察者。通常与 autoDispose 修饰符一起使用。
  • StreamProvider:处理 基于流的异步数据,监听一个Stream,并在新数据到达前通知其观察者。

😄 用 FutureProviderStreamProvider 写个简单的异步加载网络数据的简单例子:

import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

final articleFutureProvider = FutureProvider.autoDispose(
    (ref) async => await Dio().get('https://www.wanandroid.com/article/list/0/json').then((res) => res.data));

final articleStreamProvider = StreamProvider.autoDispose((ref) async* {
  final response = await Dio().get('https://www.wanandroid.com/article/list/0/json');
  yield response.data;
});

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

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(title: const Text('Riverpod Demo')),
        body: const Row(
          children: [
            Expanded(child: FutureProviderExample()),
            Expanded(child: StreamProviderExample()),
          ],
        ),
      ),
    );
  }
}

class FutureProviderExample extends ConsumerWidget {
  const FutureProviderExample({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final responseAsyncValue = ref.watch(articleFutureProvider);

    return Center(
        child: SingleChildScrollView(
            child: responseAsyncValue.when(
      data: (data) => Text('Data: $data'),
      loading: () => const CircularProgressIndicator(),
      error: (err, stack) => Text('Error: $err'),
    )));
  }
}

class StreamProviderExample extends ConsumerWidget {
  const StreamProviderExample({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final responseStream = ref.watch(articleStreamProvider);
    return Center(
        child: SingleChildScrollView(
            child: responseStream.when(
      data: (data) => Text('Data: $data'),
      loading: () => const CircularProgressIndicator(),
      error: (err, stack) => Text('Error: $err'),
    )));
  }
}

运行结果如下,

跟🤡杰哥一起学Flutter (十五、玩转状态管理之——Riverpod使用详解)

上面监听 FutureProviderStreamProvider 时,返回类型是 AsyncValue,用于表示 异步操作的不同状态 (加载中、已完成、操作失败),可以使用 when关键字 来处理不同的状态。

😊 继续过完剩下的Provider:

❗️ Riverpod 2.0新增

  • NotifierProvider:提供一种更灵活的方式来管理状态和业务逻辑,支持任何类型的 "Notifier"
  • AsyncNotifierProvider:专门用于管理异步操作的状态,如网络请求,它提供了一个结构化的方法来处理异步数据的加载、成功、错误和状态更新。

已过时

  • StateProvider:创建和提供一个简单的可变状态,允许监听状态变化并响应这些变化。Riverpod 2.0 中推荐使用 NotifierProvider 来代替它。
  • StateNotifierProvider :将 StateNotifier 类与 Riverpod 集成,管理复杂的状态逻辑,并通知UI更新。Riverpod 2.0 中推荐使用 NotifierProvider 来代替它。
  • ChangeNotifierProvider:将 ChangeNotifier 类与 Riverpod 集成,管理可观察的状态对象,ChangeNotifier 中需要自己调用 notifyListeners() 通知变更。

😏 吼吼,试下用2.0新增的 NotifierProvider 来替换前面的 StateProvider,解决报错的问题:

class ClickCount extends Notifier<int> {
  // 重写此方法返回Notifier的初始状态
  @override
  int build() => 0;

  void increment() {
    // state 表示当前状态
    state++;
  }
}

final clickCountProvider = NotifierProvider<ClickCount, int>(() => ClickCount());

// 之前的 ref.read(clickCountProvider.notifier).state++;
ref.read(clickCountProvider.notifier).increment();

😄 正常运行,但没法像之前那样直接 notifier).state++ ,看下NotifierProvider 的定义:

typedef NotifierProvider<NotifierT extends Notifier, T>

呕吼,类型别名,接受两个 泛型参数,前者是 Notifier 的字类,定义了 状态和如何修改状态的逻辑,后者则是 管理的状态类型NotifierProvider 实例会创建一个 NotifierT 类型的对象,并监听其状态变化,当状态变化时,所有依赖此提供者的部分都将重新构建。

Notifierstate 属性是 protected 的,只能在 Notifier类或其子类中被访问和修改。这样确保了状态的一致性和可预测性,防止在Notifier之外的地方意外修改状态。这就是上面修改状态,不直接获取state自增,而是老老实实在Notifier里写状态改变方法的原因 ❗️

😏 每次使用 NotifierProvider 都得先创建Notifier类,然后创建NotifierProvider来包裹他,有点麻烦了啊,其实可以使用 @riverpod 注解来自动生成:

import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'main.g.dart';

@riverpod
class ClickCount extends _$ClickCount {
  @override
  int build() => 0;

  void increment() {
    state++;
  }
}

执行 flutter pub run build_runner build 生成代码,点开这个 _$ClickCount 类:

跟🤡杰哥一起学Flutter (十五、玩转状态管理之——Riverpod使用详解)

呕吼,ClickCount 其实是继承了 AutoDisposeNotifier 类,相比 Notifier 多了一个特性,当没有任何监听器监听它时 (ref.watch/ref.listen),它会自动被清理,这样有助于避免内存泄露。😁 如果想让 ClickCount 继承 Notifier,只需改下注解: @Riverpod(keepAlive: true) ,注意此处 首字母是大写的 ❗️ 看下生成后的代码:

跟🤡杰哥一起学Flutter (十五、玩转状态管理之——Riverpod使用详解)

😄 看到这里,读者估计会好奇: @riverpod@Riverpod 两个注解有什关系呢?看下源码就知道了~

跟🤡杰哥一起学Flutter (十五、玩转状态管理之——Riverpod使用详解)

@riverpod 就是 Riverpod构造方法的简化调用 而已,keepAlive 默认为 false,即 autoDispose

Tips:使用 注解生成不同类型 Provider的用法示例,可自行查阅《About code generation》

2.5. WidgetRef

说完 Provider,接着说下 WidgetRef,它提供了一些方法,用于监听和读取Provider的状态:

  • watch() :监听Provider,当状态改变时,使用 watch() 的 Widget 会自动重建。
  • read()只读取Provider的当前状态,状态改变,Widget不会重建。
  • listen() :通常用于在 build() 中监听Provider,当状态改变时,会调用设置的监听器,监听器会在idget重建时自动移除。
  • listenManual() :通常在 State.initState() 或其它生命周期中监听Provider,此方法返回一个 ProviderSubscription 对象,可以使用它来停止监听close(),或者读取Provider的当前状态。
  • refresh() :立即使Provider的当前状态无效,重新计算并返回新值,常用于触发异步Proivder的重新获取数据,如:下拉刷新、错误重试 等场景。
  • invalidate() :使Provider的当前状态无效,然后在下一次读取provider或者下一帧时,Provider会被重新计算。refresh() 是同步的,它是 异步 的,没有返回值。
  • exists() :判断 Provider 是否已经初始化。

然后是 获取WidgetRef的方式,除了上面继承 ConsumerWidget,直接通过它的 build() 获取外,还可以:

  • 继承 ConsumerStatefulWidget,通过它的 State.build() 获取。
  • 使用 Consumer/ConsumerBuilder 包裹需要使用 refWidget,在 builder() 中获取。

简单示例如下:

class Example extends ConsumerStatefulWidget {
  @override
  _ExampleState createState() => _ExampleState();
}

class _ExampleState extends ConsumerState<Example> {
  @override
  Widget build(BuildContext context) {
    // 在这里你可以使用 ref
    final value = ref.watch(someProvider);
    return Text(value);
  }
}

class Example extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Consumer(
      builder: (context, ref, child) {
        final AsyncValue<Example> example = ref.watch(someProvider);
        return Center(
          child: switch(example) {
             AsyncData(:final value) => Text('Example: ${value.example}'),
             AsyncError() => const Text('Oops, something unexpected happened'),
             AsyncLoading() => const CircularProgressIndicator(),
          }
        )
        // 在这里你可以使用 ref
        final value = ref.watch(someProvider);
        return Text(value);
      },
    );
    }
}

对应关系ConsumerWidgetStatelessWidgetConsumerStatefulWidgetStatefulWidget,前者更适合在 Widget.build() 中使用,后者可以在 Widget的生命周期 中使用Provider,如 initState() 或 dispose() 中。

🤷‍♂️ 基本使用就讲到这,基本可以畅通玩耍Riverpod了,接着对官方文档提到的一些细节点进行解读~

3. 进阶使用

3.1. 使用Provider发起你的第一个请求

官方文档 如是说:

网络请求通常属于 业务逻辑,在 Riverpod 中,业务逻辑被放置在 "providers" 中,Provider 是一个强大的函数,具有:缓存默认错误/加载处理可监听某些数据变化时自动重新执行 等特性。这使得 Provider 很适合拿来处理 Get 请求。

😄 稍微小改下官方给出的代码实例:

跟🤡杰哥一起学Flutter (十五、玩转状态管理之——Riverpod使用详解)

跟🤡杰哥一起学Flutter (十五、玩转状态管理之——Riverpod使用详解)

跟🤡杰哥一起学Flutter (十五、玩转状态管理之——Riverpod使用详解)

运行输出结果

跟🤡杰哥一起学Flutter (十五、玩转状态管理之——Riverpod使用详解)

🤷‍♂️ 当然,也可以使用文档里的风骚写法,用 switch 来代替 when

跟🤡杰哥一起学Flutter (十五、玩转状态管理之——Riverpod使用详解)

然后是可以用 ConsumerWidget 来替代 Consumer 来减少代码缩进:

跟🤡杰哥一起学Flutter (十五、玩转状态管理之——Riverpod使用详解)

再然后是用 ConsumerStatefulWidget 来重写:

跟🤡杰哥一起学Flutter (十五、玩转状态管理之——Riverpod使用详解)

3.2. 执行副作用 (Side Effects)

没有太多前端开发经验的我,一开始看到 副作用 这个词是一脸懵逼的 🤡,后来发现指的是:

函数或方法在执行时,除了返回值之外,对外部产生的任何影响。如:修改全局变量、发起网络请求、写入文件、改变程序状态等。

举两个例子 🌰

// 修改全局变量
// 函数不仅返回计算结果,还修改了一个全局计数器,这个行为就是副作用,它影响了程序中其它部分的状态
counter = 0  # 全局变量

def increment_and_return():
  global counter
  counter += 1
  return counter

print(increment_and_return())  # 输出 1,同时修改了全局变量 counter

// 发送网络请求
// 点击按钮发送请求以添加一个Todo项,发送请求这个动作就是一个副作用
// 因为它改变了应用的状态 (可能从服务器获取了新数据)
ElevatedButton(
  onPressed: () {
    // 请求以添加一个Todo项
  },
  child: Text('Add Todo'),
)

副作用可能使程序的逻辑变得复杂和难以预测,因此在设计程序时,管理好副作用 非常重要,在 函数式编程中,推崇没有副作用的函数 (纯函数),这样的函数 仅通过输入值来确定输出值,不会影响外部状态,这有助于提高代码的可预测性和可维护性。但在Flutter中,副作用通常是 与用户交互和数据管理的一部分Riverpod 就是用来帮助开发者以一种 更可控和可预测的 的方式来处理这些副作用的。

🤔 Riverpod 中提供了一种 特殊的Provider 来封装执行副作用的逻辑,并通过其方法触发这些副作用 → Notifier,😄 是的,就是我们前面写 NotifierProvider例子 提到的那个 Notifier。然后文档写了一个添加Todo的例子,这里稍微调整一下。

跟🤡杰哥一起学Flutter (十五、玩转状态管理之——Riverpod使用详解)

看下生成的代码:

跟🤡杰哥一起学Flutter (十五、玩转状态管理之——Riverpod使用详解)

接着添加一个 addTodo() 的方法,向后台提交一条Todo记录,添加完,客户端得 刷新状态(更新本地缓存) ,比如为 待办列表变量 添加一个Todo。然后是几种不同的情况:

提交时后端返回新的资源状态

跟🤡杰哥一起学Flutter (十五、玩转状态管理之——Riverpod使用详解)

后端没返回新的状态资源,需要自己重新执行Get请求拉取

跟🤡杰哥一起学Flutter (十五、玩转状态管理之——Riverpod使用详解)

手动更新本地缓存

跟🤡杰哥一起学Flutter (十五、玩转状态管理之——Riverpod使用详解)

3.3. 将参数传递给请求

HTTP请求,通常依赖于 外部参数,现在请求是放在Provider里的,该如何传参呢?

跟🤡杰哥一起学Flutter (十五、玩转状态管理之——Riverpod使用详解)

监听处:

跟🤡杰哥一起学Flutter (十五、玩转状态管理之——Riverpod使用详解)

运行输出结果

跟🤡杰哥一起学Flutter (十五、玩转状态管理之——Riverpod使用详解)

注意事项

❗️ 如果两个Widget使用 相同的Provider+ 相同的参数,那只会发起一个请求,否则会发起两个请求。Riverpod依赖于参数的 ==运算符,如果直接实例化一个新对象作为Provider的参数,该对象没有重写==运算符的话,Riverpod会认为参数不同,从而尝试发起新的网络请求。如想传递一个list:ref.watch(activityProvider( ['recreational', 'cooking'] )); 应该添加一个 const 修饰符 → const ['recreational', 'cooking'] ,或者重写List的==运算符。为了帮助发现此类错误,建议使用 riverpod_lint 并启用 provider_parameters lint 规则来帮助发现和避免上述错误。

另外,如果不是使用注解生成代码,可以通过 family() 来添加参数,代码示例如下:

final messagesFamily = FutureProvider.family<Message, String>((ref, id) async {
  return dio.get('http://my_api.dev/messages/$id');
});

3.4. WebSocket 与 同步执行

Future 是构建 Riverpod 应用的核心方式,但它也支持其它格式,如 同步对象Stream。同步对象示例:

// 函数返回类型没用用 Future 包裹
@riverpod
int synchronousExample(SynchronousExampleRef ref) {
  return 0;
}

Consumer(
  builder: (context, ref, child) {
    // 因为是同步的,所以值不需要用AsyncValue来包裹
    int value = ref.watch(synchronousExampleProvider);
    return Text('$value');
  },
);

不支持 ChangeNotifierStateNotifier 等可监听对象,如果需要与这些对象交互的话,可以将其 通知机制 从管道传递到 Riverpod。代码示例:

@riverpod
ValueNotifier<int> myListenable(MyListenableRef ref) {
  final notifier = ValueNotifier(0);

  // 添加清理回调,当Provider被销毁时,它会调用ValueNotifier#dispose() 来释放资源
  ref.onDispose(notifier.dispose);

  // 添加监听器,当 ValueNotifier 值发生改变时,通知依赖于Provider的所有Widget
  notifier.addListener(ref.notifyListeners);

  // 返回ValueNotifier对象,以便在应用的其它部分中使用
  return notifier;
}

如果需要频繁编写这样的逻辑,可以写个 Ref扩展,将处理可监听对象的逻辑提取出来,方便复用:

extension on Ref {
  T disposeAndListenChangeNotifier<T extends ChangeNotifier>(T notifier) {
    onDispose(notifier.dispose);
    notifier.addListener(notifyListeners);
    return notifier;
  }
}

// 调用代码示例
@riverpod
ValueNotifier<int> myListenable(MyListenableRef ref) {
  return ref.disposeAndListenChangeNotifier(ValueNotifier(0));
}

@riverpod
ValueNotifier<int> anotherListenable(AnotherListenableRef ref) {
  return ref.disposeAndListenChangeNotifier(ValueNotifier(42));
}

关于 Stream 的监听用法,前面讲过了,就不再赘述了~

3.5. 请求合并

实际开发中,可能存在 需要基于一个请求的结果来触发另一个请求 的场景,一种解法是将一个Provider的结果作为参数传递给另一个Provider ,可以,但用起来比较麻烦。为了改善这一点,Riverpod提供了另一种解法,将Ref参数 传递给Provider。代码示例 (先获取用户位置,然后使用此位置来获取附近的餐馆):

跟🤡杰哥一起学Flutter (十五、玩转状态管理之——Riverpod使用详解)

3.6. 状态销毁

  • @Riverpod 注解设置 keepAlive: true,可以防止Provider没有监听者时状态被销毁。
  • 可以调用 ref.onDispose() 设置一个监听器,以便在状态被销毁时执行一些逻辑,如:关闭 StreamController。
  • 除此之外,还有 ref.onCancel (当Provider最后一个监听者被移除时调用) 和 ref.onResume ( 在onCancel()被调用后添加了新的监听者时调用) 。
  • 使用 ref.invalidate() 可以强制销毁Provider,如果Provider正在被监听,会创建一个新状态,如果没有,Provider 将被完全销毁。
  • 更细粒度的控制销毁:通过 ref.keepAlive() ,可以在自动销毁被启用的情况下,更细致地控制状态的销毁行为。如:在请求成功后保持状态,请求失败时不缓存。
  • 让状态活跃一段时间:Riverpod没有内置方法来实现,可以通过 Timer + ref.KeepAlive() 来实现,文档中定义了这样一个扩展:
extension CacheForExtension on AutoDisposeRef<Object?> {
  void cacheFor(Duration duration) {
    // 调用keepAlive()创建一个链接,只要链接保持打开状态,就会阻止对象被自动清理
    final link = keepAlive();
    // 定时器,指定时间段过去后,对象将不再保持活动状态
    final timer = Timer(duration, link.close);
    // 对象被处理时取消定时器,避免内存泄露
    onDispose(timer.cancel);
  }
}

// 使用示例
@riverpod
Future<Object> example(ExampleRef ref) async {
  /// 让状态存在5分钟
  ref.cacheFor(const Duration(minutes: 5));
  return http.get(Uri.https('example.com'));
}

3.7. Provider 即时初始化

Provider 默认是 懒加载 的,在首次使用时才初始化,如果需要实现 即时初始化,可以在 ProviderScope 下放置一个 ConsumerWidget,并在其中使用 watch() 来观察 Provider,以此实现 即时初始化。代码示例如下:

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return const _EagerInitialization(
      child: MaterialApp(),
    );
  }
}

class _EagerInitialization extends ConsumerWidget {
  const _EagerInitialization({required this.child});
  final Widget child;

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    ref.watch(myProvider);
    return child;
  }
}

:当Provider重建,会不会导致整个应用重建?

:不会,它返回一个child,而不是 实例化MaterialApp本身,_EagerInitialization 重新构建,child变量不会改变,Widget没变化,Flutter自然不会重建它。

如果需要处理加载和错误状态,可以添加下述判断:

class _EagerInitialization extends ConsumerWidget {
  const _EagerInitialization({required this.child});
  final Widget child;

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final result = ref.watch(myProvider);

    if (result.isLoading) {
      return const CircularProgressIndicator();
    } else if (result.hasError) {
      return const Text('Oopsy!');
    }

    return child;
  }
}

3.8. 更细粒度的监听-select()

指定 Provider中某个值改变才进行刷新,精确控制刷新范围,可以避免不必要的重建。通常与ref.watch() 结合使用,简单使用代码示例:

class User {
  late String firstName, lastName;
}

@riverpod
User example(ExampleRef ref) => User()
  ..firstName = 'John'
  ..lastName = 'Doe';

class ConsumerExample extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    // 只关心firstName属性
    String name = ref.watch(exampleProvider.select((it) => it.firstName));
    return Text('Hello $name');
  }
}

如果是监听另外一个 异步Provider,可以使用 selectAsync() ,代码示例:

@riverpod
Object? example(ExampleRef ref) async {
  final firstName = await ref.watch(
    userProvider.selectAsync((it) => it.firstName),
  );
}

3.9. 案例:下拉刷新

🤷‍♂️ 就一个下拉刷新,重新执行请求的例子,比较简单:

import 'dart:convert';

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:http/http.dart' as http;
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'test_provider.g.dart';
part 'test_provider.freezed.dart';

void main() => runApp(const ProviderScope(child: MyApp()));

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(home: ActivityView());
  }
}

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

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final activity = ref.watch(activityProvider);

    return Scaffold(
      appBar: AppBar(title: const Text('Pull to refresh')),
      body: RefreshIndicator(
        // 刷新时调用 ref.refresh() 刷新 Provider
        onRefresh: () => ref.refresh(activityProvider.future),
        child: ListView(
          children: [
            switch (activity) {
              AsyncValue<Activity>(:final valueOrNull?) =>
                  Text(valueOrNull.activity),
              AsyncValue(:final error?) => Text('Error: $error'),
              _ => const CircularProgressIndicator(),
            },
          ],
        ),
      ),
    );
  }
}

@riverpod
Future<Activity> activity(ActivityRef ref) async {
  final response = await http.get(
    Uri.https('www.boredapi.com', '/api/activity'),
  );

  final json = jsonDecode(response.body) as Map;
  return Activity.fromJson(Map.from(json));
}

@freezed
class Activity with _$Activity {
  factory Activity({
    required String activity,
    required String type,
    required int participants,
    required double price,
  }) = _Activity;

  factory Activity.fromJson(Map<String, dynamic> json) =>
      _$ActivityFromJson(json);
}

3.10. 案例:防抖动/取消网络请求

  • 防抖动 (Debouncing):发送请求前等待用户输入一段时间,确保即使用户输入很快,也只发送一个请求。
  • 取消 (Cancelling):如果用户在请求完成前离开了页面,则取消该请求,避免处理用户看不到的响应。

Riverpod 中可以利用 ref.onDispose() 结合 autoDisposeref.watch() 来实现上述行为。官方文档先写了个一个简单的例子:main.dart → 没啥内容,就按钮点击跳转 DetailPageView

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'detail_screen.dart';

void main() => runApp(const ProviderScope(child: MyApp()));

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      routes: {
        '/detail-page': (_) => const DetailPageView(),
      },
      home: const ActivityView(),
    );
  }
}

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

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    return Scaffold(
      appBar: AppBar(title: const Text('Home screen')),
      body: const Center(
        child: Text('Click the button to open the detail page'),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () => Navigator.of(context).pushNamed('/detail-page'),
        child: const Icon(Icons.add),
      ),
    );
  }
}

detail_screen.dart → 下拉发起请求,并刷新页面

import 'dart:convert';

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:http/http.dart' as http;
part 'detail_screen.freezed.dart';
part 'detail_screen.g.dart';

@freezed
class Activity with _$Activity {
  factory Activity({
    required String activity,
    required String type,
    required int participants,
    required double price,
  }) = _Activity;

  factory Activity.fromJson(Map<String, dynamic> json) =>
      _$ActivityFromJson(json);
}

@riverpod
Future<Activity> activity(ActivityRef ref) async {
  final response = await http.get(
    Uri.https('www.boredapi.com', '/api/activity'),
  );

  final json = jsonDecode(response.body) as Map;
  return Activity.fromJson(Map.from(json));
}

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

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final activity = ref.watch(activityProvider);

    return Scaffold(
      appBar: AppBar(
        title: const Text('Detail page'),
      ),
      body: RefreshIndicator(
        onRefresh: () => ref.refresh(activityProvider.future),
        child: ListView(
          children: [
            switch (activity) {
              AsyncValue(:final valueOrNull?) => Text(valueOrNull.activity),
              AsyncValue(:final error?) => Text('Error: $error'),
              _ => const Center(child: CircularProgressIndicator()),
            },
          ],
        ),
      ),
    );
  }
}

然后是离开页面 取消请求

@riverpod
Future<Activity> activity(ActivityRef ref) async {
  final client = http.Client();
  // 🌟 当 Provider 关闭时,关闭http客户端
  ref.onDispose(client.close);
  final response = await client.get(
    Uri.https('www.boredapi.com', '/api/activity'),
  );
  final json = jsonDecode(response.body) as Map;
  return Activity.fromJson(Map.from(json));
}

然后加上 防抖

@riverpod
Future<Activity> activity(ActivityRef ref) async {
  // 🌟 Provider 被销毁的标记,在onDispose() 回调时将值设置为true
  var didDispose = false;
  ref.onDispose(() => didDispose = true);
  // 延时500ms防抖
  await Future<void>.delayed(const Duration(milliseconds: 500));
  // 🌟 如果标记为true,说明Provider已经被销毁了,抛出异常
  if (didDispose) {
    throw Exception('Cancelled');
  }
  final client = http.Client();
  // 🌟 当 Provider 关闭时,关闭http客户端
  ref.onDispose(client.close);

  final response = await client.get(
    Uri.https('www.boredapi.com', '/api/activity'),
  );

  final json = jsonDecode(response.body) as Map;
  return Activity.fromJson(Map.from(json));
}

接着通过定义 Ref的扩展方法,减少重复代码编写:

extension DebounceAndCancelExtension on Ref {
  Future<http.Client> getDebouncedHttpClient([Duration? duration]) async {
    var didDispose = false;
    onDispose(() => didDispose = true);
    await Future<void>.delayed(duration ?? const Duration(milliseconds: 500));
    if (didDispose) {
      throw Exception('Cancelled');
    }
    final client = http.Client();
    onDispose(client.close);
    return client;
  }
}

// 调用处
@riverpod
Future<Activity> activity(ActivityRef ref) async {
  final client = await ref.getDebouncedHttpClient();
  final response = await client.get(
    Uri.https('www.boredapi.com', '/api/activity'),
  );
  final json = jsonDecode(response.body) as Map;
  return Activity.fromJson(Map.from(json));
}

3.11. 启用 riverpod_lint/custom_lint

Riverpod 附带一个可选的 riverpod_lint 包,该包提供 lint 规则来帮助您编写更好的代码,并提供自定义重构选项。添加完依赖,要启用它,还要添加一个与 pubspec.yaml 同级目录的 analysis_options.yaml 文件,并包含以下内容:

analyzer:
  plugins:
    - custom_lint

然后当你错误使用Riverpod,就可以在IDE中看到警告了,详细规则可以查阅:riverpod_lint