Android 架构设计学习

常见的架构原则

应用架构定义了应用的各个部分之间的界限以及每个部分应承担的职责。所以应当按照某些特定原则设计应用架构。

分离关注点

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

注意:Activity 和 Fragment 只是表示 Android 操作系统与应用之间关系的粘合类。操作系统可能会根据用户互动或因内存不足等系统条件随时销毁它们。所以最好尽量减少对它们的依赖。

通过数据模型驱动界面

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

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

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

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

单一数据源

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

此模式具有多种优势:

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

单向数据流

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

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

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

合理的应用架构

每个应用可以有三个层:

  • 界面层 - 在屏幕上显示应用数据。
  • 数据层 - 包含应用的业务逻辑并公开应用数据。
  • 网域层 - 以简化和重复使用界面层与数据层之间的交互。(网域层依赖于数据层类)

 现代应用架构

现代应用架构鼓励采用以下方法:

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

界面层

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

不过,从数据层获取的应用数据的格式通常不同于需要显示的信息的格式。例如,可能只需要在界面中显示部分数据,或者可能需要合并两个不同的数据源,以便提供切合用户需求的信息。无论应用的是什么逻辑,都需要向界面传递完全呈现界面所需的所有信息。界面层是一个流水线,负责将应用数据变化转换为界面可以呈现的形式,然后将其显示出来。

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

  • 在屏幕上呈现数据的界面元素。您可以使用 View 或 Jetpack Compose 函数构建这些元素。
  • 用于存储数据、向界面提供数据以及处理逻辑的状态容器(如 ViewModel 类)。

界面层架构

“界面”这一术语是指用于显示数据的 activity 和 fragment 等界面元素,无论它们使用哪个 API(Views 还是 Jetpack Compose)来显示数据。由于数据层的作用是存储和管理应用数据,以及提供对应用数据的访问权限,因此界面层必须执行以下步骤:

  1. 使用应用数据,并将其转换为界面可以轻松呈现的数据。
  2. 使用界面可呈现的数据,并将其转换为用于向用户呈现的界面元素。
  3. 使用来自这些组合在一起的界面元素的用户输入事件,并根据需要反映它们对界面数据的影响。
  4. 根据需要重复第 1-3 步。

如何实现用于执行这些步骤的界面层:

  • 如何定义界面状态。
  • 单向数据流 (UDF),作为提供和管理界面状态的方式。
  • 如何根据 UDF 原则使用可观察数据类型公开界面状态。
  • 如何实现使用可观察界面状态的界面。

如果界面是相对用户而言的,那么界面状态就是相对应用而言的。这就像同一枚硬币的两面,界面是界面状态的直观呈现。对界面状态所做的任何更改都会立即反映在界面中。

界面元素与界面状态绑定在一起的结果:

 举例:将完全呈现界面所需的信息封装在如下定义的 NewsUiState 数据类中:

data class NewsUiState(
    val isSignedIn: Boolean = false,
    val isPremium: Boolean = false,
    val newsItems: List<NewsItemUiState> = listOf(),
    val userMessages: List<Message> = listOf()
)

data class NewsItemUiState(
    val title: String,
    val body: String,
    val bookmarked: Boolean = false,
    ...
)
  • 不可变性

示例中的界面状态定义是不可变的。这样的主要好处是,不可变对象可保证即时提供应用的状态。
这样一来,界面便可专注于发挥单一作用:读取状态并相应地更新其界面元素。因此,切勿直接在界面中修改界面状态,除非界面本身是其数据的唯一来源。违反这个原则会导致同一条信息有多个可信来源,从而导致数据不一致和轻微的 bug。

要点:只有数据源或数据所有者才应负责更新其公开的数据。

界面状态类命名惯例

界面状态类是根据其描述的屏幕或部分屏幕的功能命名的。具体命名惯例如下:

功能 + UiState。

例如,用于显示新闻的屏幕的状态可以称为 NewsUiState,新闻报道列表中的新闻报道的状态可以为 NewsItemUiState。

使用单向数据流管理状态

界面状态是呈现界面所需的详细信息的不可变快照。不过,应用中数据的动态特性意味着状态可能会随时间而变化。这可能是因为用户互动,也可能是因为其他事件修改了用于填充应用的底层数据。

这些互动可以受益于处理它们的 mediator,从而定义要为每个事件应用的逻辑,并对后备数据源执行必要的转换,以便创建界面状态。这些互动及其逻辑可以位于界面本身中,但随着界面开始担任其名称所表明的角色以外的角色(数据所有者、提供方、转换器等),这可能很快就会变得难以掌控。此外,这可能会影响可测试性,因为生成的代码是紧密耦合的代码,没有可辨别的边界。归根结底,界面能够受益于减轻的负担。除非界面状态非常简单,否则界面的唯一职责应该是使用和显示界面状态。

单向数据流 (UDF),这是一种架构模式,有助于强制实施这种健康的职责分离。

状态容器

符合以下条件的类称为状态容器:负责提供界面状态,并且包含执行相应任务所必需的逻辑。状态容器有多种大小,具体取决于所管理的界面元素的作用域(从底部应用栏等单个微件,到整个屏幕或导航目的地,不一而足)。

在后一种情况下,典型的实现是 ViewModel 的实例,不过根据应用的要求,使用简单的类可能就足够了。例如,案例研究中的“新闻”应用使用 NewsViewModel 类作为状态容器,以便为该部分显示的屏幕画面提供界面状态。

要点:ViewModel 类型是推荐的实现,用于管理屏幕级界面状态,具有数据层访问权限。此外,它会在配置发生变化后自动继续存在。ViewModel 类用于定义要为应用中的事件应用的逻辑,并提供更新后的状态作为结果。

可以通过多种方式为界面与其状态提供方之间的互相依赖关系建模。不过,由于界面与其 ViewModel 类之间的互动在很大程度上可以理解为事件输入及其随后的状态输出,因此这种关系可以按下图所示来表示:

状态向下流动、事件向上流动的这种模式称为单向数据流 (UDF)。这种模式对应用架构的影响如下:

  • ViewModel 会存储并公开界面要使用的状态。界面状态是经过 ViewModel 转换的应用数据。
  • 界面会向 ViewModel 发送用户事件通知。
  • ViewModel 会处理用户操作并更新状态。
  • 更新后的状态将反馈给界面以进行呈现。
  • 系统会对导致状态更改的所有事件重复上述操作。

作为状态提供方,ViewModel 的职责是定义所有必需的逻辑,以便填充界面状态中的所有字段,并处理界面完全呈现所需的事件。

 数据层

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

数据层由多个存储库组成,其中每个存储库都可以包含零到多个数据源。应该为应用中处理的每种不同类型的数据分别创建一个存储库类。

存储库类负责以下任务:

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

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

网域层

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

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

常见的最佳实践

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

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

减少对 Android 类的依赖。

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

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

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

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

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

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

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

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

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

类型负责其并发政策。

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

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

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

猜你喜欢

转载自blog.csdn.net/zhangying1994/article/details/130170254