Android 官方现代 App 架构解读 - Domain Layer

Android 官方现代 App 架构解读 - Domain Layer

Arch - Domain Layer

Android 官方 App 架构指南系列文章

概述

📌 名词解释

  • Domain Layer:介于 Data LayerUI Layer 之间的可选层,由 UseCase 组成。
  • UseCase:是组成 Domain Layer 的具体执行类,其仅通过一个 public 函数提供能力。

Domain Layer 主要是可以做以下两件事情:

  • 封装复杂的业务逻辑,避免出现大型类
  • 封装多 ViewModel 通用的业务逻辑,避免代码重复

需要回答的问题:

  • 如何定义封装 UseCase 类?
  • UseCase 之间如何交互?
  • UseCase 如何提供数据?

为了使 UseCase 保持简单轻量化,每个 UseCase 都应仅负责单个功能,且不应包含可变数据。

如何定义 UseCase ?

💡 命名规范:

在本指南中,用例以其负责的单一操作命名。具体命名惯例如下:

一般现在时动词 + 名词/内容(可选)+ UseCase

如:FormatDateUseCaseLogOutUserUseCaseGetLatestNewsWithAuthorsUseCase 或 MakeLoginRequestUseCase

在 Kotlin 中,您可以通过使用 operato 修饰符定义 invoke() 函数,将用例类实例作为函数进行调用。请参阅以下示例:

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

这种方式对于对传参及返回值的扩展性都比较好,若采用统一的封装可能会破坏其灵活性,而且会增加一些模板代码。

来自网域层的用例必须具有主线程安全性的*,*也就是可以在主线程上被调用。但是这并不是说要在 Domain 层做线性调度相关的事情,而是由数据的产生方做处理。若 DataLayer 中没有处理线程处理的的,需要在 Domain 中添加一层防护。

UseCase 之间如何交互?

UseCase 通常是调用 Repository 逻辑,也可以对多个 Repository 进行组合调用,也可以调用其他的 UseCase。其依赖关系大致如下:

image.png

UseCase 除了封装重复的代码之外,还可以整合多个 Repository 中的逻辑,比如将 NewsRepositoryAuthorsRepository 组合成一个新的数据列表进行返回。示例代码如下:

/**
 * 将新闻与作者关联起来
 */
class GetLatestNewsWithAuthorsUseCase(
    private val newsRepository: NewsRepository,
    private val authorsRepository: AuthorsRepository,
    private val defaultDispatcher: CoroutineDispatcher = Dispatchers.Default
) {
    suspend operator fun invoke(): List<ArticleWithAuthor> = withContext(defaultDispatcher) {
        newsRepository.fetchLatestNews().map { article ->
            val author = authorsRepository.getAuthor(article.authorId)
            // 整合两种数据
            ArticleWithAuthor(article, author)
        }
    }
}
复制代码

UseCase 如何提供数据 ?

UseCase 处理业务的结果一般是要通过数据的形式通知到调用方,大致会有以下几种方式提供数据。

直接返回

最常见的方式就是直接返回数据了,如下:

class UseCaseNormal(userRepository: UserRepository) {
    operator fun invoke(date: Date): String {
        return userRepository.format(date)
    }
}
复制代码

这种方式对耗时操作而言,可能会存在主线程调用安全的问题。

Suspend 函数

对于 Kotlin 而言可以使用 suspend 函数替代:

class MyUseCaseWithSuspend {

    suspend operator fun invoke(): MyUiState {
        val result: Boolean = doSomething()
        return if (result) {
            MyUiState.Successful("OK")
        } else {
            MyUiState.Error(-1)
        }
    }
}
复制代码

Callback

对于 Kotlin 而言比较常见的方式就是回调了,如将 MyUiState 通过回调的方式通知到调用者:

class MyUseCaseWithCallback {
    
    operator fun invoke(onData: (MyUiState) -> Unit) {
        val result: Boolean = doSomething()
        if(result) {
            onData(MyUiState.Successful("OK"))
        } else {
            onData(MyUiState.Error(-1))
        }
    }
}
复制代码

回调方式不仅可以处理一次性任务,还可以处理多次任务,比如进度条的结果回调。但是回调使用不当可能会导致“回调地狱” 问题。

Flow

Kotlin 中对于多次结果回调可以使用 Flow,如下:

class MyUseCaseWithFlow {

    operator fun invoke(): Flow<MyUiState> = flow {
        emit(MyUiState.Loading)
        val result: Boolean = doSomething()
        if (result) {
            emit(MyUiState.Successful("OK"))
        } else {
            emit(MyUiState.Error(-1))
        }
    }
}
复制代码

上述四种方式可以根据其具体的使用场景自行选择,但是都可以实现的方式,建议按照维护成本由低到高采用,建议顺序如下:直接返回 > suspend 函数 > flow > Callback 。

总结

Domain Layer 主要是可以做以下两件事情:

  • 封装复杂的业务逻辑,避免出现大型类
  • 封装多 ViewModel 通用的业务逻辑,避免代码重复

想要做好上述两件事情,就需要回答的问题:

  • 如何定义封装 UseCase 类?
  • UseCase 之间如何交互?
  • UseCase 如何提供数据?

核心总结如下:

  • 使用 operato + invoke 的方式定义 UseCase,保证其灵活性及扩展性;
  • UseCase 可以依赖 UseCase,也可以依赖一个或多个 Repository
  • UseCase 暴露数据的方式有直接返回、suspend 函数、Callback、Flow,建议尽量避免采用 Callback 形式。

更多内容会第一时间发布在微信公众号中,欢迎大家关注:

扫码_搜索联合传播样式-标准色版.png