以下页面演示了如何使用最佳实践构建应用。本指南中的建议适用于大多数应用,可以使其更易于扩展、测试和维护。然而,这些是指导原则,并非固定不变的规则,您应根据自己的独特需求进行调整。

本节概述了 Flutter 应用如何进行架构设计。它解释了应用的各个层级以及每个层级中存在的类。本节之后的部分将提供具体的代码示例,并详细讲解一个实现了这些建议的 Flutter 应用。

项目结构概述

#

关注点分离是设计 Flutter 应用时最重要的原则。您的 Flutter 应用应分为两个主要层级:UI 层和数据层。

每个层级都进一步细分为不同的组件,每个组件都有独特的职责、定义良好的接口、边界和依赖关系。本指南建议您将应用拆分为以下组件:

  • 视图
  • 视图模型
  • 存储库
  • 服务

MVVM

#

如果您遇到过模型-视图-视图模型 (Model-View-ViewModel) 架构模式 (MVVM),对此会很熟悉。MVVM 是一种架构模式,将应用的一个功能分为三个部分:模型 (Model)视图模型 (ViewModel)视图 (View)。视图和视图模型构成了应用的 UI 层。存储库和服务代表应用的数据,或 MVVM 的模型层。这些组件中的每一个都将在下一节中定义。

MVVM architectural pattern

应用中的每个功能都将包含一个描述 UI 的视图和一个处理逻辑的视图模型,一个或多个作为应用数据真实来源的存储库,以及零个或多个与外部 API(如客户端服务器和平台插件)交互的服务。

应用的单个功能可能需要以下所有对象:

An example of the Dart objects that might exist in one feature using the architecture described on page.

到本页末尾,这些对象以及连接它们的箭头都将得到详细解释。在本指南中,以下该图的简化版本将用作锚点。

A simplified diagram of the architecture described on this page.

UI 层

#

应用的 UI 层负责与用户交互。它向用户显示应用的数据,并接收用户输入,例如点击事件和表单输入。

UI 会对数据变化或用户输入做出反应。当 UI 从存储库接收到新数据时,它应该重新渲染以显示新数据。当用户与 UI 交互时,UI 应该随之改变以反映该交互。

UI 层由两个基于 MVVM 设计模式的架构组件组成:

  • 视图描述了如何向用户呈现应用数据。具体来说,它们指的是小部件组合,用于构成一个功能。例如,视图通常(但并非总是)是具有 Scaffold 小部件以及小部件树中其下方所有小部件的屏幕。视图还负责响应用户交互将事件传递给视图模型。
  • 视图模型包含将应用数据转换为UI 状态的逻辑,因为来自存储库的数据通常与需要显示的数据格式不同。例如,您可能需要组合来自多个存储库的数据,或者您可能想要过滤数据记录列表。

视图和视图模型应具有一对一的关系。

A simplified diagram of the architecture described on this page with the view and view model objects highlighted.

最简单地说,视图模型管理 UI 状态,视图显示该状态。使用视图和视图模型,您的 UI 层可以在配置更改(例如屏幕旋转)期间保持状态,并且您可以独立于 Flutter 小部件测试 UI 的逻辑。

应用的一个功能是以用户为中心的,因此由 UI 层定义。每对视图和视图模型的实例都定义了应用中的一个功能。这通常是应用中的一个屏幕,但并非必须如此。例如,考虑登录和注销。登录通常在特定屏幕上完成,其唯一目的是为用户提供登录方式。在应用代码中,登录屏幕将由 LoginViewModel 类和 LoginView 类组成。

另一方面,注销应用通常不在专用屏幕上完成。注销功能通常以菜单中的按钮、用户帐户屏幕或任何数量的不同位置呈现给用户。它通常在多个位置呈现。在这种情况下,您可能有一个 LogoutViewModel 和一个 LogoutView,其中只包含一个可以放入其他小部件的按钮。

视图

#

在 Flutter 中,视图是应用的小部件类。视图是渲染 UI 的主要方法,不应包含任何业务逻辑。它们应从视图模型接收渲染所需的所有数据。

A simplified diagram of the architecture described on this page with the view object highlighted.

视图应包含的唯一逻辑是:

  • 基于视图模型中的标志或可空字段来显示和隐藏小部件的简单 if 语句
  • 动画逻辑
  • 基于设备信息(如屏幕尺寸或方向)的布局逻辑。
  • 简单路由逻辑

所有与数据相关的逻辑都应在视图模型中处理。

视图模型

#

视图模型公开渲染视图所需的应用数据。在本页描述的架构设计中,Flutter 应用的大部分逻辑都存在于视图模型中。

A simplified diagram of the architecture described on this page with the view model object highlighted.

视图模型的主要职责包括:

  • 从存储库中检索应用数据,并将其转换为适合在视图中呈现的格式。例如,它可能会过滤、排序或聚合数据。
  • 维护视图中所需的当前状态,以便视图可以在不丢失数据的情况下重建。例如,它可能包含布尔标志以在视图中条件性地渲染小部件,或者包含一个字段来跟踪轮播图的哪个部分在屏幕上处于活动状态。
  • 向视图公开可以附加到事件处理程序(如按钮按下或表单提交)的回调(称为命令)。

命令以命令模式命名,是 Dart 函数,允许视图在不知道其实现的情况下执行复杂逻辑。命令作为视图模型类的成员编写,由视图类中的手势处理程序调用。

您可以在应用架构案例研究UI 层部分找到视图、视图模型和命令的示例。

要温和地了解 Flutter 中的 MVVM,请查看状态管理基础

数据层

#

应用的数据层处理您的业务数据和逻辑。数据层由两个架构部分组成:服务和存储库。这些部分应具有明确的输入和输出,以简化其可重用性和可测试性。

A simplified diagram of the architecture described on this page with the Data layer highlighted.

使用 MVVM 术语,服务和存储库构成了您的模型层。

存储库

#

存储库 (Repository) 类是模型数据的真实来源。它们负责从服务中轮询数据,并将原始数据转换为领域模型。领域模型代表应用所需的数据,并以视图模型类可以使用的格式进行格式化。您的应用中处理的每种不同类型的数据都应该有一个存储库类。

存储库处理与服务相关的业务逻辑,例如:

  • 缓存
  • 错误处理
  • 重试逻辑
  • 刷新数据
  • 轮询服务以获取新数据
  • 根据用户操作刷新数据
A simplified diagram of the architecture described on this page with the Repository object highlighted.

存储库将应用数据作为领域模型输出。例如,社交媒体应用可能有一个 UserProfileRepository 类,它公开一个 Stream<UserProfile?>,每当用户登录或注销时都会发出新值。

存储库输出的模型由视图模型使用。存储库和视图模型之间存在多对多的关系。一个视图模型可以使用多个存储库来获取所需数据,一个存储库也可以被多个视图模型使用。

存储库之间不应相互感知。如果您的应用有需要来自两个存储库的数据的业务逻辑,您应该在视图模型或领域层中组合数据,尤其是在存储库到视图模型的关系复杂时。

服务

#

服务位于应用的最底层。它们封装 API 端点并公开异步响应对象,例如 FutureStream 对象。它们仅用于隔离数据加载,并且不持有任何状态。您的应用每个数据源都应该有一个服务类。服务可能封装的端点示例包括:

  • 底层平台,如 iOS 和 Android API
  • REST 端点
  • 本地文件

根据经验,当所需数据存在于应用 Dart 代码之外时(以上所有示例都属于这种情况),服务是最有用的。

服务和存储库之间存在多对多的关系。一个存储库可以使用多个服务,一个服务也可以被多个存储库使用。

A simplified diagram of the architecture described on this page with the Service object highlighted.

可选:领域层

#

随着应用的增长和功能的增加,您可能需要抽象出那些给视图模型增加过多复杂度的逻辑。这些类通常被称为交互器或用例

用例负责使 UI 层和数据层之间的交互更简单、更可重用。它们从存储库中获取数据并使其适用于 UI 层。

MVVM design pattern with an added domain layer object

用例主要用于封装否则将存在于视图模型中的业务逻辑,并满足以下一个或多个条件:

  1. 需要合并来自多个存储库的数据
  2. 非常复杂
  3. 该逻辑将被不同的视图模型重用

此层是可选的,因为并非所有应用或应用中的功能都有这些要求。如果您认为您的应用会从此附加层中受益,请考虑其优缺点:

优点缺点
✅ 避免视图模型中的代码重复❌ 增加架构的复杂性,引入更多类和更高的认知负荷
✅ 通过将复杂的业务逻辑与 UI 逻辑分离来提高可测试性❌ 测试需要额外的模拟对象
✅ 提高视图模型中的代码可读性❌ 增加了代码中的额外样板

使用用例进行数据访问

#

添加领域层时的另一个考虑因素是,视图模型是否会继续直接访问存储库数据,或者您是否会强制视图模型通过用例来获取数据。换句话说,您会根据需要添加用例吗?也许当您注意到视图模型中重复的逻辑时?或者,无论用例中的逻辑是否简单,每次视图模型需要数据时都会创建一个用例吗?

如果您选择后者,它会加剧前面概述的优缺点。您的应用代码将极具模块化和可测试性,但也会增加大量不必要的开销。

一个好的方法是仅在需要时添加用例。如果您发现您的视图模型大部分时间都是通过用例访问数据,那么您随时可以重构代码以完全利用用例。本指南后面使用的示例应用为某些功能提供了用例,但也包含直接与存储库交互的视图模型。一个复杂的功能最终可能看起来像这样:

A simplified diagram of the architecture described on this page with a use case object.

添加用例的这种方法由以下规则定义:

  • 用例依赖于存储库
  • 用例和存储库之间存在多对多的关系
  • 视图模型依赖于一个或多个用例以及一个或多个存储库

这种使用用例的方法最终看起来不像分层千层面,而更像是一份摆盘正餐,有两道主菜(UI 和数据层)和一道配菜(领域层)。用例只是具有明确输入和输出的实用工具类。这种方法灵活且可扩展,但需要更勤奋地维护秩序。

反馈

#

由于本网站的此部分正在不断发展,我们欢迎您的反馈