在 CODE WITH ANDREA:使用 Riverpod 的 Flutter 应用架构介绍 文章中,介绍了基于 Riverpod 包的应用程序架构,明确了该架构包含四层:数据层、 领域层、 应用层、展示层。 每一层都有各自的职责,并且对于如何跨越边界进行沟通有着明确的约定。 但是所有这些不同的类如何相互作用,以及我们如何使用它们来构建和发布应用程序中的功能? 这就是 Riverpod 及其所有有用的提供商发挥作用的地方。 在构建移动应用程序时,我们所做的大部分工作可以归结为两件事:
- 如何获取数据并将其显示在 UI 中?
- 如何响应输入事件执行数据变更? 因此,在本文中,我将回答这些问题,并让您更清楚地了解一切是如何结合在一起的。
准备好了吗?出发!
如何使用 Riverpod 架构获取数据
从概念上讲,当我们的应用从网络读取一些数据时,数据会从数据源一直“流”到 UI:

但如果我们仅获取数据,我们真的需要创建单独的服务和Controller吗?
当然不是。
相反,使用 Riverpod 获取数据的最简单方法是声明一个FutureProvider(或者StreamProvider如果您有实时数据)并在build我们的Widget的方法内部观察它。
对于此用例,我们的应用程序架构的简化版本如下所示:

整个过程由四个步骤组成:
- 该Widget监视 a
FutureProvider,后者又调用Repository方法来检索数据 - Repository从数据源获取数据
- 一旦收到响应,就会对其进行解析并从Repository(以及附加的
FutureProvider,也会缓存数据)返回 - 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(或StateNotifier或ChangeNotifier)!
您所需要的只是一个FutureProvider(用于一次性读取)或一个StreamProvider(如果您有实时数据源)。通过使用它们,您可以免费获得数据缓存!
要了解有关 Riverpod 数据缓存功能的更多信息,请阅读:Riverpod 数据缓存和提供程序生命周期完整指南。
获取数据:基本步骤
总之,当您需要获取一些数据并将其显示在 UI 中时,请按照以下步骤操作:
- 创建一个模型类并添加一个fromMap/fromJson工厂构造函数(仅存储/解析您需要在 UI 中显示的值)。
- 创建一个Repository并添加一个获取数据并返回Future(或Stream)的方法。
- 创建一个Repository提供程序以及使用刚刚添加的方法的FutureProvider(或)。StreamProvider
- 在Widget中,观察该提供程序并将您的数据映射到 UI。
再次说明一下它在运行时的工作方式:

当然,获取数据只是故事的一部分(而且是简单的一部分!)。
但有时,您还需要将一些数据写回数据库或远程后端,以响应输入事件。
“写回数据”的过程称为数据变更,让我们了解一下它是如何工作的。👇
如何使用 Riverpod 架构执行数据变更
当发生变更时,数据按以下顺序传播:Widget→Controller→服务(可选) →Repository→数据源:

当谈到数据变更时,我们需要解决一些问题:
- 我们如何响应输入事件在后端写入(创建/更新/删除)一些数据?
- 我们如何确保我们的 UI 在变更之前、期间和之后处于正确状态(数据/加载/错误)?
- 变更完成后,我们如何将数据(或错误)传播回 UI?
所有这些问题都要求引入Controller类,它可用于:
- 从Widget接收输入数据
- 调用Repository方法并传递数据,以便将其写入后端
- 更新状态,以便Widget可以处理数据/加载/错误场景
下面是一个更详细的图表,展示了变更发生时的情况:

这些步骤按以下顺序进行:
- 发生输入事件(例如用户提交表单)
- 输入数据被传递给Controller进行处理
- Controller将状态设置为“loading”,以便Widget可以更新 UI
- Controller调用异步方法,传递要写入Repository的数据
- Repository将数据转换为 DTO 并将其(异步)写入数据源
- 收到响应,或者抛出异常
- 成功或错误状态会传回Controller,由Controller进行处理
- 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之间进行调解。

让我们回顾一下所学到的知识。👇
数据变更:基本步骤
如果您需要在应用中实现数据变更,请按照以下步骤操作:
- 如果您之前还没有这样做,请在您的模型中添加序列化逻辑(toMap/ )。toJson
- 添加执行所需变更(创建/更新/删除)的Repository方法。
- 创建一个Controller作为AsyncNotifier子类。将build方法留空,并添加一个方法,该方法调用步骤 2 中的Repository方法并更新状态。
- 在Widget回调内部,用于ref.read访问Controller并调用步骤 3 中的方法。
作为额外的步骤,您可以在Widget的方法中观察Controller的状态build,并在变更过程中使用它来禁用 UI。要了解有关此步骤的更多信息,请阅读:如何使用 Flutter 中的 StateNotifier 和 AsyncValue 处理加载和错误状态
上述步骤负责执行数据变更。
然而,一个关键问题被忽略了。👇
一旦变更完成,我们如何在 UI 中显示更新的数据?
答案取决于数据源是否支持实时更新。
处理实时监听器与一次性读取器将是另一篇文章的主题。但现在,我会这样说:
尽可能使用支持实时更新的后端,因为这样可以在数据发生变化时轻松自动重建UI。👍
为了解释这一点,这里有一个更新的图表,显示每当发生变化时,UI 也会自动更新:

实现这个功能最简单的方式是:
- 添加一个Repository方法,该Stream方法返回一个新值,每次数据发生变化时都会发出一个新值
- 创建相应的StreamProvider
- 观察StreamProviderWidget中的,以便当数据发生变化时重建 UI
顺便说一句,这意味着Widget可以同时监视StreamProvider(获取数据)和AsyncNotifierProvider(在变更进行时从Controller获取状态更新),事实上,我在自己的应用程序中经常这样做。
话虽如此,现在该总结一下了。🙂
结论
在构建移动应用程序时,我们需要关注两个重要问题:获取数据和执行数据变更。
如果我们使用 Riverpod,遵循这个简单的规则将使我们的生活更轻松:
- 获取数据时,使用FutureProvider或StreamProvider
- 改变数据时,使用AsyncNotifier
由于 Riverpod 不太固执己见,我们可以遵循一个参考架构,其中每一层都有自己的职责。
通过遵循这种架构,获取数据变得非常简单:

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

上面的图表和我分享的步骤应该足以帮助您入门。 但随着你深入挖掘,你很可能会有一些其他问题:
- 当您拥有许多复杂的功能时,如何构建您的项目?
- 如果您需要合并来自不同Repository的数据该怎么办?
- 如果数据源抛出异常,谁负责捕获它?Repository还是Controller?
- 我们应该使用什么样的 UI 来表示加载和错误状态?
- 如果用户提交一些数据并在变更完成之前离开页面,会发生什么?
其中一些实施细节可以一次性为整个项目决定,而其他决定则可以根据具体情况进行。
参考资料
- 【转】CODE WITH ANDREA:如何使用 Riverpod 架构获取数据并执行数据变更
- How to Fetch Data and Perform Data Mutations with the Riverpod Architecture
文档信息
- 本文作者:Bob.Zhu
- 本文链接:https://home.mytool.group/2024/08/16/03-data-mutations-riverpod/
- 版权声明:自由转载-非商用-非衍生-保持署名(创意共享3.0许可证)
