应用架构指南
构建 Flutter 应用的推荐架构方式。
以下页面演示如何按最佳实践构建应用。本指南建议适用于大多数应用,使其更易扩展、测试与维护。但它们是指导原则而非铁律,你应根据自身需求调整。
本节从宏观概述 Flutter 应用如何架构,说明应用各层及层内存在的类。其后一节提供具体代码示例,并 walkthrough 一个落实这些建议的 Flutter 应用。
项目结构概览
#设计 Flutter 应用时,关注点分离 是最重要的原则。 Flutter 应用应划分为两大层:UI 层与数据层。
每一层再拆分为不同组件,各自职责、接口、边界与依赖清晰。本指南建议将应用拆分为以下组件:
View(视图)
View model(视图模型)
Repository(仓库)
Service(服务)
MVVM
#MVVM
#
若你见过 Model-View-ViewModel 架构模式(MVVM),会对此感到熟悉。
MVVM 将应用的一个功能拆为三部分:Model、ViewModel 与 View。
View 与 view model 构成应用的 UI 层;仓库与服务代表应用数据,即 MVVM 的 model 层。下一节将定义这些组件。
应用中每个功能通常包含:一个描述 UI 的 view、一个处理逻辑的 view model、一个或多个作为应用数据单一数据源的仓库,以及零个或多个与客户端服务器、平台插件等外部 API 交互的服务。
单个应用功能可能需要以下全部对象:
本页末尾将详细解释这些对象及其连接箭头。贯穿本指南,将以下述简化图作为锚点。
UI 层
#应用的 UI 层负责与用户交互:向用户展示应用数据,并接收点击事件、表单输入等用户输入。
UI 会对数据变化或用户输入做出反应。当 UI 从仓库收到新数据时,应重新渲染以展示新数据;当用户与 UI 交互时,UI 应变化以反映该交互。
UI 层基于 MVVM 设计模式,由两个架构组件构成:
-
View 描述如何向用户呈现应用数据,具体指构成某一功能的 widget 组合。例如,view 常常是(但不总是)包含
Scaffold及 widget 树中其下所有 widget 的屏幕。 View 还负责将用户交互产生的事件传递给 view model。 -
View model 包含将应用数据转换为 UI State 的逻辑,因为仓库数据格式常与展示所需格式不同。例如,你可能需要合并多个仓库的数据,或过滤数据记录列表。
View 与 view model 应为一一对应关系。
简而言之,view model 管理 UI 状态,view 展示该状态。借助 view 与 view model,UI 层可在配置变更(如屏幕旋转)时保持状态,且可将 UI 逻辑与 Flutter widget 分开测试。
应用功能以用户为中心,因此由 UI 层定义。每一对 view 与 view model 定义应用中的一个功能,通常是应用中的一个屏幕,但未必如此。例如登录与登出:登录通常在专用屏幕上完成,该屏幕唯一目的是提供登录方式;在应用代码中,登录屏幕由 LoginViewModel 与 LoginView 类构成。
另一方面,登出通常不在专用屏幕完成,而是以菜单、账户屏幕等多处按钮呈现,且常出现在多个位置。此时可有仅含一个可嵌入其他 widget 的按钮的 LogoutViewModel 与 LogoutView。
Views
#View
#在 Flutter 中,view 是应用的 widget 类,是渲染 UI 的主要方式,不应包含业务逻辑。渲染所需数据应全部由 view model 传入。
View 仅应包含以下逻辑:
-
根据 view model 中的标志或可空字段,用简单 if 语句显示或隐藏 widget
动画逻辑
-
基于屏幕尺寸、方向等设备信息的布局逻辑
简单路由逻辑
所有与数据相关的逻辑都应在 view model 中处理。
View models
#View model
#View model 暴露渲染 view 所需的应用数据。在本页描述的架构中,Flutter 应用的大部分逻辑位于 view model。
View model 的主要职责包括:
-
从仓库获取应用数据并转换为适合在 view 中展示的格式,例如过滤、排序或聚合数据。
-
维护 view 所需的当前状态,使 view 重建时不丢失数据;例如包含用于条件渲染 widget 的布尔标志,或跟踪轮播当前区块的字段。
-
向 view 暴露可挂到事件处理器(如按钮点击、表单提交)的回调(称为 command)。
Command 得名于 命令模式,是允许 view 在不了解实现细节的情况下执行复杂逻辑的 Dart 函数。 Command 作为 view model 类的成员编写,由 view 类中的手势处理器调用。
可在 应用架构案例研究 的 UI 层 部分查看 view、view model 与 command 的示例。
若要温和入门 Flutter 中的 MVVM,请参阅 状态管理基础。
数据层
#应用的数据层处理业务数据与逻辑。数据层由 service 与仓库两部分构成,应有清晰的输入输出以便复用与测试。
用 MVVM 术语说,service 与仓库构成 model 层。
仓库
#Repository(仓库) 类是 model 数据的单一数据源,负责从 service 轮询数据并将原始数据转换为 领域模型(domain model)。领域模型表示应用所需数据,格式可供 view model 消费。应用中每种不同数据类型应有一个仓库类。
仓库处理与 service 相关的业务逻辑,例如:
缓存
错误处理
重试逻辑
刷新数据
轮询 service 获取新数据
根据用户操作刷新数据
仓库以领域模型形式输出应用数据。例如,社交应用可有 UserProfileRepository,暴露 Stream<UserProfile?>,在用户登录或登出时发出新值。
仓库输出的模型由 view model 消费。仓库与 view model 为多对多关系:一个 view model 可使用多个仓库获取数据,一个仓库也可被多个 view model 使用。
仓库彼此不应感知。若业务逻辑需要两个仓库的数据,应在 view model 或领域层合并数据,尤其在仓库与 view model 关系复杂时。
管理应用级会话状态
#仓库是应用数据的单一数据源,也是管理应用级生命周期状态的理想位置—— 需在多个 view model 间共享但不应超出当前应用会话持久化的状态。
应用级生命周期状态的例子包括活跃用户会话、内存数据缓存或临时应用设置。由于 view model 与仓库为多对多关系,多个 view model 可依赖同一仓库实例(通常通过 service locator 或依赖注入容器管理),使不同功能通过仓库暴露的 stream 与方法响应式观察并修改同一共享状态,同时不破坏 view 与其 view model 之间清晰的一对一边界。
Services
#Service
#
Service 位于应用最底层,封装 API 端点并暴露 Future、Stream 等异步响应对象;仅用于隔离数据加载,不持有状态。每个数据源应有一个 service 类。
Service 可能封装的端点示例包括:
底层平台,如 iOS 与 Android API
REST 端点
本地文件
经验法则:当所需数据位于应用 Dart 代码之外时,service 最有用——上述示例皆属此类。
Service 与仓库为多对多关系:单个仓库可使用多个 service,一个 service 也可被多个仓库使用。
可选:领域层
#随应用增长与功能增加,你可能需要将过多复杂逻辑从 view model 中抽象出来,这类类常称为 interactor 或用例(use-case)。
用例负责简化并复用 UI 层与数据层之间的交互,从仓库取数并整理为 UI 层可用形式。
用例主要用于封装否则会放在 view model 中、且满足以下一项或多项条件的业务逻辑:
需要合并多个仓库的数据
极其复杂
逻辑将被不同 view model 复用
该层是可选的,因为并非所有应用或应用内功能都有这些需求。若你认为应用会受益于这一额外层,请权衡利弊:
| Pros | Cons |
|---|---|
| ✅ Avoid code duplication in view models | ❌ Increases complexity of your architecture, adding more classes and higher cognitive load |
| ✅ Improve testability by separating complex business logic from UI logic | ❌ Testing requires additional mocks |
| ✅ Improve code readability in view models | ❌ Adds additional boilerplate to your code |
| 优点 | 缺点 |
|---|---|
| ✅ 避免 view model 中的代码重复 | ❌ 增加架构复杂度,类更多、认知负担更高 |
| ✅ 将复杂业务逻辑与 UI 逻辑分离,提升可测试性 | ❌ 测试需要额外 mock |
| ✅ 提升 view model 代码可读性 | ❌ 增加样板代码 |
通过用例访问数据
#增设领域层时还需考虑:view model 是否仍可直接访问仓库数据,还是强制通过用例获取数据。换言之,是按需添加用例(例如在 view model 中发现重复逻辑时),还是每次 view model 需要数据都创建用例,即使用例逻辑很简单?
若选择后者,会放大前述利弊:代码将极度模块化且可测试,但也会带来大量不必要开销。
较好做法是仅在需要时添加用例。若发现 view model 大多通过用例访问数据,可随时重构为完全通过用例。本指南后续示例应用部分功能有用例,也有 view model 直接与仓库交互。复杂功能最终可能如下:
这种添加用例的方式遵循以下规则:
用例依赖仓库
用例与仓库为多对多关系
-
View model 依赖一个或多个用例以及一个或多个仓库
这种用例用法看起来不像分层千层面,而像一盘有两道主菜(UI 层与数据层)和配菜(领域层)的晚餐。用例只是输入输出清晰的工具类。该方式灵活可扩展,但需更用心维护秩序。
反馈
#网站本节内容仍在完善中, 欢迎提供反馈!
除非另有说明,本文档之所提及适用于 Flutter 3.44.0 版本。本页面最后更新时间:2026-06-04。查看文档源码 或者 为本页面内容提出建议。