跳到主内容

数据层

对实现 MVVM 架构的应用的数据层进行详细介绍。

应用程序的数据层(在 MVVM 术语中称为模型)是所有应用程序数据的单一事实来源。作为事实来源,它是更新应用程序数据的唯一场所。

它负责从各种外部 API 获取数据,将数据暴露给 UI,处理来自 UI 需要更新数据的事件,并根据需要向这些外部 API 发送更新请求。

本指南中的数据层有两个主要组件:仓库 (repositories)服务 (services)

A diagram that highlights the data layer components of an application.

  • 仓库是应用程序数据的事实来源,包含与这些数据相关的逻辑,例如响应新的用户事件来更新数据,或从服务中轮询数据。在支持离线功能时,仓库负责同步数据、管理重试逻辑以及缓存数据。
  • 服务是无状态的 Dart 类,用于与 API(如 HTTP 服务器和平台插件)进行交互。应用程序所需的任何非自身代码创建的数据都应从服务类中获取。

定义服务

#

服务类是所有架构组件中最明确的一个。它是无状态的,且其函数没有副作用。它的唯一工作是封装外部 API。通常每个数据源对应一个服务类,例如客户端 HTTP 服务器或平台插件。

A diagram that shows the inputs and outputs of service objects.

例如,在 Compass 应用中,有一个 APIClient 服务,负责处理对客户端服务器的 CRUD 调用。

api_client.dart
dart
class ApiClient {
  // Some code omitted for demo purposes.

  Future<Result<List<ContinentApiModel>>> getContinents() async { /* ... */ }

  Future<Result<List<DestinationApiModel>>> getDestinations() async { /* ... */ }

  Future<Result<List<ActivityApiModel>>> getActivityByDestination(String ref) async { /* ... */ }

  Future<Result<List<BookingApiModel>>> getBookings() async { /* ... */ }

  Future<Result<BookingApiModel>> getBooking(int id) async { /* ... */ }

  Future<Result<BookingApiModel>> postBooking(BookingApiModel booking) async { /* ... */ }

  Future<Result<void>> deleteBooking(int id) async { /* ... */ }

  Future<Result<UserApiModel>> getUser() async { /* ... */ }
}

服务本身是一个类,其中的每个方法封装了不同的 API 端点并暴露异步响应对象。继续前面删除已保存预订的例子,deleteBooking 方法返回一个 Future<Result<void>>

定义仓库

#

仓库的唯一职责是管理应用程序数据。仓库是单一类型应用程序数据的事实来源,它应该是该数据类型被变更的唯一场所。仓库负责从外部源轮询新数据、处理重试逻辑、管理缓存数据以及将原始数据转换为领域模型。

A diagram that highlights the repository component of an application.

你应该为应用程序中每种不同类型的数据分别建立一个仓库。例如,Compass 应用拥有名为 UserRepositoryBookingRepositoryAuthRepositoryDestinationRepository 等的仓库。

以下示例是 Compass 应用中的 BookingRepository,展示了仓库的基本结构。

booking_repository_remote.dart
dart
class BookingRepositoryRemote implements BookingRepository {
  BookingRepositoryRemote({
    required ApiClient apiClient,
  }) : _apiClient = apiClient;

  final ApiClient _apiClient;
  List<Destination>? _cachedDestinations;

  Future<Result<void>> createBooking(Booking booking) async {...}
  Future<Result<Booking>> getBooking(int id) async {...}
  Future<Result<List<BookingSummary>>> getBookingsList() async {...}
  Future<Result<void>> delete(int id) async {...}
}

BookingRepositoryApiClient 服务作为输入,并使用它来获取和更新服务器上的原始数据。重要的是,该服务应为私有成员,这样 UI 层就无法绕过仓库直接调用服务。

通过 ApiClient 服务,仓库可以轮询服务器上可能发生的用户已保存预订的更新,并发送 POST 请求以删除已保存的预订。

仓库转换为应用模型所需的原始数据可以来自多个源和多个服务,因此仓库和服务之间是多对多关系。一个服务可以被任意数量的仓库使用,而一个仓库也可以使用多个服务。

A diagram that highlights the data layer components of an application.

领域模型

#

BookingRepository 输出 BookingBookingSummary 对象,这些是领域模型。所有仓库都输出相应的领域模型。这些数据模型与 API 模型不同,它们只包含应用程序其余部分所需的数据。API 模型包含原始数据,通常需要过滤、组合或删除才能对应用的视图模型有用。仓库会对原始数据进行精炼,并将其作为领域模型输出。

在示例应用中,领域模型是通过诸如 BookingRepository.getBooking 等方法的返回值暴露出来的。getBooking 方法负责从 ApiClient 服务获取原始数据,并将其转换为 Booking 对象。它通过组合来自多个服务端点的数据来实现这一点。

booking_repository_remote.dart
dart
// This method was edited for brevity.
Future<Result<Booking>> getBooking(int id) async {
  try {
    // Get the booking by ID from server.
    final resultBooking = await _apiClient.getBooking(id);
    if (resultBooking is Error<BookingApiModel>) {
      return Result.error(resultBooking.error);
    }
    final booking = resultBooking.asOk.value;

    final destination = _apiClient.getDestination(booking.destinationRef);
    final activities = _apiClient.getActivitiesForBooking(
            booking.activitiesRef);

    return Result.ok(
      Booking(
        startDate: booking.startDate,
        endDate: booking.endDate,
        destination: destination,
        activity: activities,
      ),
    );
  } on Exception catch (e) {
    return Result.error(e);
  }
}

完成事件周期

#

纵观本页面,你已经看到用户如何通过事件(即用户在 Dismissible 组件上滑动)来删除已保存的预订。视图模型通过将实际的数据变更委托给 BookingRepository 来处理该事件。以下代码片段显示了 BookingRepository.deleteBooking 方法。

booking_repository_remote.dart
dart
Future<Result<void>> delete(int id) async {
  try {
    return _apiClient.deleteBooking(id);
  } on Exception catch (e) {
    return Result.error(e);
  }
}

仓库使用 _apiClient.deleteBooking 方法向 API 客户端发送 POST 请求,并返回一个 ResultHomeViewModel 消费该 Result 及其包含的数据,最后调用 notifyListeners,完成整个周期。

反馈

#

由于网站的此部分内容在不断演进,我们欢迎你的反馈