【转】CODE WITH ANDREA:如何使用 Riverpod 架构获取数据并执行数据变更

2024/08/16 Flutter 共 6947 字,约 20 分钟
Bob.Zhu

CODE WITH ANDREA:使用 Riverpod 的 Flutter 应用架构介绍 文章中,介绍了基于 Riverpod 包的应用程序架构,明确了该架构包含四层:数据层、 领域层、 应用层、展示层。 每一层都有各自的职责,并且对于如何跨越边界进行沟通有着明确的约定。 但是所有这些不同的类如何相互作用,以及我们如何使用它们来构建和发布应用程序中的功能? 这就是 Riverpod 及其所有有用的提供商发挥作用的地方。 在构建移动应用程序时,我们所做的大部分工作可以归结为两件事:

  • 如何获取数据并将其显示在 UI 中?
  • 如何响应输入事件执行数据变更? 因此,在本文中,我将回答这些问题,并让您更清楚地了解一切是如何结合在一起的。

准备好了吗?出发!

如何使用 Riverpod 架构获取数据

从概念上讲,当我们的应用从网络读取一些数据时,数据会从数据源一直“流”到 UI:

flutter-layers-data-flow @2x

但如果我们仅获取数据,我们真的需要创建单独的服务和Controller吗?

当然不是。

相反,使用 Riverpod 获取数据的最简单方法是声明一个FutureProvider(或者StreamProvider如果您有实时数据)并在build我们的Widget的方法内部观察它。

对于此用例,我们的应用程序架构的简化版本如下所示:

fetching-data-detail @2x

整个过程由四个步骤组成:

  1. 该Widget监视 a FutureProvider,后者又调用Repository方法来检索数据
  2. Repository从数据源获取数据
  3. 一旦收到响应,就会对其进行解析并从Repository(以及附加的FutureProvider,也会缓存数据)返回
  4. Widget接收数据并将AsyncValue<Model>其映射到 UI

但是我们如何在代码中实现这一点呢?

数据获取示例:天气应用

作为一个实际的例子,让我们看看如何从 API 中获取一些天气数据并将其显示在 UI 中。

第一步是创建一个Weather模型类:

// domain/weather.dart
class Weather {
  Weather(this.temp);
  final double temp;

  // just a basic implementation
  factory Weather.fromJson(Map<String, dynamic> json) {
    return Weather(json['temp'] as double);
  }
  // TODO: Implement ==, hashCode, toString()
}

然后,我们可以定义一个WeatherRepository和相应的提供程序:

// data/weather_repository.dart
import 'package:http/http.dart' as http;
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:riverpod_examples/weather.dart';

part 'weather_repository.g.dart';

class WeatherRepository {
  WeatherRepository(this.client);
  // this is the data source (from the http package)
  final http.Client client;

  Future<Weather> fetchWeather(String city) {
    // TODO: use the http client to:
    // 1. fetch the weather data
    // 2. parse the response and return a Weather object
  }
}

// this will generate a weatherRepositoryProvider
@riverpod
WeatherRepository weatherRepository(WeatherRepositoryRef ref) {
  return WeatherRepository(http.Client());
}

// this will generate a fetchWeatherProvider
@riverpod
Future<Weather> fetchWeather(FetchWeatherRef ref, String city) {
  return ref.watch(weatherRepositoryProvider).fetchWeather(city);
}

上面的代码使用了Riverpod Generator 包@riverpod中的现代语法(但这不是强制性的,我们可以使用常规提供程序代替)。要了解更多信息,请阅读:如何使用 Flutter Riverpod Generator 自动生成提供程序。

最重要的是我们有个provider

  • 这样weatherRepositoryProvider我们就可以访问Repository
  • 这样fetchWeatherProvider我们就可以获取天气信息(并缓存它)

最后,这是 UI 代码:

// presentation/weather_ui.dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:riverpod_examples/weather_repository.dart';

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

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    // note 
    final weatherAsync = ref.watch(fetchWeatherProvider('London'));
    return weatherAsync.when(
      data: (weather) => Text(weather.temp.toString()),
      loading: () => const Center(child: CircularProgressIndicator()),
      error: (e, _) => Text(e.toString()),
    );
  }
}

请注意,由于我们从异步提供程序获取数据,因此我们的窗口Widget将重建两次。第一次使用值为AsyncLoading(首次安装窗口Widget时),然后再次使用值为AsyncData(weather)(获取数据后)。

这是一个非常简单的数据获取示例。但即使我们需要解析复杂的 JSON 或获取整个结果列表,我们也只需要四个要素:

  • 用于显示 UI 的Widget类
  • 表示数据的模型类
  • 从网络中提取数据的Repository
  • 一些provider把所有东西粘合在一起

请注意,在获取数据时,根本不需要Controller。

我知道使用 Riverpod 的人在这一点上经常会感到困惑,所以我再说一遍:

当您只是获取数据(无论多么复杂)时,您根本不需要AsyncNotifier(或StateNotifierChangeNotifier)!

您所需要的只是一个FutureProvider(用于一次性读取)或一个StreamProvider(如果您有实时数据源)。通过使用它们,您可以免费获得数据缓存!

要了解有关 Riverpod 数据缓存功能的更多信息,请阅读:Riverpod 数据缓存和提供程序生命周期完整指南

获取数据:基本步骤

总之,当您需要获取一些数据并将其显示在 UI 中时,请按照以下步骤操作:

  • 创建一个模型类并添加一个fromMap/fromJson工厂构造函数(仅存储/解析您需要在 UI 中显示的值)。
  • 创建一个Repository并添加一个获取数据并返回Future(或Stream)的方法。
  • 创建一个Repository提供程序以及使用刚刚添加的方法的FutureProvider(或)。StreamProvider
  • 在Widget中,观察该提供程序并将您的数据映射到 UI。

再次说明一下它在运行时的工作方式:

flutter-layers-data-flow @2x

当然,获取数据只是故事的一部分(而且是简单的一部分!)。

但有时,您还需要将一些数据写回数据库或远程后端,以响应输入事件。

“写回数据”的过程称为数据变更,让我们了解一下它是如何工作的。👇

如何使用 Riverpod 架构执行数据变更

当发生变更时,数据按以下顺序传播:Widget→Controller→服务(可选) →Repository→数据源:

flutter-layers-data-mutation @2x

当谈到数据变更时,我们需要解决一些问题:

  • 我们如何响应输入事件在后端写入(创建/更新/删除)一些数据?
  • 我们如何确保我们的 UI 在变更之前、期间和之后处于正确状态(数据/加载/错误)?
  • 变更完成后,我们如何将数据(或错误)传播回 UI?

所有这些问题都要求引入Controller类,它可用于:

  • 从Widget接收输入数据
  • 调用Repository方法并传递数据,以便将其写入后端
  • 更新状态,以便Widget可以处理数据/加载/错误场景

下面是一个更详细的图表,展示了变更发生时的情况:

data-mutation-detail @2x

这些步骤按以下顺序进行:

  1. 发生输入事件(例如用户提交表单)
  2. 输入数据被传递给Controller进行处理
  3. Controller将状态设置为“loading”,以便Widget可以更新 UI
  4. Controller调用异步方法,传递要写入Repository的数据
  5. Repository将数据转换为 DTO 并将其(异步)写入数据源
  6. 收到响应,或者抛出异常
  7. 成功或错误状态会传回Controller,由Controller进行处理
  8. Controller将状态设置为“成功”或“错误”,并且Widget更新 UI

请注意,按照上述步骤,我们可以解决两个问题:

  • 执行数据变更(通过在后端创建/更新/删除数据)
  • 更新 UI(通过将状态设置为加载/成功/错误)

在上述所有类中,Controller起着非常重要的作用。👇

使用Controller类处理变更

Controller可以作为子类实现。我在关于展示层AsyncNotifier的文章中已经详细介绍了Controller,因此我不会在这里分享完整的示例。

但主要思想是数据变更是可能成功或失败的异步操作,最好将变更逻辑保留在Widget之外。

例如,我们可以这样实现一个可以用来更新产品的Controller:

@riverpod
class EditProductController extends _$EditProductController {
  @override
  FutureOr<void> build() {
    // perform some initialization if needed
    // then return the initial value
  }

  Future<void> updateProduct({
    required Product product, // the previous product
    required String title, // the new title
    required String description, // the new description
  }) async {
    final productsRepository = ref.read(productsRepositoryProvider);
    final updatedProduct = product.copyWith(
      title: title,
      description: description,
    );
    state = const AsyncLoading();
    // perform the mutation and update the state
    state = await AsyncValue.guard(
      () => productsRepository.updateProduct(updatedProduct),
    );
  }

  Future<void> deleteProduct(Product product) async {
    // similar to the method above, but use the repository
    // to delete the product instead
  }
}

以下是我们如何updateProduct从Widget调用该方法:

onPressed: () => ref
    .read(editProductControllerProvider.notifier)
    .updateProduct(
      product: product,
      title: _titleController.text,
      description: _descriptionController.text,
    ),

我们可以看到,Controller帮助我们将 UI 代码与更新数据的逻辑分开,这使得我们的代码更具可读性、可测试性和可维护性。

通常,Controller负责:

  • 管理Widget状态(成功/加载/错误)
  • 获取任何输入参数并将其处理为可以传递给Repository(或服务类)的对象
  • (可选)如果变更成功,则导航到其他屏幕(有关更多信息,请阅读:如何在 Flutter 中使用 GoRouter 和 Riverpod 进行无上下文导航)

但请注意,Controller并不执行实际的变更(Repository通过与数据源(即真相的来源)对话来实现这一点)。

因此,如果您发现自己在Controller内存储某些应用程序状态(例如主题设置或用户身份验证状态),那么您做错了。

相反,请记住真相的来源是在数据层,并且您的Controller应该只在Widget和Repository之间进行调解。

data-mutation-detail-source-of-truth @2x

让我们回顾一下所学到的知识。👇

数据变更:基本步骤

如果您需要在应用中实现数据变更,请按照以下步骤操作:

  1. 如果您之前还没有这样做,请在您的模型中添加序列化逻辑(toMap/ )。toJson
  2. 添加执行所需变更(创建/更新/删除)的Repository方法。
  3. 创建一个Controller作为AsyncNotifier子类。将build方法留空,并添加一个方法,该方法调用步骤 2 中的Repository方法并更新状态。
  4. 在Widget回调内部,用于ref.read访问Controller并调用步骤 3 中的方法。

作为额外的步骤,您可以在Widget的方法中观察Controller的状态build,并在变更过程中使用它来禁用 UI。要了解有关此步骤的更多信息,请阅读:如何使用 Flutter 中的 StateNotifier 和 AsyncValue 处理加载和错误状态

上述步骤负责执行数据变更。

然而,一个关键问题被忽略了。👇

一旦变更完成,我们如何在 UI 中显示更新的数据?

答案取决于数据源是否支持实时更新。

处理实时监听器与一次性读取器将是另一篇文章的主题。但现在,我会这样说:

尽可能使用支持实时更新的后端,因为这样可以在数据发生变化时轻松自动重建UI。👍

为了解释这一点,这里有一个更新的图表,显示每当发生变化时,UI 也会自动更新:

data-mutation-detail-realtime @2x

实现这个功能最简单的方式是:

  • 添加一个Repository方法,该Stream方法返回一个新值,每次数据发生变化时都会发出一个新值
  • 创建相应的StreamProvider
  • 观察StreamProviderWidget中的,以便当数据发生变化时重建 UI

顺便说一句,这意味着Widget可以同时监视StreamProvider(获取数据)和AsyncNotifierProvider(在变更进行时从Controller获取状态更新),事实上,我在自己的应用程序中经常这样做。

话虽如此,现在该总结一下了。🙂

结论

在构建移动应用程序时,我们需要关注两个重要问题:获取数据和执行数据变更。

如果我们使用 Riverpod,遵循这个简单的规则将使我们的生活更轻松:

  • 获取数据时,使用FutureProvider或StreamProvider
  • 改变数据时,使用AsyncNotifier

由于 Riverpod 不太固执己见,我们可以遵循一个参考架构,其中每一层都有自己的职责。

通过遵循这种架构,获取数据变得非常简单:

fetching-data-detail @2x

虽然数据变更需要更多的工作,但它们也可以以可重复的方式实现(特别是如果我们支持实时更新):

data-mutation-detail-realtime @2x

上面的图表和我分享的步骤应该足以帮助您入门。 但随着你深入挖掘,你很可能会有一些其他问题:

  • 当您拥有许多复杂的功能时,如何构建您的项目?
  • 如果您需要合并来自不同Repository的数据该怎么办?
  • 如果数据源抛出异常,谁负责捕获它?Repository还是Controller?
  • 我们应该使用什么样的 UI 来表示加载和错误状态?
  • 如果用户提交一些数据并在变更完成之前离开页面,会发生什么?

其中一些实施细节可以一次性为整个项目决定,而其他决定则可以根据具体情况进行。

参考资料

文档信息

Search

    Table of Contents