应用架构指南

使用集合让一切井井有条 根据您的偏好保存内容并对其进行分类。

本指南包含一些最佳做法和推荐架构,有助于构建强大而优质的应用。

移动应用用户体验

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

请注意,移动设备的资源也很有限,因此操作系统可能随时终止某些应用进程以便为新的进程腾出空间。

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

常见的架构原则

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

随着 Android 应用大小不断增加,您定义的架构务必要能允许应用扩缩、提升应用的稳健性并且方便对应用进行测试。

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

分离关注点

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

请注意,您并非拥有 ActivityFragment 的实现;它们只是表示 Android 操作系统与应用之间关系的粘合类。操作系统可能会根据用户互动或因内存不足等系统条件随时销毁它们。为了提供令人满意的用户体验和更易于管理的应用维护体验,最好尽量减少对它们的依赖。

通过数据模型驱动界面

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

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

  • 如果 Android 操作系统销毁应用以释放资源,用户不会丢失数据。

  • 当网络连接不稳定或不可用时,应用会继续工作。

如果您的应用架构以数据模型类为基础,您的应用会更便于测试、更稳定可靠。

单一数据源

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

此模式具有多种优势:

  • 将对特定类型数据的所有更改集中到一处。
  • 保护数据,防止其他类型篡改此数据。
  • 更易于跟踪对数据的更改。因此,更容易发现 bug。

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

单向数据流

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

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

此模式可以更好地保证数据一致性,不易出错、更易于调试,并且具备 SSOT 模式的所有优势。

本部分将演示如何按照建议的最佳做法构建应用。

基于上一部分提到的常见架构原则,每个应用应至少有两个层:

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

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

在典型的应用架构中,界面层会从数据层或可选网域层(位于界面层和数据层之间)获取应用数据。
图 1. 典型应用架构的示意图。

界面层

界面层(或呈现层)的作用是在屏幕上显示应用数据。每当数据发生变化时,无论是因为用户互动(例如按了某个按钮),还是因为外部输入(例如网络响应),界面都应随之更新,以反映这些变化。

界面层由以下两部分组成:

  • 在屏幕上呈现数据的界面元素。您可以使用 View 或 Jetpack Compose 函数构建这些元素。
  • 用于存储数据、向界面提供数据以及处理逻辑的状态容器(如 ViewModel 类)。
在典型架构中,界面层的界面元素依赖于状态容器,而状态容器又依赖于来自数据层或可选网域层的类。
图 2.界面层在应用架构中的作用。

如需详细了解此层,请参阅界面层页面

数据层

应用的数据层包含业务逻辑。业务逻辑决定应用的价值,它包含决定应用如何创建、存储和更改数据的规则。

数据层由多个代码库组成,其中每个代码库可包含零到多个数据源。您应该为应用中处理的每种不同类型的数据分别创建一个存储库类。例如,您可以为与电影相关的数据创建一个 MoviesRepository 类,或者为与付款相关的数据创建一个 PaymentsRepository 类。

在典型架构中,数据层的代码库会向应用的其余部分提供数据,而这些代码库则依赖于数据源。
图 3. 数据层在应用架构中的作用。

代码库类负责以下任务:

  • 向应用的其余部分公开数据。
  • 集中处理数据变化。
  • 解决多个数据源之间的冲突。
  • 对应用其余部分的数据源进行抽象化处理。
  • 包含业务逻辑。

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

如需详细了解此层,请参阅数据层页面

网域层

网域层是位于界面与数据层之间的可选层。

网域层负责封装复杂的业务逻辑,或者由多个 ViewModel 重复使用的简单业务逻辑。此层是可选的,因为并非所有应用都有这类需求。请仅在需要时使用该层,例如处理复杂逻辑或支持可重用性。

如果添加了此层,则该可选网域层会向界面层提供依赖项,而它自身依赖于数据层。
图 4.网域层在应用架构中的作用。

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

如需详细了解此层,请参阅网域层页面

管理组件之间的依赖关系

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

  • 依赖注入 (DI):依赖注入使类能够定义其依赖项而不构造它们。在运行时,另一个类负责提供这些依赖项。
  • 服务定位器:服务定位器模式提供了一个注册表,类可以从中获取其依赖项而不构造它们。

您可以借助这些模式来扩展代码,因为它们可提供清晰的依赖项管理模式(无需复制代码,也不会增添复杂性)。 此外,您还可以借助这些模式在测试和生产实现之间快速切换。

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

常见的最佳做法

编程是一个创造性的领域,构建 Android 应用也不例外。 无论是在多个 Activity 或 Fragment 之间传递数据,检索远程数据并将其保留在本地以在离线模式下使用,还是复杂应用遇到的任何其他常见情况,解决问题的方法都会有很多种。

虽然以下建议不是强制性的,但在大多数情况下,遵循这些建议会使您的代码库更强大、可测试性更高且更易维护:

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

请避免将应用的入口点(如 Activity、Service 和广播接收器)指定为数据源。相反,您应只将其与其他组件协调,以检索与该入口点相关的数据子集。每个应用组件存在的时间都很短暂,具体取决于用户与其设备的交互情况以及系统当前的整体运行状况。

减少对 Android 类的依赖

您的应用组件应该是唯一依赖于 Android 框架 SDK API (例如 ContextToast)的类。将应用中的其他类与这些类分离开来有助于改善可测试性,并减少应用中的耦合

在应用的各个模块之间设定明确定义的职责界限。

例如,请勿在代码库中将从网络加载数据的代码散布到多个类或软件包中。同样,也不要将不相关的职责(如数据缓存和数据绑定)定义到同一个类中。遵循推荐的应用架构可以帮助您解决此问题。

尽量少公开每个模块中的代码。

例如,请勿试图创建从模块提供内部实现细节的快捷方式。短期内,您可能会省点时间,但随着代码库的不断发展,您可能会反复陷入技术上的麻烦。

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

不要一次又一次地编写相同的样板代码,这是在做无用功。 相反,您应将时间和精力集中放在能让应用与众不同的方面上,并让 Jetpack 库以及建议的其他库处理重复的样板。

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

例如,如果使用明确定义的 API 从网络获取数据,将会更容易测试在本地数据库中保留该数据的模块。如果您将这两个模块的逻辑混放在一处,或将网络代码分散在整个代码库中,那么即便能够进行有效测试,难度也会大很多。

类型负责其并发政策

如果某种类型正在执行长时间运行的阻塞工作,则应负责将该计算移至正确的线程。该特定类型知道它正在执行的计算类型及其应在哪个线程中执行。类型应该具有主线程安全性,这意味着,您可以安全地从主线程调用这些类型而不会阻塞。

保留尽可能多的相关数据和最新数据

这样,即使用户的设备处于离线模式,他们也可以使用您应用的功能。请记住,并非所有用户都能享受到稳定的高速连接 - 即使有时可以使用,在比较拥挤的地方网络信号也可能不佳。

架构的优势

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

  • 提高整个应用的可维护性、质量和稳健性。
  • 允许应用扩缩。尽可能减少代码冲突,使更多人和更多团队可以为同一代码库做贡献。
  • 有助于新手上手。架构能使您的项目保持一致性,让团队中的新成员可以快速上手,并在更短时间内提高效率。
  • 更易于测试。良好的架构鼓励使用更简单的类型,这些类型通常更易于测试。
  • 可以使用明确定义的流程有条理地调查 bug。

在架构方面的投入也会对您的用户产生直接积极影响。用户能从更稳定的应用中获益;同时,由于工程团队效率提高,用户还可以享受更多功能。但是,架构也需要前期时间投入。建议您阅读这些案例研究,了解其他公司在应用中使用良好架构的成功案例,这有助于您向所在公司解释前期时间投入的必要性。

示例

以下 Google 示例展示了良好的应用架构。您不妨浏览一下它们,了解如何实际运用本指南: