跳到主要内容

组合请求

到目前为止,我们只看到请求彼此独立的案例。 但一个常见的用例是必须根据另一个请求的结果触发请求。

我们可以使用将参数传递给您的请求机制来做到这一点, 方法是将一个提供者程序的结果作为参数传递给另一个提供者程序。

但这种方法有一些缺点:

  • 这泄露了实现细节。 现在,UI 需要了解所有被其他提供者程序使用的提供者程序。
  • 每当参数发生变化时,都会产生一个全新的状态。 通过传递参数,当参数更改时,无法保持以前的状态。
  • 它使合并请求变得更加困难。
  • 这使得开发工具的用处降低。devtool 不会知道提供者程序之间的关系。

为了改善这一点,Riverpod 提供了一种不同的方法来合并请求。

基础知识:获取 "ref"

组合请求的所有可能方法都有一个共同点:它们都基于 Ref 对象。

Ref 对象是所有提供者程序都有权访问的对象。 它允许他们访问各种生命周期监听器, 还可以使用各种方法来组合提供者程序。

可以获取的位置 Ref 取决于提供者程序的类型。

在函数提供者程序中,将 Ref 作为参数传递给提供者程序的函数:


int example(ExampleRef ref) {
// "Ref" 可以在这里用来阅读其他提供者程序
final otherValue = ref.watch(otherProvider);

return 0;
}

在类变体中,Ref 是通知者程序类的一个属性:


class Example extends _$Example {

int build() {
// "Ref" 可以在这里用来阅读其他提供者程序
final otherValue = ref.watch(otherProvider);

return 0;
}
}

使用 ref 读取提供者程序。

ref.watch 方法。

现在我们已经获得了一个 Ref,我们可以用它来组合请求。 执行此操作的主要方法是使用 ref.watch
通常建议对代码进行架构设计, 以便可以使用 ref.watch 的其他选项,因为它通常更易于维护。

ref.watch 方法采用提供者程序,并返回其当前状态。 然后,每当监听的提供者程序发生更改时,我们的提供者程序将在 下一帧或下一次读取时失效并重新生成。

通过使用 ref.watch,您的逻辑变得既是“反应式”又是“声明式”。
这意味着您的逻辑将在需要时自动重新计算。 并且更新机制不依赖于副作用,例如“更改”。 这类似于 StatelessWidgets 的行为方式。

例如,我们可以定义一个监听用户位置的提供者程序。 然后,我们可以使用这个位置来获取用户附近的餐馆列表。


Stream<({double longitude, double latitude})> location(LocationRef ref) {
// TO-DO: 返回获取当前位置的流
return someStream;
}


Future<List<String>> restaurantsNearMe(RestaurantsNearMeRef ref) async {
// 我们使用 "ref.watch" 来获取最新位置。
// 通过在提供者程序之后指定 ".future",
// 我们的代码将在等待到至少一个位置信息后可用。
final location = await ref.watch(locationProvider.future);

// 我们现在可以根据该位置发出网络请求。
// 例如,我们可以使用 Google Map API:
// https://developers.google.com/maps/documentation/places/web-service/search-nearby
final response = await http.get(
Uri.https('maps.googleapis.com', 'maps/api/place/nearbysearch/json', {
'location': '${location.latitude},${location.longitude}',
'radius': '1500',
'type': 'restaurant',
'key': '<your api key>',
}),
);
// 从 JSON 中获取餐厅名称
final json = jsonDecode(response.body) as Map;
final results = (json['results'] as List).cast<Map<Object?, Object?>>();
return results.map((e) => e['name']! as String).toList();
}
信息

当监听的提供者程序发生更改并且我们的请求重新计算时,将保留以前的状态,直到新请求完成。
同时,当请求处于挂起状态时,将设置 "isLoading" 和 "isReloading" 标志。

这使 UI 能够显示以前的状态或加载指示器,甚至两者兼而有之。

信息

请注意我们如何使用 ref.watch(locationProvider.future) 来替代 ref.watch(locationProvider)。 那是因为我们 locationProvider 是异步的。因此,我们希望等待初始值可用。

如果我们省略了这一点 .future,我们将收到一个 AsyncValue, 它是 locationProvider 当前状态的快照。但是,如果还没有可用的位置, 我们将无能为力。

警告

调用 ref.watch “命令式”执行的内部代码被认为是不好的做法。 这意味着在提供者程序的构建阶段可能未执行的任何代码。 这包括通告程序上的“监听器”回调或方法:


int example(ExampleRef ref) {
ref.watch(otherProvider); // 好!
ref.onDispose(() => ref.watch(otherProvider)); // 糟糕!

final someListenable = ValueNotifier(0);
someListenable.addListener(() {
ref.watch(otherProvider); // 糟糕!
});

return 0;
}


class MyNotifier extends _$MyNotifier {

int build() {
ref.watch(otherProvider); // 好!
ref.onDispose(() => ref.watch(otherProvider)); // 糟糕!

return 0;
}

void increment() {
ref.watch(otherProvider); // 糟糕!
}
}

ref.listen/listenSelf 方法。

ref.listen 方法是 ref.watch 的替代方法。
它类似于传统的 "listen"/"addListener" 方法。 它接受一个提供者程序和一个回调, 并在提供者程序的内容发生更改时调用该回调。

通常建议重构代码,您可以使用 ref.watch 替代 ref.listen, 因为后者由于其命令性质而更容易出错。
但是 ref.listen 可以有助于添加一些快速逻辑而不必进行重大重构。

我们可以重写 ref.watch 示例并使用 ref.listen 代替


int example(ExampleRef ref) {
ref.listen(otherProvider, (previous, next) {
print('Changed from: $previous, next: $next');
});

return 0;
}
信息

在提供者程序的构建阶段使用 ref.listen 是完全安全的。 如果以某种方式重新计算提供者程序,则以前的监听器将被删除。

或者,您可以根据需要使用 ref.listen 的返回值手动删除监听器。

ref.read 方法

最后一个可用选项是 ref.read。 它类似于 ref.watch 返回提供者程序的当前状态。 但与 ref.watch 不同的是,它不监听提供者程序。

因此,ref.read 应该只被用在你不能使用 ref.watch 的地方, 比如通告程序的内部方法。


class MyNotifier extends _$MyNotifier {

int build() {
// 糟糕的!不要在这里使用 "read",因为它不是反应性的
ref.read(otherProvider);

return 0;
}

void increment() {
ref.read(otherProvider); // 这里使用 "read" 就可以了
}
}
警告

ref.read 在提供者程序上使用时要小心,因为它不监听提供者程序, 因此如果不监听提供者程序,则该提供者程序可能会决定处置其状态。