应用架构指南
以下页面演示了如何使用最佳实践构建应用。本指南中的建议适用于大多数应用,使它们更容易扩展、测试和维护。但是,它们是指南,而不是一成不变的规则,您应该根据您的独特需求对其进行调整。
本节提供 Flutter 应用架构的高级概述。它解释了应用的各层,以及每层中存在的类。下一节将提供具体的代码示例,并逐步介绍一个实现了这些建议的 Flutter 应用。
项目结构概述
#在设计 Flutter 应用时,关注点分离 是最重要的原则。您的 Flutter 应用应该分成两个主要层:UI 层和数据层。
每个层进一步细分为不同的组件,每个组件都具有不同的职责、明确定义的接口、边界和依赖项。本指南建议您将应用划分为以下组件
- 视图
- 视图模型
- 存储库
- 服务
MVVM
#如果您遇到过模型-视图-视图模型设计模式 (MVVM),您会发现它很熟悉。MVVM 是一种设计模式,它将应用的一个功能分成三个部分:Model
、ViewModel
和 View
。视图和视图模型构成应用的 UI 层。存储库和服务表示应用的数据,或 MVVM 的模型层。每个组件将在下一节中定义。
应用中的每个功能都将包含一个视图来描述 UI 和一个视图模型来处理逻辑,一个或多个存储库作为应用数据的真相来源,以及零个或多个与外部 API(如客户端服务器和平台插件)交互的服务。
应用的单个功能可能需要以下所有对象
在本页结束时,将对这些对象及其连接的箭头进行全面解释。在本指南中,以下该图的简化版本将用作锚点。
UI 层
#应用的 UI 层负责与用户交互。它向用户显示应用的数据并接收用户输入,例如点击事件和表单输入。
UI 对数据更改或用户输入做出反应。当 UI 从存储库接收新数据时,它应重新渲染以显示新数据。当用户与 UI 交互时,它应更改以反映该交互。
UI 层由两个架构组件组成,基于 MVVM 设计模式
- 视图描述如何向用户呈现应用数据。具体来说,它指的是构成一个功能的*小部件组合*。例如,视图通常(但并非总是)是一个具有
Scaffold
小部件以及小部件树中所有下方小部件的屏幕。视图还负责响应用户交互将事件传递给视图模型。 - 视图模型包含将应用数据转换为UI 状态的逻辑,因为来自存储库的数据格式通常与需要显示的数据不同。例如,您可能需要组合来自多个存储库的数据,或者可能想要过滤数据记录列表。
视图和视图模型应该具有 1:1 的关系。
简单来说,视图模型管理 UI 状态,而视图显示该状态。使用视图和视图模型,您的 UI 层可以在配置更改(例如屏幕旋转)期间维护状态,并且您可以独立于 Flutter 小部件测试 UI 的逻辑。
应用的一个功能以用户为中心,因此由 UI 层定义。视图和视图模型对的每个实例都在您的应用中定义了一个功能。这通常是您应用中的一个屏幕,但它不必是屏幕。例如,考虑登录和注销。登录通常在特定屏幕上完成,该屏幕的唯一目的是为用户提供登录方式。在应用代码中,登录屏幕将由一个LoginViewModel
类和一个LoginView
类组成。
另一方面,注销应用通常不会在专用屏幕上完成。注销功能通常以菜单、用户帐户屏幕或任何其他多个位置的按钮形式呈现给用户。它通常在多个位置呈现。在这种情况下,您可能有一个LogoutViewModel
和一个LogoutView
,它只包含一个可以放入其他小部件的按钮。
视图
#在 Flutter 中,视图是应用的小部件类。视图是渲染 UI 的主要方法,不应包含任何业务逻辑。它们应该从视图模型传递渲染所需的所有数据。
视图应该包含的唯一逻辑是
- 基于视图模型中的标志或可空字段显示和隐藏小部件的简单 if 语句
- 动画逻辑
- 基于设备信息的布局逻辑,例如屏幕尺寸或方向。
- 简单的路由逻辑
所有与数据相关的逻辑都应在视图模型中处理。
视图模型
#视图模型公开渲染视图所需的应用数据。在本页描述的架构设计中,Flutter 应用中的大部分逻辑都位于视图模型中。
视图模型的主要职责包括
- 从存储库检索应用数据并将其转换为适合在视图中呈现的格式。例如,它可能过滤、排序或聚合数据。
- 维护视图中所需的当前状态,以便视图可以在不丢失数据的情况下重建。例如,它可能包含布尔标志以有条件地渲染视图中的小部件,或跟踪屏幕上活动轮播部分的字段。
- 向视图公开回调(称为命令),这些回调可以附加到事件处理程序,例如按钮按下或表单提交。
命令以命令模式命名,是 Dart 函数,允许视图执行复杂逻辑而无需了解其实现。命令被编写为视图模型类的成员,以便由视图类中的手势处理程序调用。
您可以在UI 层部分的应用架构案例研究中找到视图、视图模型和命令的示例。
有关 Flutter 中 MVVM 的入门介绍,请查看状态管理基础知识。
数据层
#应用的数据层处理您的业务数据和逻辑。两个架构部分构成了数据层:服务和存储库。这些部分应具有明确定义的输入和输出,以简化其可重用性和可测试性。
使用 MVVM 语言,服务和存储库构成了您的模型层。
存储库
#存储库类是模型数据的真相来源。它们负责从服务轮询数据,并将原始数据转换为领域模型。领域模型表示应用所需的数据,并以视图模型类可以使用的格式进行格式化。对于应用中处理的每种不同类型的数据,都应该有一个存储库类。
存储库处理与服务相关的业务逻辑,例如
- 缓存
- 错误处理
- 重试逻辑
- 刷新数据
- 轮询服务以获取新数据
- 根据用户操作刷新数据
存储库将应用数据输出为领域模型。例如,社交媒体应用可能有一个UserProfileRepository
类,它公开一个Stream<UserProfile?>
,每当用户登录或注销时,它都会发出一个新值。
存储库输出的模型由视图模型使用。存储库和视图模型具有多对多的关系。视图模型可以使用多个存储库来获取所需的数据,而存储库可以被多个视图模型使用。
存储库永远不应该彼此了解。如果您的应用具有需要来自两个存储库的数据的业务逻辑,则应在视图模型或领域层中组合数据,尤其是在存储库到视图模型的关系很复杂的情况下。
服务
#服务位于应用的最低层。它们包装 API 端点并公开异步响应对象,例如Future
和Stream
对象。它们仅用于隔离数据加载,并且不保存任何状态。您的应用应每个数据源有一个服务类。服务可能包装的端点示例包括
- 底层平台,如 iOS 和 Android API
- REST 端点
- 本地文件
根据经验,当必要数据位于应用的 Dart 代码之外时,服务最有用——这适用于前面提到的每个示例。
服务和存储库具有多对多的关系。单个存储库可以使用多个服务,而服务可以被多个存储库使用。
可选:领域层
#随着应用的增长和功能的增加,您可能需要抽象出为视图模型添加过多复杂性的逻辑。这些类通常称为交互器或用例。
用例负责使 UI 和数据层之间的交互更简单、更易于重用。它们获取存储库中的数据并使其适合 UI 层。
用例主要用于封装原本位于视图模型中的业务逻辑,并满足以下一个或多个条件
- 需要合并来自多个存储库的数据
- 极其复杂
- 逻辑将被不同的视图模型重用
此层是可选的,因为并非所有应用或应用中的所有功能都具有这些要求。如果您怀疑您的应用将受益于此额外层,请考虑其优缺点
优点 | 缺点 |
---|---|
✅避免视图模型中的代码重复 | ❌增加架构的复杂性,添加更多类并增加认知负荷 |
✅通过将复杂的业务逻辑与 UI 逻辑分离来提高可测试性 | ❌测试需要额外的模拟 |
✅提高视图模型的代码可读性 | ❌为代码添加额外的样板文件 |
使用用例进行数据访问
#添加领域层时,另一个需要考虑的问题是视图模型是否将继续直接访问存储库数据,或者您是否会强制视图模型通过用例来获取数据。换句话说,您是否会根据需要添加用例?也许当您注意到视图模型中重复的逻辑时?或者,每当视图模型需要数据时,您是否都会创建一个用例,即使用例中的逻辑很简单?
如果你选择后者,它会加剧前面概述的优缺点。你的应用程序代码将非常模块化且易于测试,但它也会增加大量不必要的开销。
一个好的方法是在需要时才添加用例。如果你发现你的视图模型大部分时间都通过用例访问数据,你可以随时重构你的代码以专门使用用例。本指南后面使用的示例应用程序对某些功能使用了用例,但也有一些视图模型直接与存储库交互。一个复杂的功能最终可能看起来像这样
这种添加用例的方法由以下规则定义
- 用例依赖于存储库
- 用例和存储库之间存在多对多关系
- 视图模型依赖于一个或多个用例和一个或多个存储库
这种使用用例的方法最终看起来不太像分层的千层面,更像是一盘包含两道主菜(UI 层和数据层)和一道配菜(领域层)的晚餐。用例只是具有明确定义的输入和输出的实用程序类。这种方法灵活且可扩展,但需要更大的努力来保持秩序。
反馈
#由于本网站的这一部分正在不断发展,我们欢迎您的反馈!
除非另有说明,否则本网站上的文档反映了 Flutter 的最新稳定版本。页面上次更新于 2024-12-04。 查看源代码 或 报告问题.