应用架构指南
接下来的页面将展示如何使用最佳实践来构建一个应用。本指南中的建议可以应用于大多数应用,使它们更容易扩展、测试和维护。然而,它们是指导原则,而不是严格的规则,您应该根据自己的独特需求进行调整。
本节提供了一个 Flutter 应用如何进行架构的高层概览。它解释了应用的分层以及每层中存在的类。下一节将提供具体的代码示例,并逐步介绍一个实现了这些建议的 Flutter 应用。
项目结构概览
#关注点分离 是设计 Flutter 应用时最重要的原则。您的 Flutter 应用应该分为两个大的层面:UI 层和数据层。
每一层又进一步划分为不同的组件,每个组件都有明确的职责、定义良好的接口、边界和依赖关系。本指南建议您将应用划分为以下组件:
- 视图
- ViewModel
- Repository
- Service
MVVM
#如果您遇到过 Model-View-ViewModel (MVVM) 架构模式,那么这会很熟悉。MVVM 是一种架构模式,它将应用程序的一个功能分为三个部分:Model
、ViewModel
和 View
。View 和 ViewModel 构成了应用程序的 UI 层。Repository 和 Service 代表应用程序的数据,或者 MVVM 中的 Model 层。这些组件中的每一个都在下一节中定义。

应用程序中的每个功能都将包含一个用于描述 UI 的 View,一个用于处理逻辑的 ViewModel,一个或多个 Repository 作为应用程序数据的真相来源,以及零个或多个与外部 API(如客户端服务器和平台插件)交互的 Service。
应用程序的单个功能可能需要以下所有对象:

页面结束时,我们将详细解释这些对象以及连接它们的箭头。在本指南中,将使用该图的简化版本作为参考。

UI 层
#应用程序的 UI 层负责与用户交互。它将应用程序的数据展示给用户,并接收用户输入,例如点击事件和表单输入。
UI 会对数据更改或用户输入做出响应。当 UI 从 Repository 接收到新数据时,它应该重新渲染以显示新数据。当用户与 UI 交互时,它应该发生变化以反映该交互。
UI 层由两个架构组件组成,基于 MVVM 设计模式:
- View 描述了如何将应用程序数据呈现给用户。具体来说,它们指的是构成一个功能的Widget 组合。例如,一个 View 通常(但不总是)是一个屏幕,它有一个
Scaffold
Widget,以及 Widget 树中它下面的所有 Widget。View 还负责响应用户交互,将事件传递给 ViewModel。 - ViewModel 包含将应用程序数据转换为UI 状态的逻辑,因为来自 Repository 的数据通常与需要显示的数据格式不同。例如,您可能需要合并来自多个 Repository 的数据,或者您可能想过滤数据记录列表。
View 和 ViewModel 应该是一对一的关系。

最简单的说法是,ViewModel 管理 UI 状态,View 显示该状态。使用 View 和 ViewModel,您的 UI 层可以在配置更改(如屏幕旋转)期间维护状态,并且您可以独立于 Flutter Widget 测试 UI 的逻辑。
应用程序的一个功能是以用户为中心的,因此由 UI 层定义。成对的View 和 ViewModel 的每个实例定义了您应用中的一个功能。这通常是您应用中的一个屏幕,但不必如此。例如,考虑登录和退出。登录通常在一个特定的屏幕上完成,该屏幕的唯一目的是为用户提供登录方式。在应用程序代码中,登录屏幕将由一个 LoginViewModel
类和一个 LoginView
类组成。
另一方面,退出应用通常不是在一个专用屏幕上完成的。退出应用的功能通常以菜单中的按钮、用户帐户屏幕或任何其他不同位置的形式呈现给用户。它通常在多个位置呈现。在这种情况下,您可能有一个 LogoutViewModel
和一个 LogoutView
,它只包含一个可以放入其他 Widget 中的按钮。
视图
#在 Flutter 中,View 是应用程序的 Widget 类。View 是渲染 UI 的主要方法,不应包含任何业务逻辑。它们应该从 ViewModel 接收所有渲染所需的数据。

View 应该包含的唯一逻辑是:
- 简单的 if 语句,根据 ViewModel 中的标志或可空字段来显示和隐藏 Widget。
- 动画逻辑。
- 基于设备信息(如屏幕大小或方向)的布局逻辑。
- 简单的路由逻辑。
所有与数据相关的逻辑都应在 ViewModel 中处理。
ViewModel
#ViewModel 暴露了渲染 View 所需的应用程序数据。在本页面描述的架构设计中,您 Flutter 应用程序中的大部分逻辑都位于 ViewModel 中。

ViewModel 的主要职责包括:
- 从 Repository 检索应用程序数据,并将其转换为适合在 View 中呈现的格式。例如,它可能会过滤、排序或聚合数据。
- 维护 View 所需的当前状态,以便 View 可以在不丢失数据的情况下重建。例如,它可能包含布尔标志来有条件地渲染 View 中的 Widget,或者一个字段来跟踪屏幕上轮播图的哪个部分是活动的。
- 向 View 公开回调(称为 **Commands**),可以将其附加到事件处理程序,如按钮点击或表单提交。
Commands 以 Command 模式命名,是 Dart 函数,允许 Views 在不了解其实现的情况下执行复杂逻辑。Commands 被编写为 ViewModel 类中的成员,供 View 类中的手势处理程序调用。
您可以在 应用架构案例研究 的 UI 层 部分找到 View、ViewModel 和 Command 的示例。
要温和地了解 Flutter 中的 MVVM,请查看 状态管理基础知识。
数据层
#应用的数据层处理您的业务数据和逻辑。数据层由两部分架构组成:Service 和 Repository。这些部分应具有定义明确的输入和输出,以简化其可重用性和可测试性。

使用 MVVM 语言,Service 和 Repository 构成了您的Model 层。
Repository
#Repository 类是您的 Model 数据的真相来源。它们负责从 Service 拉取数据,并将原始数据转换为领域模型 (domain models)。领域模型代表应用程序所需的数据,并以 View Model 类可以消费的方式进行格式化。您的应用中处理的每种不同类型的数据都应该有一个 Repository 类。
Repository 处理与 Service 相关的业务逻辑,例如:
- 缓存
- 错误处理
- 重试逻辑
- 数据刷新
- 轮询 Service 获取新数据
- 基于用户操作刷新数据

Repository 输出应用程序数据为领域模型。例如,一个社交媒体应用可能有一个 UserProfileRepository
类,它公开一个 Stream<UserProfile?>
,每当用户登录或注销时就会发出一个新值。
Repository 输出的模型被 ViewModel 消费。Repository 和 ViewModel 之间存在多对多关系。一个 ViewModel 可以使用多个 Repository 来获取它需要的数据,一个 Repository 也可以被多个 ViewModel 使用。
Repository 之间永远不应该相互了解。如果您的应用程序的业务逻辑需要来自两个 Repository 的数据,您应该在 ViewModel 或领域层中组合数据,特别是如果您的 Repository-to-ViewModel 关系很复杂。
Service
#Service 位于您应用程序的最底层。它们封装 API 端点并公开异步响应对象,如 Future
和 Stream
对象。它们仅用于隔离数据加载,并且不包含状态。您的应用应该为每个数据源都有一个 Service 类。Service 可能封装的端点示例包括:
- 底层平台,如 iOS 和 Android API。
- REST 端点。
- 本地文件。
经验法则,当所需数据位于您的应用程序的 Dart 代码之外时,Service 最有帮助——前面所有示例都是如此。
Service 和 Repository 之间存在多对多关系。单个 Repository 可以使用多个 Service,一个 Service 可以被多个 Repository 使用。

可选:领域层
#随着您的应用程序的增长和功能的增加,您可能需要抽象掉那些给 ViewModel 带来过多复杂性的逻辑。这些类通常称为 Interactor 或Use-case。
Use-case 负责使 UI 层和数据层之间的交互更简单、更可重用。它们从 Repository 获取数据,并使其适用于 UI 层。

Use-case 主要用于封装原本会留在 ViewModel 中且满足以下一个或多个条件的业务逻辑:
- 需要合并来自多个 Repository 的数据。
- 非常复杂。
- 该逻辑将被不同的 ViewModel 重用。
这一层是可选的,因为并非所有应用程序或应用程序中的功能都有这些要求。如果您认为您的应用程序会从这个额外的层中受益,请考虑其优缺点:
优点 | 缺点 |
---|---|
✅ 避免 ViewModel 中的代码重复。 | ❌ 增加了架构的复杂性,增加了更多的类和更高的认知负荷。 |
✅ 通过将复杂的业务逻辑与 UI 逻辑分离来提高可测试性。 | ❌ 测试需要额外的 Mock。 |
✅ 提高 ViewModel 中代码的可读性。 | ❌ 为您的代码增加了额外的样板代码。 |
通过 Use-case 进行数据访问
#添加领域层的另一个考虑因素是 ViewModel 是否将继续直接访问 Repository 数据,还是您将强制 ViewModel 通过 Use-case 获取数据。换句话说,您是根据需要添加 Use-case 吗?也许当您注意到 ViewModel 中存在重复逻辑时?或者,您是每次 ViewModel 需要数据时都创建一个 Use-case,即使 Use-case 中的逻辑很简单?
如果您选择后者,它会加剧前面概述的优缺点。您的应用程序代码将高度模块化和可测试,但它也会增加大量的开销。
一个好的方法是仅在需要时添加 Use-case。如果您发现您的 ViewModel 大部分时间都通过 Use-case 访问数据,您可以随时重构代码以专门使用 Use-case。本指南后面使用的示例应用程序为某些功能提供了 Use-case,但也让 ViewModel 直接与 Repository 交互。一个复杂的功能最终可能看起来像这样:
这种添加 Use-case 的方法由以下规则定义:
- Use-case 依赖于 Repository。
- Use-case 和 Repository 之间存在多对多关系。
- ViewModel 依赖于一个或多个 Use-case *和* 一个或多个 Repository。
这种使用 Use-case 的方法看起来不像分层的千层面,更像是一盘餐盘,有两道主菜(UI 层和数据层)和一个配菜(领域层)。Use-case 只是具有定义明确的输入和输出的实用工具类。这种方法是灵活且可扩展的,但它需要更高的严谨性来保持秩序。
反馈
#随着本网站部分的不断发展,我们欢迎您的反馈!