应用架构指南
构建 Flutter 应用的推荐方式。
以下页面演示了如何使用最佳实践来构建应用。本指南中的建议适用于大多数应用,能够使它们更易于扩展、测试和维护。但请注意,这些只是指导方针而非铁律,你应该根据自己的独特需求进行调整。
本节对 Flutter 应用的架构方式进行了概括性介绍。它解释了应用的各个层级以及每个层级中存在的类。本节之后的部分将提供具体的代码示例,并逐步演示如何实现这些建议。
项目结构概览
#关注点分离 (Separation-of-concerns) 是设计 Flutter 应用时要遵循的最重要原则。你的 Flutter 应用应该分为两个主要层级:UI 层和数据层。
每一层进一步细分为不同的组件,每个组件都有明确的职责、定义良好的接口、边界和依赖项。本指南建议将应用拆分为以下组件:
- 视图
- 视图模型 (View models)
- 仓库 (Repositories)
- 服务 (Services)
MVVM
#如果你了解 Model-View-ViewModel 架构模式 (MVVM),你会对这套方案感到熟悉。MVVM 是一种将应用功能拆分为三个部分的架构模式:Model(模型)、ViewModel(视图模型)和 View(视图)。视图和视图模型组成了应用的 UI 层。仓库和服务则代表了应用的数据层,即 MVVM 中的模型层。下一节将定义这些组件中的每一个。
应用中的每个功能都应包含一个用于描述 UI 的视图、一个用于处理逻辑的视图模型、一个或多个作为应用数据来源(Single Source of Truth)的仓库,以及零个或多个用于与外部 API(如客户端服务器和平台插件)交互的服务。
应用的单个功能可能需要以下所有对象:
到本页结束时,我们将详细解释每个对象及其连接箭头。在本指南中,以下简化版的图表将作为核心参考。
UI 层
#应用的 UI 层负责与用户交互。它向用户展示应用数据并接收用户输入,例如点击事件和表单输入。
UI 会对数据变化或用户输入做出响应。当 UI 从仓库接收到新数据时,它应该重新渲染以显示新数据。当用户与 UI 交互时,UI 也应做出相应改变以反映该交互。
根据 MVVM 设计模式,UI 层由两个架构组件组成:
-
视图 (Views) 描述了如何向用户呈现应用数据。具体来说,它们是指构成一个功能的组件集合 (compositions of widgets)。例如,视图通常(但不总是)是一个包含
Scaffold组件以及组件树中下方所有组件的屏幕。视图还负责将响应用户交互的事件传递给视图模型。 - 视图模型 (View models) 包含将应用数据转换为UI 状态 (UI State) 的逻辑,因为来自仓库的数据格式通常与 UI 所需展示的数据格式不同。例如,你可能需要合并来自多个仓库的数据,或者想要过滤数据记录列表。
视图和视图模型应该保持一对一的关系。
简单来说,视图模型管理 UI 状态,而视图负责显示该状态。通过使用视图和视图模型,你的 UI 层可以在配置更改(如屏幕旋转)期间保持状态,并且你可以独立于 Flutter 组件来测试 UI 的逻辑。
应用的功能是以用户为中心的,因此由 UI 层定义。每对配对的视图和视图模型都定义了应用中的一个功能。这通常是应用中的一个屏幕,但并非必须如此。例如,考虑登录和登出操作。登录通常是在一个特定屏幕上完成的,该屏幕的唯一目的就是为用户提供登录方式。在应用代码中,登录屏幕由 LoginViewModel 类和 LoginView 类组成。
另一方面,登出操作通常不在专门的屏幕上完成。登出功能通常以菜单中的按钮、用户账户屏幕或许多其他位置呈现给用户。它通常会出现在多个位置。在这种情况下,你可以拥有一个 LogoutViewModel 和一个 LogoutView,其中仅包含一个可以嵌入到其他组件中的按钮。
视图
#在 Flutter 中,视图即应用的组件类。视图是渲染 UI 的主要方法,不应包含任何业务逻辑。它们应接收从视图模型传递而来的所有渲染所需数据。
视图中唯一应该包含的逻辑是:
- 基于视图模型中的标志位或可空字段来显示或隐藏组件的简单 if 语句。
- 动画逻辑。
- 基于设备信息(如屏幕尺寸或方向)的布局逻辑。
- 简单的路由逻辑。
所有与数据相关的逻辑都应在视图模型中处理。
视图模型 (View models)
#视图模型公开了渲染视图所需的应用数据。在本页描述的架构设计中,Flutter 应用的大部分逻辑都存在于视图模型中。
视图模型的主要职责包括:
- 从仓库中检索应用数据并将其转换为适合视图呈现的格式。例如,它可能会过滤、排序或聚合数据。
- 维护视图所需的当前状态,以便视图在重建时不会丢失数据。例如,它可能包含用于有条件地渲染组件的布尔标志,或者跟踪轮播图当前处于哪一段的字段。
- 向视图公开回调函数(称为命令 (commands)),这些回调可以附加到事件处理器上,例如按钮按下或表单提交。
命令以 命令模式 (command pattern) 命名,是 Dart 函数,允许视图在无需了解具体实现的情况下执行复杂的逻辑。命令作为视图模型类的成员编写,由视图类中的手势处理器调用。
你可以在 应用架构案例研究 的 UI 层 部分找到视图、视图模型和命令的示例。
如需了解 Flutter 中 MVVM 的入门知识,请查看 状态管理基础知识。
数据层
#应用的数据层处理业务数据和逻辑。数据层由两部分架构组成:服务和仓库。这些部分应该具有定义明确的输入和输出,以简化其可重用性和可测试性。
使用 MVVM 的术语,服务和仓库构成了你的模型层 (model layer)。
仓库 (Repositories)
#仓库 (Repository) 类是模型数据的单一事实来源。它们负责从服务轮询数据,并将原始数据转换为领域模型 (domain models)。领域模型代表了应用需要的数据,并以视图模型类可以消费的格式进行组织。应用中处理的每种不同类型的数据都应该有一个对应的仓库类。
仓库负责处理与服务相关的业务逻辑,例如:
- 缓存。
- 错误处理。
- 重试逻辑。
- 刷新数据。
- 为新数据轮询服务。
- 基于用户操作刷新数据。
仓库以领域模型的形式输出应用数据。例如,社交媒体应用可能有一个 UserProfileRepository 类,它公开一个 Stream<UserProfile?>,每当用户登录或登出时就会发出一个新值。
仓库输出的模型被视图模型所使用。仓库和视图模型之间是多对多关系。一个视图模型可以使用多个仓库来获取所需数据,而一个仓库也可以被多个视图模型使用。
仓库之间不应相互感知。如果你的应用逻辑需要来自两个仓库的数据,你应该在视图模型或领域层中合并这些数据,特别是当你的仓库到视图模型的逻辑较为复杂时。
管理应用范围内的会话状态。
#由于仓库是应用数据的单一事实来源,它们也是管理应用生命周期状态 (app-wide lifecycle state) 的理想场所——即那些需要在多个视图模型之间共享,但不应超出当前应用会话而持久存在的数据。
应用生命周期状态的例子包括活动的登录会话、内存中的数据缓存或临时的应用设置。由于视图模型和仓库之间是多对多关系,多个视图模型可以依赖同一个仓库实例(通常通过服务定位器或依赖注入容器管理)。这允许不同的功能通过仓库公开的流和方法,以响应式方式观察和修改同一共享状态,而不会破坏视图与其视图模型之间清晰的一对一边界。
服务 (Services)
#服务位于应用的最底层。它们封装 API 端点并公开异步响应对象,如 Future 和 Stream 对象。它们仅用于隔离数据加载,且不持有任何状态。你的应用应为每个数据源配置一个服务类。服务可以封装的端点示例包括:
- 底层平台,如 iOS 和 Android API。
- REST 端点。
- 本地文件。
经验法则:当必要的数据存在于你应用的 Dart 代码之外时(上述每个例子都是如此),使用服务是最有帮助的。
服务和仓库之间是多对多关系。单个仓库可以使用多个服务,而一个服务也可以被多个仓库使用。
可选:领域层 (Domain layer)
#随着应用的增长和功能的增加,你可能需要抽象出那些给视图模型增加过多复杂性的逻辑。这些类通常被称为交互器或用例 (use-cases)。
用例负责使 UI 层和数据层之间的交互更简单且更具可重用性。它们从仓库获取数据,并使其适用于 UI 层。
用例主要用于封装原本会存在于视图模型中的业务逻辑,并满足以下一个或多个条件:
- 需要合并来自多个仓库的数据。
- 逻辑过于复杂。
- 该逻辑将被不同的视图模型重用。
此层是可选的,因为并非所有应用或应用内的所有功能都有这些需求。如果你认为你的应用将从这个额外的层级中受益,请考虑其利弊:
| 优点 | 缺点 |
|---|---|
| ✅ 避免视图模型中的代码重复 | ❌ 增加了架构的复杂性,添加了更多类并提高了认知负荷 |
| ✅ 通过将复杂的业务逻辑与 UI 逻辑分离来提高可测试性 | ❌ 测试需要额外的 Mock 对象 |
| ✅ 提高视图模型中的代码可读性 | ❌ 为你的代码增加了额外的样板代码 |
使用用例 (Use-cases) 进行数据访问
#添加领域层时的另一个考量是:视图模型是否继续直接访问仓库数据,或者你是否强制视图模型通过用例来获取数据?换句话说,你是按需添加用例吗?也许是在你注意到视图模型中出现重复逻辑时?还是说,无论逻辑是否简单,只要视图模型需要数据,你就创建一个用例?
如果你选择后者,会强化上述的利弊。你的应用代码将极其模块化且易于测试,但也会增加大量不必要的开销。
一种好的做法是仅在需要时添加用例。如果你发现视图模型大部分时间都在通过用例访问数据,你总是可以重构代码以完全利用用例。本指南后续使用的示例应用在某些功能中使用了用例,但也拥有直接与仓库交互的视图模型。一个复杂的功能最终看起来可能是这样的:
这种添加用例的方法由以下规则定义:
- 用例依赖于仓库。
- 用例和仓库之间是多对多关系。
- 视图模型依赖于一个或多个用例以及一个或多个仓库。
这种使用用例的方法看起来不像层层叠叠的千层面,而更像是一顿有两道主菜(UI 和数据层)和一道配菜(领域层)的正餐。用例只是具有明确输入和输出的实用类。这种方法灵活且可扩展,但需要更勤勉地维护秩序。
反馈
#由于网站的这一部分正在不断完善中,我们欢迎你的反馈!