跳到主要内容

开始你的第一次 provider/network 请求

网络请求是任何应用程序的核心。但是,在发出网络请求时,需要考虑很多事项:

  • UI 应在发出请求时呈现加载状态
  • 应妥善处理错误
  • 如果可能,应缓存请求

在本节中,我们将看到 Riverpod 如何帮助我们自然地处理所有这些问题。

配置 ProviderScope

在开始发出网络请求之前,请确保将其 ProviderScope 添加到应用程序的根目录。

void main() {
runApp(
// To install Riverpod, we need to add this widget above everything else.
// This should not be inside "MyApp" but as direct parameter to "runApp".
// 为了安装 Riverpod,我们需要将这个小组件添加到所有的小组件之上。
// 它不应该在 “MyApp” 内部,而是作为 “runApp” 的直接参数。
ProviderScope(
child: MyApp(),
),
);
}

这样就可以为整个应用程序启用 Riverpod。

备注

有关完整的安装步骤(例如安装 riverpod_lint 和运行代码生成器),请查看入门指南

在 “provider” 中执行网络请求

执行网络请求通常就是我们所说的“业务逻辑”。在 Riverpod 中,业务逻辑位于“providers”中。
provider 是一种具有超能力的函数。它们的行为与正常函数类似,并具有以下额外好处:

  • 保持缓存
  • 提供默认错误/加载处理
  • 可以被监听
  • 当某些数据发生变化时自动重新执行

这使得 provider 非常适合 GET 网络请求(与 POST/etc 请求一样,请参阅执行副作用)。

举个例子,让我们做一个简单的应用程序,建议在无聊时做一个随机的活动。
为此,我们将使用 Bored API。具体而言, 我们将在 /api/activity 端点上执行 GET 请求。端点返回一个 JSON 对象,我们将把它解析为 Dart 类实例。
然后,下一步是在 UI 中显示此活动。我们还将确保在发出请求时呈现加载状态,并优雅地处理错误。

听起来不错?让我们开始吧!

定义数据模型

在开始之前,我们需要定义从 API 接收的数据模型。 该模型还需要一种方法将 JSON 对象解析为 Dart 类实例。

通常,建议使用 Freezedjson_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`。

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(Ref ref) {
  <你的逻辑写这里>
}
注解

所有提供者程序都必须使用 @riverpod@Riverpod() 进行注释。 此注释可以放置在全局函数或类上。
通过此注释,可以配置提供者程序。

例如,我们可以通过编写 @Riverpod(keepAlive: true) 来禁用“自动处置”(我们将在后面看到)。

带注解的函数

带批注的函数的名称决定了如何与提供者程序进行交互。
对于给定的函数 myFunction ,将生成一个生成的 myFunctionProvider 变量。

带注释的函数必须指定“ref”作为第一个参数。
除此之外,该函数可以具有任意数量的参数,包括泛型。 如果愿意,该函数也可以自由返回 Future/Stream

首次读取提供者程序时将调用此函数。
后续读取不会再次调用该函数,而是返回缓存的值。

Ref

用于与其他提供者程序交互的对象。
所有提供者程序都有一个 Ref;要么作为 provider 函数的参数,要么作为 Notifier 的属性。 此对象的类型由函数/类的名称确定。

在我们的例子中,我们希望从 API 中 GET 一个活动。
由于 GET 是异步操作,这意味着我们需要创建一个 Future<Activity>

因此,使用前面定义的语法,我们可以按如下方式定义提供者程序:

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` 的提供者程序
/// 它可以缓存函数执行的结果

Future<Activity> activity(Ref ref) async {
// 使用 package:http, 我们可以从 Bored API 获取一个随机的活动。
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 的提供者程序, 我们的 UI 将能够使用该提供者程序来获取随机活动。值得一提的是:

  • 在 UI 读取提供者程序至少一次之前,不会执行网络请求。
  • 后续读取不会重新执行网络请求,而是返回之前提取的活动。
  • 如果 UI 停止使用此提供者程序,则缓存将被处置。 然后,如果 UI 再次使用提供者程序,则会发出新的网络请求。
  • 我们没有捕获错误。这是自动的,因为提供者程序本身会处理错误。
    如果网络请求或 JSON 解析抛出错误,则 Riverpod 将捕获该错误。 然后,UI 将自动包含呈现错误页面所需的信息。
信息

提供者程序是“懒惰的”。定义提供者程序不会执行网络请求。 相反,网络请求将在首次读取提供者程序时执行。

在 UI 中呈现网络请求的响应

现在我们已经定义了一个提供者程序,我们可以开始在 UI 中使用它来显示活动。

为了与提供者程序交互,我们需要一个名为“ref”的对象。 您之前可能在提供者程序定义中看到过它,因为提供者程序自然可以访问“ref”对象。
但在我们的例子中,我们不是提供者程序,而是小部件。那么我们如何获得“ref”呢?

解决方案是使用名为 Consumer 的自定义小部件。 Consumer 是一个类似于 Builder 的小部件,但还有一个额外的好处,那就是为我们提供了一个“ref”。 这使我们的 UI 能够读取提供者程序。以下示例展示了如何使用 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});


Widget build(BuildContext context) {
return Consumer(
builder: (context, ref, child) {
// 读取 activityProvider。如果没有准备开始,这将会开始一个网络请求。
// 通过使用 ref.watch,小组件将会在 activityProvider 更新时重建。
// 当下面的事情发生时,会更新小组件:
// - 响应从“正在加载”变为“数据/错误”
// - 请求重刷新
// - 结果被本地修改(例如执行副作用时)
// ...
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 显示活动。 我们还优雅地处理了加载/错误状态。
请注意 UI 如何能够处理加载/错误状态,而无需在提供者程序中执行任何特殊操作。
同时,如果小部件要重建,则不会正确地重新执行网络请求。 其他小部件也可以访问同一提供者程序,而无需重新执行网络请求。

信息

小部件可以根据需要,监听任意数量的提供者程序。为此,只需添加更多 ref.watch 调用即可。

提示

确保安装 linter。这将使您的 IDE 能够提供重构选项, 以自动添加 Consumer 或将 StatelessWidget 重构为 ConsumerWidget

有关安装步骤,请参阅入门指南

更进一步:使用 ConsumerWidget 替代 Consumer 删除代码缩进。

在前面的示例中,我们使用 Consumer 来读取提供者程序。
尽管这种方法没有错,但添加的缩进会使代码更难阅读。

Riverpod 提供了另一种实现相同结果的方法: 我们可以定义 ConsumerWidget / ConsumerStatefulWidget 来代替在 StatelessWidget / StatefulWidget 返回 Consumer 小组件。
ConsumerWidgetConsumerStatefulWidget 实际上是 StatelessWidget / StatefulWidgetConsumer 的融合。 它们的行为与原来的 couterpart 相同,但具有提供“ref”的额外好处。

我们可以使用 ConsumerWidget 重写前面的例子,如下所示:

/// 我们将“ConsumerWidget”替代“StatelessWidget”进行子类化。
/// 这相当于使用“StatelessWidget”并返回“Consumer”小组件。
class Home extends ConsumerWidget {
const Home({super.key});


// 请注意“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});


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

// 请注意,我们如何扩展“ConsumerState”而不是“State”。
// 这和 "ConsumerWidget" 与 "StatelessWidget" 是相同的原理。
class _HomeState extends ConsumerState<Home> {

void initState() {
super.initState();

// 状态生命周期也可以访问“ref”。
// 这使得在特定提供者程序上添加监听器,以便实现显示对话框/信息栏等功能。
ref.listenManual(activityProvider, (previous, next) {
// TODO 显示一个 snackbar/dialog
});
}


Widget build(BuildContext context) {
// "ref" is not passed as parameter anymore, but is instead a property of "ConsumerState".
// We can therefore keep using "ref.watch" inside "build".
// “ref”不再作为参数传递,而是作为“ConsumerState”的属性。
// 因此,我们可以继续在“build”中使用“ref.watch”。
final AsyncValue<Activity> activity = ref.watch(activityProvider);

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

Flutter_hooks 注意事项:结合 HookWidgetConsumerWidget

警告

如果您以前从未听说过“钩子(hooks)”,请随时跳过本节。
Flutter_hooks 是一个独立于 Riverpod 的软件包, 但经常与 Riverpod 一起使用。如果您不熟悉 Riverpod,不建议使用“钩子”。 有关详细信息,请参阅关于 Hooks(钩子)

如果您正在使用 flutter_hooks,您可能想知道如何将 ConsumerWidgetHookWidget 组合在一起。毕竟,两者都涉及更改扩展的小部件类。

Riverpod 为此问题提供了解决方案:HookConsumerWidgetStatefulHookConsumerWidget
类似于 ConsumerWidgetConsumerStatefulWidgetStatelessWidget / StatefulWidgetConsumer 融合, HookConsumerWidgetStatefulHookConsumerWidgetHookWidget / HookStatefulWidgetConsumer 的融合。 因此,它们允许在同一个小部件中同时使用钩子和提供者程序。

为了展示这一点,我们可以再次重写前面的例子:

/// 我们子类化了 "HookConsumerWidget"。
/// 这同时组合了 "StatelessWidget"、"Consumer"、"HookWidget"。
class Home extends HookConsumerWidget {
const Home({super.key});


// 请注意“build”现在如何接收一个额外的参数:“ref”
Widget build(BuildContext context, WidgetRef ref) {
// 可以在我们的小部件中使用诸如“useState”之类的钩子
final counter = useState(0);

// 我们还可以使用读取提供者程序
final AsyncValue<Activity> activity = ref.watch(activityProvider);

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