跳转至正文

应用架构指南

构建 Flutter 应用的推荐架构方式。

以下页面演示如何按最佳实践构建应用。本指南建议适用于大多数应用,使其更易扩展、测试与维护。但它们是指导原则而非铁律,你应根据自身需求调整。

本节从宏观概述 Flutter 应用如何架构,说明应用各层及层内存在的类。其后一节提供具体代码示例,并 walkthrough 一个落实这些建议的 Flutter 应用。

项目结构概览

#

设计 Flutter 应用时,关注点分离 是最重要的原则。 Flutter 应用应划分为两大层:UI 层与数据层。

每一层再拆分为不同组件,各自职责、接口、边界与依赖清晰。本指南建议将应用拆分为以下组件:

  • View(视图)

  • View model(视图模型)

  • Repository(仓库)

  • Service(服务)

MVVM

#

MVVM

#

若你见过 Model-View-ViewModel 架构模式(MVVM),会对此感到熟悉。 MVVM 将应用的一个功能拆为三部分:ModelViewModelView。 View 与 view model 构成应用的 UI 层;仓库与服务代表应用数据,即 MVVM 的 model 层。下一节将定义这些组件。

MVVM architectural pattern

应用中每个功能通常包含:一个描述 UI 的 view、一个处理逻辑的 view model、一个或多个作为应用数据单一数据源的仓库,以及零个或多个与客户端服务器、平台插件等外部 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 设计模式,由两个架构组件构成:

  • View 描述如何向用户呈现应用数据,具体指构成某一功能的 widget 组合。例如,view 常常是(但不总是)包含 Scaffold 及 widget 树中其下所有 widget 的屏幕。 View 还负责将用户交互产生的事件传递给 view model。

  • View model 包含将应用数据转换为 UI State 的逻辑,因为仓库数据格式常与展示所需格式不同。例如,你可能需要合并多个仓库的数据,或过滤数据记录列表。

View 与 view model 应为一一对应关系。

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

简而言之,view model 管理 UI 状态,view 展示该状态。借助 view 与 view model,UI 层可在配置变更(如屏幕旋转)时保持状态,且可将 UI 逻辑与 Flutter widget 分开测试。

应用功能以用户为中心,因此由 UI 层定义。每一对 viewview model 定义应用中的一个功能,通常是应用中的一个屏幕,但未必如此。例如登录与登出:登录通常在专用屏幕上完成,该屏幕唯一目的是提供登录方式;在应用代码中,登录屏幕由 LoginViewModelLoginView 类构成。

另一方面,登出通常不在专用屏幕完成,而是以菜单、账户屏幕等多处按钮呈现,且常出现在多个位置。此时可有仅含一个可嵌入其他 widget 的按钮的 LogoutViewModelLogoutView

Views

#

View

#

在 Flutter 中,view 是应用的 widget 类,是渲染 UI 的主要方式,不应包含业务逻辑。渲染所需数据应全部由 view model 传入。

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

View 仅应包含以下逻辑:

  • 根据 view model 中的标志或可空字段,用简单 if 语句显示或隐藏 widget

  • 动画逻辑

  • 基于屏幕尺寸、方向等设备信息的布局逻辑

  • 简单路由逻辑

所有与数据相关的逻辑都应在 view model 中处理。

View models

#

View model

#

View model 暴露渲染 view 所需的应用数据。在本页描述的架构中,Flutter 应用的大部分逻辑位于 view model。

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

View model 的主要职责包括:

  • 从仓库获取应用数据并转换为适合在 view 中展示的格式,例如过滤、排序或聚合数据。

  • 维护 view 所需的当前状态,使 view 重建时不丢失数据;例如包含用于条件渲染 widget 的布尔标志,或跟踪轮播当前区块的字段。

  • 向 view 暴露可挂到事件处理器(如按钮点击、表单提交)的回调(称为 command)。

Command 得名于 命令模式,是允许 view 在不了解实现细节的情况下执行复杂逻辑的 Dart 函数。 Command 作为 view model 类的成员编写,由 view 类中的手势处理器调用。

可在 应用架构案例研究UI 层 部分查看 view、view model 与 command 的示例。

若要温和入门 Flutter 中的 MVVM,请参阅 状态管理基础

数据层

#

应用的数据层处理业务数据与逻辑。数据层由 service 与仓库两部分构成,应有清晰的输入输出以便复用与测试。

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

用 MVVM 术语说,service 与仓库构成 model 层

仓库

#

Repository(仓库) 类是 model 数据的单一数据源,负责从 service 轮询数据并将原始数据转换为 领域模型(domain model)。领域模型表示应用所需数据,格式可供 view model 消费。应用中每种不同数据类型应有一个仓库类。

仓库处理与 service 相关的业务逻辑,例如:

  • 缓存

  • 错误处理

  • 重试逻辑

  • 刷新数据

  • 轮询 service 获取新数据

  • 根据用户操作刷新数据

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

仓库以领域模型形式输出应用数据。例如,社交应用可有 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 端点并暴露 FutureStream 等异步响应对象;仅用于隔离数据加载,不持有状态。每个数据源应有一个 service 类。 Service 可能封装的端点示例包括:

  • 底层平台,如 iOS 与 Android API

  • REST 端点

  • 本地文件

经验法则:当所需数据位于应用 Dart 代码之外时,service 最有用——上述示例皆属此类。

Service 与仓库为多对多关系:单个仓库可使用多个 service,一个 service 也可被多个仓库使用。

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

可选:领域层

#

随应用增长与功能增加,你可能需要将过多复杂逻辑从 view model 中抽象出来,这类类常称为 interactor 或用例(use-case)

用例负责简化并复用 UI 层与数据层之间的交互,从仓库取数并整理为 UI 层可用形式。

MVVM design pattern with an added domain layer object

用例主要用于封装否则会放在 view model 中、且满足以下一项或多项条件的业务逻辑:

  1. 需要合并多个仓库的数据

  2. 极其复杂

  3. 逻辑将被不同 view model 复用

该层是可选的,因为并非所有应用或应用内功能都有这些需求。若你认为应用会受益于这一额外层,请权衡利弊:

ProsCons
✅ 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 直接与仓库交互。复杂功能最终可能如下:

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

这种添加用例的方式遵循以下规则:

  • 用例依赖仓库

  • 用例与仓库为多对多关系

  • View model 依赖一个或多个用例以及一个或多个仓库

这种用例用法看起来不像分层千层面,而像一盘有两道主菜(UI 层与数据层)和配菜(领域层)的晚餐。用例只是输入输出清晰的工具类。该方式灵活可扩展,但需更用心维护秩序。

反馈

#

网站本节内容仍在完善中, 欢迎提供反馈