应用架构指南

本指南涵盖了构建稳健、高质量应用的最佳实践和推荐架构

移动应用用户体验

典型的 Android 应用包含多个应用组件,包括 activityfragment服务内容提供器广播接收器。您在应用清单中声明了大部分这些应用组件。然后,Android 操作系统使用此文件来决定如何将您的应用集成到设备的整体用户体验中。鉴于典型的 Android 应用可能包含多个组件,并且用户经常在短时间内与多个应用交互,应用需要适应不同类型的用户驱动型工作流和任务。

请记住,移动设备资源有限,因此在任何时候,操作系统都可能终止某些应用进程,为新进程腾出空间。

鉴于这种环境条件,您的应用组件可能会单独启动且顺序不确定,并且操作系统或用户可能随时销毁它们。由于这些事件不在您的控制范围内,因此您不应在应用组件中存储或保留任何应用数据或状态,并且您的应用组件不应相互依赖。

通用架构原则

如果您不应该使用应用组件来存储应用数据和状态,那么应该如何设计您的应用呢?

随着 Android 应用规模的增长,定义一个能够让应用扩展、提高应用稳健性并使其更易于测试的架构非常重要。

应用架构定义了应用各部分之间的界限以及每个部分应承担的职责。为了满足上述需求,您应该设计您的应用架构以遵循一些特定原则。

关注点分离

最重要的原则是关注点分离。将所有代码都写在 ActivityFragment 中是一个常见的错误。这些基于界面的类应该只包含处理界面和操作系统交互的逻辑。通过使这些类尽可能精简,您可以避免许多与组件生命周期相关的问题,并提高这些类的可测试性。

请记住,您不拥有 ActivityFragment 的实现;它们只是连接 Android OS 和您的应用之间的粘合类。操作系统可以根据用户交互或系统条件(例如内存不足)随时销毁它们。为了提供令人满意的用户体验和更易于管理的应用维护体验,最好最大限度地减少对它们的依赖。

通过数据模型驱动界面

另一个重要原则是,您应该通过数据模型(最好是持久性模型)驱动界面。数据模型代表应用的数据。它们独立于应用中的界面元素和其他组件。这意味着它们不与界面和应用组件生命周期绑定,但在操作系统决定从内存中移除应用进程时,它们仍会被销毁。

持久性模型是理想的选择,原因如下:

  • 如果 Android OS 销毁您的应用以释放资源,您的用户不会丢失数据。

  • 在网络连接不稳定或不可用的情况下,您的应用仍能继续运行。

如果您的应用架构基于数据模型类,您的应用将更易于测试且更稳健。

单一可信来源

当在应用中定义新的数据类型时,您应为其分配一个单一可信来源 (SSOT)。SSOT 是该数据的*所有者*,并且只有 SSOT 可以修改或改变它。为了实现这一点,SSOT 使用不可变类型公开数据,并且为了修改数据,SSOT 公开函数或接收其他类型可以调用的事件。

此模式带来多项优势:

  • 它将特定类型数据的所有更改集中在一个位置。
  • 它保护数据,使其他类型无法篡改。
  • 它使数据更改更易于追踪。因此,更易于发现错误。

在离线优先应用中,应用数据的真相来源通常是数据库。在其他一些情况下,真相来源可以是 ViewModel 甚至界面。

单向数据流

单一可信来源原则经常在我们的指南中与单向数据流 (UDF) 模式结合使用。在 UDF 中,状态仅沿一个方向流动。修改数据的事件沿相反方向流动。

在 Android 中,状态或数据通常从层次结构中作用域更高的类型流向作用域更低的类型。事件通常从作用域更低的类型触发,直到它们到达相应数据类型的 SSOT。例如,应用数据通常从数据源流向界面。用户事件(例如按钮按下)从界面流向 SSOT,其中应用数据被修改并以不可变类型公开。

此模式更好地保证了数据一致性,更不容易出错,更易于调试,并带来了 SSOT 模式的所有好处。

本部分演示了如何按照推荐的最佳实践构建应用。

考虑到上一节中提到的通用架构原则,每个应用都应至少包含两个层:

  • 在屏幕上显示应用数据的界面层
  • 包含应用业务逻辑并公开应用数据的数据层

您可以添加一个名为网域层的额外层,以简化和重用界面层和数据层之间的交互。

In a typical app architecture, the UI layer gets the application data
    from the data layer or from the optional domain layer, which sits between
    the UI layer and the data layer.
图 1. 典型应用架构图。

现代应用架构

这种现代应用架构鼓励使用以下技术(其中包括):

  • 响应式分层架构。
  • 应用所有层中的单向数据流 (UDF)。
  • 带有状态容器的界面层,用于管理界面的复杂性。
  • 协程和流。
  • 依赖注入最佳实践。

如需了解更多信息,请参阅以下部分、目录中的其他架构页面以及包含最重要最佳实践摘要的推荐页面

界面层

界面层(或表示层)的作用是在屏幕上显示应用数据。无论数据何时更改(无论是由于用户交互,例如按下按钮;还是外部输入,例如网络响应),界面都应更新以反映这些更改。

界面层由两部分组成:

  • 在屏幕上呈现数据的界面元素。您可以使用 View 或 Jetpack Compose 函数构建这些元素。
  • 状态容器(例如 ViewModel 类),它们保存数据,将其公开给界面,并处理逻辑。
In a typical architecture, the UI layer's UI elements depend on state
    holders, which in turn depend on classes from either the data layer or the
    optional domain layer.
图 2. 界面层在应用架构中的作用。

如需了解有关此层的更多信息,请参阅界面层页面

数据层

应用的数据层包含业务逻辑。业务逻辑是为您的应用提供价值的内容 - 它由确定您的应用如何创建、存储和更改数据的规则组成。

数据层由仓库组成,每个仓库可以包含零个或多个数据源。您应该为您应用中处理的每种不同类型的数据创建一个仓库类。例如,您可以为电影相关数据创建一个 MoviesRepository 类,或者为支付相关数据创建一个 PaymentsRepository 类。

In a typical architecture, the data layer's repositories provide data
    to the rest of the app and depend on the data sources.
图 3. 数据层在应用架构中的作用。

仓库类负责以下任务:

  • 向应用的其他部分公开数据。
  • 集中数据更改。
  • 解决多个数据源之间的冲突。
  • 将数据源与应用的其他部分抽象化。
  • 包含业务逻辑。

每个数据源类都应负责仅处理一个数据源,该数据源可以是文件、网络源或本地数据库。数据源类是应用和系统之间进行数据操作的桥梁。

要了解有关此层的更多信息,请参阅数据层页面

网域层

网域层是一个可选层,位于界面层和数据层之间。

网域层负责封装复杂的业务逻辑,或由多个 ViewModel 重用的简单业务逻辑。此层是可选的,因为并非所有应用都具有这些要求。您应仅在需要时使用它,例如处理复杂性或优先考虑可重用性。

When it is included, the optional domain layer provides dependencies to
    the UI layer and depends on the data layer.
图 4. 网域层在应用架构中的作用。

此层中的类通常称为用例交互器。每个用例都应负责单一功能。例如,如果多个 ViewModel 依赖时区在屏幕上显示适当的消息,您的应用可以有一个 GetTimeZoneUseCase 类。

如需了解有关此层的更多信息,请参阅网域层页面

管理组件之间的依赖关系

应用中的类依赖于其他类才能正常运行。您可以使用以下任一设计模式来收集特定类的依赖项:

  • 依赖注入 (DI):依赖注入允许类定义其依赖项而无需构建它们。在运行时,另一个类负责提供这些依赖项。
  • 服务定位器:服务定位器模式提供一个注册表,类可以在其中获取其依赖项,而不是构建它们。

这些模式允许您扩展代码,因为它们提供了明确的模式来管理依赖项,而无需复制代码或增加复杂性。此外,这些模式允许您在测试和生产实现之间快速切换。

我们建议遵循依赖项注入模式,并在 Android 应用中使用 Hilt 库Hilt 通过遍历依赖项树自动构建对象,提供对依赖项的编译时保证,并为 Android 框架类创建依赖项容器。

一般最佳实践

编程是一个创意领域,构建 Android 应用也不例外。解决问题的方法有很多;您可以在多个 Activity 或 Fragment 之间传递数据,检索远程数据并将其本地持久化以用于离线模式,或者处理非平凡应用遇到的其他常见场景。

尽管以下建议并非强制性,但在大多数情况下,遵循它们可以使您的代码库在长期内更稳健、更易于测试和维护:

不要将数据存储在应用组件中。

避免将应用的入口点(例如 activity、服务和广播接收器)指定为数据源。相反,它们应该只与其他组件协调以检索与该入口点相关的数据子集。每个应用组件的生命周期都相当短,具体取决于用户与设备的交互以及系统的整体当前健康状况。

减少对 Android 类的依赖。

您的应用组件应该是唯一依赖 Android 框架 SDK API(例如 ContextToast)的类。将应用中的其他类与它们抽象化有助于提高可测试性并减少应用内部的耦合

在应用的各个模块之间创建明确的职责边界。

例如,不要将从网络加载数据的代码分散到代码库中的多个类或软件包中。同样,不要在同一个类中定义多个不相关的职责,例如数据缓存和数据绑定。遵循推荐的应用架构将帮助您实现这一点。

尽可能少地公开每个模块的内容。

例如,不要试图创建公开模块内部实现细节的快捷方式。短期内您可能会节省一些时间,但随着代码库的发展,您很可能会多次承担技术债务。

专注于您应用的独特核心,使其在其他应用中脱颖而出。

不要重复造轮子,一遍又一遍地编写相同的样板代码。相反,将时间和精力集中在让您的应用独一无二的方面,让 Jetpack 库和其他推荐库处理重复的样板代码。

考虑如何使应用的每个部分都能独立测试。

例如,拥有一个定义明确的 API 用于从网络获取数据,可以更容易地测试将数据持久化到本地数据库的模块。如果将这两个模块的逻辑混合在一个地方,或者将网络代码分散到整个代码库中,那么进行有效测试就会变得困难得多,甚至不可能。

类型负责其并发策略。

如果一个类型正在执行长时间运行的阻塞工作,它应该负责将该计算转移到正确的线程。该特定类型知道它正在进行的计算类型以及它应该在哪个线程中执行。类型应该是主线程安全的,这意味着它们可以安全地从主线程调用而不会阻塞它。

尽可能持久化相关且最新的数据。

这样,即使设备处于离线模式,用户也可以享受应用的功能。请记住,并非所有用户都享有持续、高速的连接——即使他们有,也可能在拥挤的地方收到不良信号。

架构的优势

在您的应用中实现良好的架构会为项目和工程团队带来许多好处:

  • 它提高了整个应用的可维护性、质量和稳健性。
  • 它允许应用扩展。更多的人和团队可以以最少的代码冲突为同一个代码库做出贡献。
  • 它有助于新人入职。由于架构为您的项目带来了一致性,团队的新成员可以快速上手并在更短的时间内提高效率。
  • 更易于测试。良好的架构鼓励更简单的类型,这些类型通常更易于测试。
  • 可以有条不紊地通过明确定义的流程调查错误。

对架构的投入也直接影响您的用户。他们受益于更稳定的应用程序,以及由于更高效的工程团队而带来的更多功能。但是,架构也需要前期时间投入。为了帮助您向公司其他成员说明这些时间的合理性,请查看这些案例研究,了解其他公司在应用中拥有良好架构时的成功经验。

示例

以下 Google 示例展示了良好的应用架构。请深入探索它们,以了解这些指南的实际应用: