应用架构指南

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

移动应用用户体验

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

请记住,移动设备也是资源受限的,因此操作系统随时可能杀死一些应用进程以腾出空间用于新的进程。

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

常见的架构原则

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

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

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

关注点分离

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

请记住,您不拥有ActivityFragment的实现;相反,它们只是代表 Android 操作系统与您的应用之间的契约的粘合类。操作系统可以根据用户交互或由于低内存等系统条件随时销毁它们。为了提供满意的用户体验和更易于管理的应用维护体验,最好最大限度地减少对它们的依赖。

从数据模型驱动 UI

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

持久化模型非常适合以下原因

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

  • 当网络连接不稳定或不可用时,你的应用程序可以继续运行。

如果你将应用程序架构建立在数据模型类上,你就可以使你的应用程序更易于测试和更健壮。

单一数据源

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

这种模式带来很多好处

  • 它将对特定类型数据的更改集中在一个地方。
  • 它保护数据,以防止其他类型篡改它。
  • 它使数据的更改更易于追踪。因此,更容易发现错误。

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

单向数据流

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

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

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

本节演示如何按照推荐的最佳实践构建你的应用程序。

考虑到上一节中提到的常见架构原则,每个应用程序至少应该有两层

  • UI 层,它在屏幕上显示应用程序数据。
  • 数据层,它包含应用程序的业务逻辑,并公开应用程序数据。

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

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)。
  • 具有状态持有者的 UI 层,用于管理 UI 的复杂性。
  • 协程和流。
  • 依赖注入最佳实践。

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

UI 层

UI 层(或表示层)的作用是在屏幕上显示应用程序数据。无论何时数据发生变化,无论是由于用户交互(如按下按钮)还是外部输入(如网络响应),UI 应该更新以反映这些变化。

UI 层由两部分组成

  • 在屏幕上呈现数据的 UI 元素。你可以使用视图或Jetpack Compose 函数构建这些元素。
  • 状态持有者(如ViewModel 类),它们持有数据,将其公开给 UI,并处理逻辑。
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. UI 层在应用程序架构中的作用。

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

数据层

应用程序的数据层包含业务逻辑。业务逻辑是赋予你的应用程序价值的东西——它由规则组成,这些规则决定了你的应用程序如何创建、存储和更改数据。

数据层由资源库组成,每个资源库可以包含零到多个数据源。你应该为应用程序中处理的每种不同类型的数据创建一个资源库类。例如,你可能会创建一个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. 数据层在应用程序架构中的作用。

资源库类负责以下任务

  • 将数据公开给应用程序的其余部分。
  • 集中对数据的更改。
  • 解决多个数据源之间的冲突。
  • 从应用程序的其余部分抽象出数据源。
  • 包含业务逻辑。

每个数据源类应该负责只与一个数据源交互,该数据源可以是文件、网络源或本地数据库。数据源类是应用程序与数据操作系统之间的桥梁。

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

域层

域层是一个可选的层,位于 UI 和数据层之间。

域层负责封装复杂的业务逻辑,或由多个 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 应用程序也不例外。解决问题有很多方法;你可能会在多个活动或片段之间通信数据,检索远程数据并将其持久保存到本地以用于离线模式,或者处理非平凡应用程序遇到的许多其他常见场景。

虽然以下推荐不是强制性的,但在大多数情况下,遵循它们会使你的代码库在长期内更健壮、更易于测试和维护

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

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

减少对 Android 类的依赖关系。

你的应用程序组件应该是唯一依赖于 Android 框架 SDK API(如ContextToast)的类。将应用程序中的其他类从它们抽象出来有助于可测试性,并减少应用程序中的耦合

在应用程序中的各个模块之间创建明确定义的责任边界。

例如,不要将从网络加载数据的代码分散到代码库中的多个类或包中。类似地,不要在同一个类中定义多个无关的责任(如数据缓存和数据绑定)。遵循推荐的应用程序架构 将有助于你实现这一点。

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

例如,不要试图创建一个快捷方式,该快捷方式会公开模块中的内部实现细节。你可能会在短期内获得一些时间,但随着代码库的不断发展,你很可能会产生许多倍的技术债务。

专注于应用程序的独特核心,使其脱颖而出。

不要通过一遍又一遍地编写相同的样板代码来重新发明轮子。相反,将你的时间和精力集中在使你的应用程序独一无二的地方,并让 Jetpack 库和其他推荐的库来处理重复的样板代码。

考虑如何使应用程序的每个部分都可以独立地进行测试。

例如,为从网络获取数据创建一个明确定义的 API,可以更容易地测试将该数据持久保存到本地数据库的模块。相反,如果你在一个地方混合了这两个模块的逻辑,或者将网络代码分散到整个代码库中,那么有效地进行测试就变得更加困难——如果不是不可能的话。

类型负责其并发策略。

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

持久保存尽可能多的相关和最新数据。

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

架构的优势

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

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

对架构进行投资也会直接影响您的用户。由于工程团队的工作效率更高,他们将受益于更稳定的应用和更多功能。但是,架构也需要前期时间投入。为了帮助您向公司其他部门证明这段时间的价值,请查看这些案例研究,其中其他公司分享了他们在应用中采用良好架构后的成功故事。

示例

以下 Google 示例演示了良好的应用架构。您可以探索它们以实际了解这些指南。