应用架构是打造高品质 Android 应用的基础。借助定义明确的架构,您可以创建可扩缩、可维护的应用,该应用能够适应不断扩大的 Android 设备生态系统,包括手机、平板电脑、可折叠设备、ChromeOS 设备、车载显示屏和 XR 设备。
应用组成
典型的 Android 应用由多个应用组件组成,例如 Service、内容提供程序和广播接收器。您需要在应用清单中声明这些组件。
应用的界面也是一个组件。过去,界面是使用多个 activity 构建的。不过,现代应用使用单 activity 架构。单个 Activity 可作为以 fragment 或 Jetpack Compose 目的地实现的屏幕的容器。
多种设备规格
应用可以在多种类型的设备上运行,不仅包括手机,还包括平板电脑、可折叠设备、ChromeOS 设备等。应用不能假定为纵向或横向。配置变更(例如设备旋转或折叠/展开可折叠设备)会强制应用重新组合其界面,从而影响应用数据和状态。
资源限制
移动设备(即使是大屏设备)的资源也有限,因此操作系统可能会随时停止某些应用进程,以便为新的进程腾出空间。
变量启动条件
在资源受限的环境中,应用组件可以不按顺序地单独启动;此外,操作系统或用户可以随时销毁它们。因此,请勿在应用组件中存储任何应用数据或状态。应用组件应保持独立,彼此互不依赖。
常见的架构原则
如果您无法使用应用组件存储应用数据和状态,那么您应该如何设计应用呢?
随着 Android 应用大小不断增加,您定义的架构务必要能允许应用扩缩。精心设计的应用架构定义了应用的各个部分之间的界限以及每个部分应承担的职责。
分离关注点
设计应用架构时应遵循一些特定原则。
最重要的原则是分离关注点。一种常见的错误是在一个 Activity 或 Fragment 中编写所有代码。
Activity 或 Fragment 的主要作用是托管应用的界面。Android 操作系统控制着它们的生命周期,并会根据用户操作(如屏幕旋转)或系统事件(如内存不足)频繁地销毁和重新创建它们。
这种短暂性使得它们不适合保存应用数据或状态。如果您将数据存储在 Activity 或 Fragment 中,那么当重新创建相应组件时,这些数据会丢失。为确保数据持久性并提供稳定的用户体验,请勿将状态委托给这些界面组件。
自适应布局
应用应妥善处理配置变更,例如设备屏幕方向变更或应用窗口大小变更。实现自适应规范化布局,以便在各种设备类型上提供最佳用户体验。
通过数据模型驱动界面
另一个重要原则是您应该通过数据模型驱动界面(最好是持久性模型)。数据模型代表应用的数据。它们独立于应用中的界面元素和其他组件。这意味着它们与界面和应用组件的生命周期没有关联,但仍会在操作系统从内存中移除应用的进程时被销毁。
持久性模型是理想之选,原因如下:
如果 Android 操作系统销毁应用以释放资源,用户不会丢失数据。
当网络连接不稳定或不可用时,应用会继续工作。
以数据模型类为基础构建应用架构,可使应用更稳定可靠、更便于测试。
单一可信来源
在应用中定义新数据类型时,您应为其分配单一数据源 (SSOT)。SSOT 是该数据的所有者,而且只有此 SSOT 可以修改或转变该数据。为了实现这一点,SSOT 会以不可变类型公开数据;而且为了修改数据,SSOT 会公开其他类型可以调用的函数或接收其他类型可以调用的事件。
此模式具有多种优势:
- 将对特定类型数据的所有更改集中到一处
- 保护数据,防止其他类型篡改此数据
- 更易于跟踪对数据的更改,因此更容易发现 bug
在离线优先应用中,应用数据的单一数据源通常是数据库。在其他某些情况下,单一数据源可以是 ViewModel。
单向数据流
单一数据源原则常常与单向数据流 (UDF) 模式一起使用。在 UDF 中,状态仅朝一个方向流动,通常是从父级组件流向子级组件。修改数据流的事件朝相反方向流动。
在 Android 中,状态或数据通常从分区层次结构中较高的分区类型流向较低的分区类型。事件通常在分区层次结构中较低的分区类型触发,直到其到达 SSOT 的相应数据类型。例如,应用数据通常从数据源流向界面。用户事件(例如按钮按下操作)从界面流向 SSOT,在 SSOT 中应用数据被修改并以不可变类型公开。
此模式可以更好地保持数据一致性,不易出错、更易于调试,并且具备 SSOT 模式的所有优势。
推荐的应用架构
基于常见架构原则,每个应用应至少有两个层:
- 界面层:在屏幕上显示应用数据
- 数据层:包含应用的业务逻辑并公开应用数据
您可以额外添加一个名为“网域层”的架构层,以简化和重复使用界面层与数据层之间的交互。
现代应用架构
现代 Android 应用架构采用以下方法(及其他一些方法):
- 自适应分层架构
- 应用的所有层中的单向数据流 (UDF)
- 包含状态容器的界面层,用于管理界面的复杂性
- 协程和数据流
- 依赖项注入最佳实践
如需了解详情,请参阅有关 Android 架构的建议。
界面层
界面层(或呈现层)的作用是在屏幕上显示应用数据。每当数据发生变化时,无论是因为用户互动(例如按了某个按钮),还是因为外部输入(例如网络响应),界面都应随之更新,以反映这些变化。
界面层包含两种类型的结构:
- 在屏幕上呈现数据的界面元素。您可以使用 Jetpack Compose 函数构建这些元素,以支持自适应布局。
- 用于存储数据、向界面提供数据以及处理逻辑的状态容器(如
ViewModel)
对于自适应界面,ViewModel 对象等状态容器会公开可适应不同窗口大小类别的界面状态。您可以使用 currentWindowAdaptiveInfo() 来派生此界面状态。然后,NavigationSuiteScaffold 等组件可以使用此信息,根据可用的屏幕空间在不同的导航模式(例如 NavigationBar、NavigationRail 或 NavigationDrawer)之间自动切换。
如需了解详情,请参阅界面层页面。
数据层
应用的数据层包含业务逻辑。业务逻辑决定应用的价值,它包含决定应用如何创建、存储和更改数据的规则。
数据层由多个仓库组成,其中每个仓库都可以包含零到多个数据源。您应该为应用中处理的每种不同类型的数据分别创建一个存储库类。例如,您可以为与电影相关的数据创建一个 MoviesRepository 类,或者为与付款相关的数据创建一个 PaymentsRepository 类。
存储库类负责以下方面:
- 向应用的其余部分公开数据
- 集中处理数据变化
- 解决多个数据源之间的冲突
- 对应用其余部分的数据源进行抽象化处理
- 包含业务逻辑
每个数据源类应仅负责处理一个数据源,数据源可以是文件、网络来源或本地数据库。 数据源类是应用与数据操作系统之间的桥梁。
如需了解详情,请参阅数据层页面。
网域层
网域层是位于界面层和数据层之间的可选层。
网域层负责封装复杂的业务逻辑,或者由多个视图模型重复使用的简单业务逻辑。网域层是可选的,因为并非所有应用都有这类需求。请仅在需要时使用该层,例如处理复杂逻辑或支持可重用性。
网域层中的类通常称为“用例”或“交互方”。
每个用例都应仅负责单个功能。例如,如果多个视图模型依赖时区在屏幕上显示适当的消息,则您的应用可能具有 GetTimeZoneUseCase 类。
如需了解详情,请参阅网域层页面。
管理组件之间的依赖关系
应用中的类要依赖其他类才能正常工作。您可以使用以下任一设计模式来收集特定类的依赖项:
您可以借助这些模式来扩展代码,因为它们可提供清晰的依赖项管理模式(无需复制代码,也不会增添复杂性)。您还可以借助这些模式在测试和生产实现之间快速切换。
常见的最佳做法
编程是一个创造性的领域,构建 Android 应用也不例外。 无论是在多个 activity 或 fragment 之间传递数据,检索远程数据并将其保留在本地以在离线模式下使用,还是复杂应用遇到的任何其他常见情况,解决问题的方法都会有很多种。
虽然以下建议不是强制性的,但在大多数情况下,遵循这些建议会使您的代码库更强大、可测试性更高且更易维护。
不要将数据存储在应用组件中。
避免将应用的入口点(如 activity、服务和广播接收器)指定为数据源。入口点应只与其他组件协调,以检索与该入口点相关的数据子集。每个应用组件存在的时间都很短暂,具体取决于用户与其设备的交互情况以及系统的容量。
减少对 Android 类的依赖。
您的应用组件应该是唯一依赖于 Android 框架 SDK API(例如 Context 或 Toast)的类。将应用中的其他类与应用组件分离开来有助于改善可测试性,并减少应用中的耦合。
在应用的各个模块之间设定明确的职责界限。
请勿在代码库中将从网络加载数据的代码散布到多个类或软件包中。同样,也不要将不相关的职责(如数据缓存和数据绑定)定义到同一个类中。遵循推荐的应用架构会有所帮助。
尽量少公开每个模块中的代码。
请勿创建会公开内部实现细节的快捷方式。短期内,您可能会省点时间,但随着代码库的不断发展,您可能会反复陷入技术上的麻烦。
专注于应用的独特核心,以使其从其他应用中脱颖而出。
不要一次又一次地编写相同的样板代码,这是在做无用功。 相反,您应将时间和精力集中放在能让应用与众不同的方面上。让 Jetpack 库以及建议的其他库处理重复的样板。
使用规范布局和应用设计模式。
Jetpack Compose 库提供了用于构建自适应界面的强大 API。在应用中使用规范化布局,以优化多种设备类型和显示尺寸上的用户体验。查看应用设计模式库,选择最适合您的使用情形的布局。
在配置更改后保留界面状态。
在设计自适应布局时,请在配置更改(例如显示屏大小调整、折叠和屏幕方向更改)期间保留界面状态。您的架构应验证用户当前状态是否得到维护,从而提供顺畅的体验。
设计可重复使用且可组合的界面组件。
构建可重复使用且可组合的界面组件,以支持自适应设计。 这样一来,您就可以组合和重新排列组件,以适应各种屏幕尺寸和姿势,而无需进行重大重构。
考虑如何使应用的每个部分可独立测试。
如果使用明确定义的 API 从网络获取数据,将会更容易测试在本地数据库中保留该数据的模块。如果您将这两个函数的逻辑混放在一处,或将网络代码分散在整个代码库中,那么即便能够进行测试,难度也会大很多。
类型负责其并发政策。
如果某种类型正在执行长时间运行的阻塞工作,则应负责将该计算移至正确的线程。该类型知道它正在执行的计算类型及其应在哪个线程中执行。类型应该是主线程安全的,这意味着,您可以安全地从主线程调用这些类型而不会阻塞。
保留尽可能多的相关数据和最新数据。
这样,即使用户的设备处于离线模式,他们也可以使用您应用的功能。请记住,并非所有用户都能享受到稳定的高速连接,即使有时可以使用,在比较拥挤的地方网络信号也可能不佳。
架构的优势
在应用中实现良好的架构会为项目和工程团队带来诸多好处:
- 提高整个应用的可维护性、质量和稳健性。
- 允许应用扩缩。尽可能减少代码冲突,使更多人和更多团队可以为同一代码库做贡献。
- 有助于新手上手。架构能使您的项目保持一致性,让团队中的新成员可以快速上手,并在更短时间内提高效率。
- 更易于测试。良好的架构鼓励使用更简单的类型,这些类型通常更易于测试。
- 可以使用明确定义的流程有条理地调查 bug。
在架构方面的投入也会对用户产生直接积极影响。用户能从更稳定的应用中获益;同时,由于工程团队效率提高,用户还可以享受更多功能。不过,架构也需要前期时间投入。建议您阅读这些案例研究,了解其他公司在应用中使用良好架构的成功案例,这有助于您向贵组织其他人解释前期时间投入的必要性。
示例
以下示例展示了良好的应用架构: