应用架构指南

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

移动应用用户体验

一个典型的 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 中修改应用程序数据并在不可变类型中公开。

这种模式可以更好地保证数据一致性,减少错误,更易于调试,并带来 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 元素。您可以使用 View 或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 示例演示了良好的应用架构。请探索它们以了解这些指南在实践中的应用