MAD:Android Architecture Layer Design

Modern Android Development 系列中,涵盖了从编程语言到开发框架等各个环节, 随着 Kotlin 及 Jetpack 等新技术的出现 Android 开发方式发生了很大变化,以及推出的 Jetpack Compose 更是将这种变化推向了新阶段,Goolge 将这些新技术下的开发方式命名为 MAD ,以此区别于旧有的低效的开发方式。

本文将展开学习 MAD 的 Guide to app architecture 系列课程。

概念

MVX

MVX(MVC/MVP/MVVM):属于一种软件设计框架,将程序逻辑与用户界面分开,有助于组织代码并将程序分离为模块,从而使得开发更高效、模块更易于重用。

Android 中的 Clean Archtecture

Clean Archtecture 是一种软件开发方法,提供一种经济高效的流程来开发性能更好、更容易更改且依赖更少的高质量代码。

clean-architecture

同心圆代表着软件的不同领域,根据抽象原则,最抽象的圆在中间,外圈是具体的实现细节;这套框架最重要的原则是依赖规则,规定着每个圈只能依赖最接近它的内圈,每个圈对外圈的事物没有知情权,这包括函数、类、变量或任何其他命名的软件实体。同时外圈中使用的数据格式不应该被内圆使用,规定外圈的任何东西都不能影响内圈。

Clean Archtecture 的优势

  • 代码比标准 MVX 更易于测试
  • 职责划分与隔离
  • 用户友好的包结构

App Archtecture Layer Design(UI Layer-Domain Layer-Data Layer)

Android官方推荐的架构,更像是 Clean Archtecture 和 MVX(官方选用 MVVM)的结合。

架构指南

在 App 开发的过程中,一个合适的架构应该包含有以下优点:

  • 功能要兼顾灵活的扩展或收缩
  • 稳定性、健壮性
  • 易于测试

所以一个合适的整体架构至少需要包含以下几大设计原则:

分离关注点

分离关注点是解决复杂问题的一种常见的解决方案,它是将一个计算机程序拆分为不同部分的设计原则。每一部分都有自己需要关注的焦点。从而达到将一个复杂问题拆解成多个简单问题的效果。

数据驱动页面

数据是项目的核心,也是状态的最终保存者。而视图,只不过是一种能够由数据延迟计算出来的最终结果而已,它本身不存储状态,View 只需要观察数据变化并做出响应即可。

KISS

KISS 同一问题的不同解决方案中,应该选择简单的那一个

SOLID

SOLD 面向对象变成的一些基本原则

应用架构推荐

App Arch Layer Design

UI Layer

界面层是作用在屏幕上显示应用数据。无论是因为用户交互还是外部输入(例如网络响应)导致数据发生变化时,界面都应更新以反应相应的变化。

界面从数据层获取的应用数据通常不同于所需要显示的信息格式,所以界面层要将应用数据变化转换为界面可以呈现的形式,然后将其显示出来,就呈现了如下流水线的模式:

android-mad-arch-overview-ui

界面层架构

界面层主要由一下两部分组成:

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

通过流程上的细分,界面层主要由以下几个部分组成:

  • ViewModelContainer:View 的容器,作为 View 渲染的入口,一般指 Activity、Fragment
  • UI Element:UI 元素,用来渲染界面的最小单元,View 或者 Jetpack Compose
  • States holders:UI State 的持有者,负责提供界面状态,并且包含执行相应任务所必需的逻辑,一般指 ViewModel
  • event:用户的交互事件,通常需要再产生、更新数据,代理给 ViewModel 处理
  • UiState:UI 的状态数据,用来渲染 UI Element 的数据

定义界面状态

界面是界面状态的直观呈现,界面状态所做的任何更改都会立即反应再界面中。

界面状态的不可变性

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

单向数据流(UDF)

作为提供和管理界面状态的方式。

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

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

为何使用 UDF ?

UDF 可为状态提供周期建模,它还可以将以下位置分离开来:状态变化来源位置、转换位置以及最终使用位置。这种分离可让界面只发挥其名称所表明的作用:通过观察状态变化来显示信息,并通过将这些变化传递给 ViewModel 来传递用户 intent。

UDF 有助于实现以下几个优点:

  • 数据一致性:界面只有一个可信来源
  • 可测试性:状态来源是独立的,以你独立于界面进行测试
  • 可维护性:状态的变更遵循明确定义的模式,即状态更改是用户事件及其数据拉取来源共同作用的结果。

如何定义 UiState ?

UiState 通常会被定义为 data class ,除了包含UI绘制的元素,还会包含一些动作的处理,例如:isUserLoggedIn 根据这个字段处理页面跳转相关逻辑。在定义 UiState 的同时,需要考虑 UI 到底需要展示、处理哪些信息,也有一些原则需要遵守:

  • 不可变性 状态字段要定义为常量而非变量,这样可以杜绝数据在传递过程被其他逻辑对其产生修改

  • 使用统一的命名 功能 + UiState

  • UiState 应处理彼此相关的状态

    data class NewsUiState(
        val isSignedIn: Boolean = false,
        val isPremium: Boolean = false,
        val newsItems: List<NewsItemUiState> = listOf()
    )
      
    val NewsUiState.canBookmarkNews: Boolean get() = isSignedIn && isPremium
    
  • 合理使用单数据流和多个数据流

UI Events

在定义 UI Events 时需区分UI事件和用户事件,UI事件在 UI Elements 中处理而无需进一步分发,用户事件需要分发给对应的 State Holders。

Domain Layer

领域层,也称网域层,是位于界面层和数据层之间的可选层。Domain Layer 主要由不同的 UseCase 组成,其依赖关系如下:

领域层负责封装复杂的业务逻辑,或者由多个 ViewModel 重复使用的简单业务逻辑。并非所有应用都有这类需求,所以应仅在需要时使用,例如处理复杂逻辑或支持可重用性。又或者,由于某 ViewModel 业务逻辑涉及多个 Repositories 并且可能会变得很复杂,因此可以创建 UseCase 类,将逻辑从 ViewModel 中提取出来并提高其可读性。

Domain Layer 的优势

  • 避免代码重复
  • 改善使用领域层类的可读性
  • 改善应用的可测试性
  • 更好的划分职责,避免出现大型类

UseCase 用例类

UseCase 是组成 Domain Layer 的具体执行类。用例类通常依赖于 Repositories 类,并且它们在界面层的通信方式与 Repositories 的通信方式相同(Java回调、Kotlin协程)。

为了使用例类保持简单轻量化,每个用例都应仅负责单个功能,且不应包含可变数据。(可变数据应在界面层或数据层中 处理)

某些情况下,用例类中可能存在的逻辑可以成为 Util 类中静态方法的一部分,不过,不建议采用后者,因为 Util 类通常很难找,而且其功能也很难发现。此外,用例还可以共享通用功能(例如积累中的线程处理和错误处理)。

如何定义 UseCase?

在 Kotlin中,可以通过使用 operator 修饰符定义 invoke() 函数,将用例类实例作为函数进行调用。

class FormatDateUseCase(userRepository: UserRepository) {
    
    // 1、operator + invoke 来提供统一函数入口
    // 2、传参及返回值可以自由定义
    operator fun invoke(date: Date): String {
        return userRepository.format(date)
    }
}

生命周期

用例类没有自己的生命周期,而是受限于使用它们的类。由于用例类不应包含可变数据,因此每次将用例类作为依赖项传递时,都应该创建一个新实例。

线程处理

来自领域层的用例类应具有主线程安全性,如果用例类执行长期运行的阻塞操作,那么它们负责将改逻辑移至合适当的线程。

class MyUseCase(
	private val defaultDispatcher: CoroutineDispatcher = Dispatcher.Defalut
) {
  suspend operator fun invoke(...) = withContext(defalutDispatcher) {
    // Long-running blocking operations happen on a background thread.
  }
}

Data Layer

数据层包含应用数据和业务逻辑。业务逻辑决定应用的价值,它由现实世界的业务规则组成,这些规则决定着应用数据的创建、存储和更改方式。

这种关注点分离使得数据层可用于多个屏幕、在应用的不同部分之间共享信息,以及在界面以外复制业务逻辑以进行单元测试。

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

Repositories

Repositories 类主要负责以下任务:

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

Data Source

Data Source 类是应用与负责数据操作的系统之间的桥梁。相应的数据源可以是 File、Netwrok、DataBase等。

命名规则:数据类型 + 来源类型 + DataSource 例如:NewsRemoteDataSource

多层 Repositories

在某些涉及更复杂业务要求的情况下,存储库可能需要依赖于其他存储库。这可能是因为所涉及的数据是来自多个数据源的数据聚合,或者是因为相应职责需要封装在其他存储库类中。

可信来源

为了提供离线优先支持,建议使用本地数据源(例如数据库)作为可信来源

数据操作类型

数据层可以处理的操作类型会因操作的重要程度而异:

  • 面向界面的操作 面向界面的操作仅在用户位于特定屏幕上时才相关,当用户离开相应屏幕时便会被取消。例如,显示从数据库获取的部分数据。 面向界面的操作通常由界面层触发,并且遵循调用方的生命周期,例如 ViewModel 的生命周期。
  • 面向应用的操作 只要应用处于打开状态,面向应用的操作就一直相关。如果应用关闭或进程终止,这些操作将会被取消。例如,缓存网络请求结果,以便在以后需要时使用。 这些操作通常遵循 Application 类或数据层的生命周期。
  • 面向业务的操作 面向业务的操作无法取消。它们应该会在进程终止后继续执行。例如,完成上传用户想要发布到其个人资料的照片。 对于面向业务的操作,建议使用 WorkManager。

总结

参考文献:

The Clean Architecture

Android 官方现代 App 架构解读-系列

Android 应用架构指南