likes
comments
collection
share

[译][官方文档] Flutter/Dart 状态管理库 Riverpod - 概要 - 创建第一个 provider/网络 请求

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

!!!译文为作者本人人肉翻译~转载请注明出处!!!


原文链接:Make your first provider/network request | Riverpod

pub:riverpod | Dart Package (flutter-io.cn)

译时版本: 2.4.5



创建第一个 provider/网络 请求

网络请求是大部分应用的核心。但是创建网络请求有大量需要考虑的地方:

  • 创建请求时 UI 应该渲染加载中的状态
  • 错误应该被优雅地处理
  • 如果可能请求应该被缓存起来

该章节中,会看到 Riverpod 如何自然地帮我们处理这些。

设置 ProviderScope

开始发起网络请求前,确保在应用的根组件上添加了  ProviderScope 。

void main() {
  runApp(
    // 要安装 Riverpod,首先需要添加该组件。
    // 这不会在 "MyApp" 里面,而是 "runApp" 的直属参数。
    ProviderScope(
      child: MyApp(),
    ),
  );
}

这样做可以使 Riverpod 用于整个应用。

在 "provider" 中执行网络请求

执行网络请求,我们一般称之为“业务逻辑”。在 Riverpod 中,业务逻辑是放在 "provider" 里的。 provider 是超级强大的函数。它们像正常的函数,具有以下优势:

  • 可缓存
  • 提供默认的错误/加载中处理
  • 可监听
  • 数据发生变化时自动重新执行

这使 provider 完美适合 GET 网络请求(对于* POST 等*请求,查看 执行副作用Performing side effects)。

作为示例,建议无聊时使用随机 activity 创建一个简单应用。 想这样做的话,可以使用 Bored API。尤其是为 /api/activity 节点执行一个 GET 请求。这会返回一个 JSON 对象,然后我们会把它解析为一个 Dart 类实例。 下一步是在 UI 上显示该 activity 。当请求创建时会确保渲染加载中状态,并优雅地处理错误 。

听着很帅吧?开始做吧!

定义(数据)模型

开始之前需要定义用以接收 API 数据的模型。该模型也需要将 JSON 对象转换为 Dart 类实例的方式。

通常,建议使用代码生成器如 Freezed 或 json_serializable 以处理 JSON 解码。当然,也可以手动编写。

不管什么方式吧,下面就是完成的模型:

activity.dart


import 'package:freezed_annotation/freezed_annotation.dart';

part 'activity.freezed.dart';
part 'activity.g.dart';

/// `GET /api/activity` 节点的响应
///
/// 使用 `freezed``json_serializable` 定义。
@freezed
class Activity with _$Activity {
  factory Activity({
    required String key,
    required String activity,
    required String type,
    required int participants,
    required double price,
  }) = _Activity;

  /// 将 JSON 对象转换为 [Activity] 实例。
  /// 这可以做到对 API 响应进行类型安全的读取。
  factory Activity.fromJson(Map<String, dynamic> json) =>
      _$ActivityFromJson(json);
}

创建 provider

现在有了模型,可以开始查询 API 了。 要这样做,需要创建第一个 provider。

定义 provider 的语法如下:

@riverpod
Result myFunction(MyFunctionRef ref) {
  <your logic here>
}
注解所有的 provider 都必须用 @riverpod 或 @Riverpod() 进行注解。该注解可以放在全局函数或类上。通过该注解,就能够配置 provider 了。例如,可以编写 @Riverpod(keepAlive: true) 关闭 "auto-dispose" (后面会看到) 。
注解函数被注解的函数名决定了要交互 provider 。对于给定的 myFunction 函数,会生成一个myFunctionProvider 变量。被注解的函数必须指定 "ref" 作为其第一个参数。除之以外,函数可带有任意数量的参数,包括泛型。该函数也可根据需要返回 Future/Stream 。该函数会在 provider 第一次被读取时调用。后续的读取中不会再调用该函数,但是会返回缓存的值。
Ref用来和其它 provider 交互的对象。所有的 provider 都会有这样一个对象;也是 provider 函数的参数,或 Notifier 的一个属性。该对象的类型取决于函数名/类名。

本例是想从 API GET 一个 activity 。 因为 GET 是一个异步操作,这意味着需要创建一个 Future<Activity>

使用前面定义的语法,就可以如下定义 providr :

provider.dart


import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'activity.dart';

// 用于代码生成器的必要代码
part 'provider.g.dart';

/// 这会创建一个名为 `activityProvider` 的 provider,
/// 它会缓存函数的结果。
@riverpod
Future<Activity> activity(ActivityRef ref) async {
  // 使用 package:http ,从 Bored API 获取一个随机 activity 。
  final response = await http.get(Uri.https('boredapi.com', '/api/activity'));
  // 使用 dart:convert ,然后将 JSON 负载解码为一个 Map 数据结构。
  final json = jsonDecode(response.body) as Map<String, dynamic>;
  // 最后,将 Map 转换为一个 Activity 实例。
  return Activity.fromJson(json);
}

在该代码片段中,定义了一个名为 activityProvider 的 provider ,UI 可以用其获取一个随机 activity 。 现在这还没啥用:

  • 网络请求不会执行直到 UI 至少读取一次 provider 。
  • 后续的读取不会再次执行网络请求,而是会返回前面获取到的 activity 。
  • 如果 UI 不再使用该 provider ,缓存会被清理。之后如果 UI 再次使用 provider ,就会创建一个新的网络请求。
  • 并不会捕获错误。这是自主的,因为 provider 本身就会处理错误。 如果网络请求或 JSON 解析抛出错误,错误会被 Riverpod 捕获。之后 UI 会自动用必要的信息渲染错误页面。

信息

provider 是懒加载的。定义一个 provider 不会执行网络请求。而是会在 provider 第一次被读取时执行网络请求。

在 UI 中渲染网络请求的响应

现在已经定义了一个 provider ,可以开始在 UI 内部用它表示 activity 了。

要和 provider 进行交互,需要一个名为 "ref" 的对象。 前面在 provider 的定义中已经看到了 provider 自带对 "ref" 对象的读取。 现在的情况不是在 provider 内部,而是一个组件。所以要如何获取到 "ref" 呢?

方案是使用称作 Consumer 的自定义组件。 一个 Consumer 是类似于 Builder 的组件,不过好处是提供了一个 "ref" 。这使 UI 能够读取 provider 。 下面示例展示了如何使用 Consumer

consumer.dart

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

import 'activity.dart';
import 'provider.dart';

/// 应用的主页
class Home extends StatelessWidget {
  const Home({super.key});

  @override
  Widget build(BuildContext context) {
    return Consumer(
      builder: (context, ref, child) {
        // 读取 activityProvider。这会启动网络请求(如果还没启动过的话)。
        // 使用 ref.watch ,无论何时 activityProvider 有所更新,该组件都会重新构建。
        // 会在以下时机发生:
        // - 从 "加载中" 变为 "data/error"
        // - 请求被刷新
        // - 结果在本地被修改 (例如执行副作用)
        // 。。。
        final AsyncValue<Activity> activity = ref.watch(activityProvider);

        return Center(
          /// 由于网络请求是异步的且有可能失败,所以既要处理错误,又要处理加载中的状态。
          /// 可以使用模式匹配来对应。
          /// 也可用 `if (activity.isLoading) { ... } else if (...)` 作为替代方案
          child: switch (activity) {
            AsyncData(:final value) => Text('Activity: ${value.activity}'),
            AsyncError() => const Text('Oops, something unexpected happened'),
            _ => const CircularProgressIndicator(),
          },
        );
      },
    );
  }
}

在这些代码片段中,使用了 Consumer 来读取 activityProvider 并显示 activity 。 也能够优雅地处理加载中/错误状态。 注意 UI 能够处理加载中/错误状态而无需在 provider 中作特殊处理。 同时,如果组件要重新构建,网络请求不会相应地再次执行。其它组件也可以访问同一个 provider 而不需要再次执行网络请求。

信息

组件可根据需要监听多个 provider 。只需要添加更多的 ref.watch 调用来做到这点。

提示

确保安装了 linter 。这可以使 IDE 提供重构选项以自动添加 Consumer 或者将 StatelessWidget 转换为 ConsumerWidget

相关的安装步骤查看 开始 

进阶:用 ConsumerWidget 代替 Consumer 移除代码阶层

在前面的示例中,用了 Consumer 读取 provider 。 尽管这种方式没有什么错误,但是添加的代码层会增加阅读代码的难度。

Riverpod 提供了等效的替代方案:替换 StatelessWidget/StatefulWidget 中返回 Consumer 的 return 语句,定义了 ConsumerWidget/ConsumerStatefulWidgetConsumerWidget 和 ConsumerStatefulWidget 是 StatelessWidget/StatefulWidget 和  Consumer 的有效混合物。它们和原始计数器部分的行为完全相同,但是便利之处是提供了 "ref" 。

可以如下使用 ConsumerWidget 来重写前面的示例:


/// 定义子类 "ConsumerWidget" 代替 "StatelessWidget" 。
/// 这等同于返回 "Consumer" 的 "StatelessWidget" 。
class Home extends ConsumerWidget {
  const Home({super.key});

  @override
  // 注意 "build" 现在是如何接收一个附加参数: "ref"
  Widget build(BuildContext context, WidgetRef ref) {
    // 可以在组件内部用和 "Consumer" 一样的方式使用 "ref.watch" 
    final AsyncValue<Activity> activity = ref.watch(activityProvider);

    // 渲染逻辑保持不变
    return Center(/* ... */);
  }
}

对于 ConsumerStatefulWidget ,可改写为:


// 继承 ConsumerStatefulWidget 。
// 这等同于 "Consumer" + "StatefulWidget" 。
class Home extends ConsumerStatefulWidget {
  const Home({super.key});

  @override
  ConsumerState<ConsumerStatefulWidget> createState() => _HomeState();
}

// 注意继承 "ConsumerState" 而不是 "State" 的方式。
// 这和 "ConsumerWidget" vs "StatelessWidget" 是一样的原则。
class _HomeState extends ConsumerState<Home> {
  @override
  void initState() {
    super.initState();

    // 状态生命周期也带有对 "ref" 的访问。
    // 这使一些处理成为可能,如为指定的 provider 添加监听以展示 对话框/snackbar 。
    ref.listenManual(activityProvider, (previous, next) {
      // TODO 展示 snackbar/对话框
    });
  }

  @override
  Widget build(BuildContext context) {
    // "ref" 不再作为参数传递,而是成了 "ConsumerState" 的一个属性。
    // 因此可以在 "build" 内部继续使用 "ref.watch" 。
    final AsyncValue<Activity> activity = ref.watch(activityProvider);

    return Center(/* ... */);
  }
}

Flutter_hooks 要考虑的事: 绑定 HookWidget 和 ConsumerWidget

警告

如果之前从未听说过 "hooks" ,完全可以跳过该章节。 Flutter_hooks 是独立于 Riverpod 的包,但是通常和 Riverpod 一起使用。如果刚开始接触 Riverpod ,并不鼓励使用 "hooks" 。查看 关于 hooks 了解更多信息。

如果在使用 flutter_hooks ,可能想知道如何绑定 HookWidget 和 ConsumerWidget 。毕竟,两者都涉及改变继承的组件类。

对于该问题,Riverpod 提供了一个方案:HookConsumerWidget 和 StatefulHookConsumerWidget 。 与 ConsumerWidget 和 ConsumerStatefulWidgetConsumer 和 StatelessWidget/StatefulWidget 的融合类似,HookConsumerWidget 和 StatefulHookConsumerWidget 也是 Consumer 和 HookWidget/HookStatefulWidget 的融合。就此而论,它们都可以在同样的组件中同时使用 hooks 和 provider 。

要展示该方式,需要一点时间来重写前面的示例:


/// 定义了子类 "HookConsumerWidget" 。
/// 它将 "StatelessWidget" + "Consumer" + "HookWidget" 绑定在一起。
class Home extends HookConsumerWidget {
  const Home({super.key});

  @override
  // 注意 "build" 现在接收附加参数: "ref" 的方式
  Widget build(BuildContext context, WidgetRef ref) {
    // 这里在组件内部可以使用 hooks 如 "useState"
    final counter = useState(0);

    // 也可以使用 provider 的读取
    final AsyncValue<Activity> activity = ref.watch(activityProvider);

    return Center(/* ... */);
  }
}