从UseCase的理念看Android当前的应用架构设计指南

No silver bullet.(没有银弹) 相信这句话大家都或多或少的听到过. 它告诉我们, 我们无法找到一个通用的方法来解决所有的问题.
就和我们常常遇到的各种架构方式一样, 我们无法使用同一种架构方式来完成所有的架构设计.
就如不断有着新的架构理念出现一样, 从杂乱无序到MVC, MVP, MVVM和MVI. 架构设计是在不断的融合发展的. 我们也在不断的发展和思考过程中尝试出一条适合解决我们现阶段的问题的道路.

架构是对客观不足的妥协, 规范是对主观不足的妥协.

我的设计架构认知史

在实际的开发过程中, 大家或多或少的遇到了各种各样的架构设计. 也在不断的学习和成长过程中形成了自己的理解.而我对来说, 我理解中的架构设计, 大概是如下几个阶段.

混沌初开, 啥也不是.

这个阶段大致出现在从小学接触编程(说你呢, 小乌龟)直到大学初期的阶段.
在这个阶段下是完全没有任何架构设计认知的.
秉承的理念就是, 能用就行.
现在想想那时候确实也是不需要这些架构设计, 完完全全的实用主义.

倒也符合事务的发展, 类似前几年的摩尔定律的黄金时代(每经过18个月到24个月处理器的性能会增加一倍.)
但在这几年的环境下, 却无法再说出新的摩尔定律或者摩尔定律修正版了.

而架构设计的理念也是软件开发在一定环境下,可能是复杂性的问题, 或者是性能导致问题,也可能是人月神话带来的现实, 从而自然而然的出现的.
哪怕我只写hello wordl, 写了一千次后, 我也会开始思考如何写会更快更好.(可能三五次之后我们就开始思考这个问题了)

同样, 这个阶段会遇到各种各样的问题, 或者尝试做一些可以看作是架构设计雏形的事情. 哪怕十分的简陋和没有归纳.

但是不论怎么说, 这个阶段的我是对软件架构没有任何的概念的.

MVC开天, 还有这个?

直到某一天的课程上听到了MVC, 对当时没有任何架构体系的我来, 无异于一个新的大门.(毕竟当时的我可是写过for(i < 10){sleep(1000)} 的人.)

虽然现在看着MVC的设计理念, 其实并不是十分否适合Android的开发设计. (Activity: 我?)

但是对我来说, 是一个事务的从无到有的过程.

我的第一个想法是, 还可以这么做?
虽然天然的不是十分的适合, 加上自己摸索使用的各种挖坑填坑.
哪怕是一个很简陋的实现方法.
但是还是在当时的一个”大”项目中使用了MVC的架构设计.

然后发现, 是真好用啊.
比我前面野蛮生长的代码要好用许多.
更加的实用也更加具有可读性.

至此, 我有了架构设计的这个理念.

MVP飞升, 不要飞升!

我此时一直认为的事情是, “用新不用旧”.直到工作的第一年.

当时需要做一个新的项目.
而当时的项目中就设计成了MVP的架构, 刚开始接触的时候一是对新技术的好奇,同时也认为新的技术一定比老的技术好的封建余孽思想.
而且我还是很积极的去尝试新的技术和设计理念的.

不过, 好像我们的项目不适合MVP的架构设计.
在MVP的设计中P层需要被动的处理大量的View信息, 同时还需要涉及到View和Model之间的数据同步.
而我们的项目是一个信息流展示的项目, 涉及View动作的部分偏多, 加上功能的不断变化, 导致P层经常需要大量的更改.
有时候写着写着就会想着, 要不这个部分写在Activity中得了吧.

好像, 新的架构并不是完全适合新的项目?

MVVM出山, 天下无敌?

而作为MVC和MVP不断演化的产物, MVVM也应运而生.

而现阶段的很多的项目中, 都可以看出MVVM架构设计的痕迹.
也一直是Google对Android的建议架构设计https://github.com/android/nowinandroid
(这里我仍然认为一直的原因再后面会详细说明.)

大家在做设计的时候第一个想到的或多或少的就是MVVM了.

那么MVVM就是开发架构设计的银弹了么?

Jetpack现世, 天上来敌

在现阶段通过Android Jetpack Compose开发Android项目的时候, 发现MVVM架构开始有点力不从心了.

一是Kotlin很多新的的特性(类似coroutines)无法很完美的融入MVVM中, 另一个是由于很多库的支持的原因, 导致很多在MVVM中约定俗成的方法(类似DataBinding)无法继续在Jetpack Compose中继续使用了.

那么, 我们下一步改如何走?

而这个时候激进派说, 我们应该尝试新的架构设计来适配新的开发模式. MVI架构则应运而生, 可能很多人都没听说过这个架构设计, 一是因为其是一个更适合Jetpack Compose下的架构设计, 二可能是它有点激进了, 尝试通过各种的Intent/Action来解决不同层间的事件触发, 导致其学习成本比其它的架构设计之间学习成本要高很多.

而保守派则认为, 你们激进派太保守了.我们需要的不是一个新的架构设计, 我们需要的是一个新的架构设计的架构设计. 或者说一个通过的架构设计理念.

所以至此, 在Unidirectional Data Flow(UDF)理念下, UseCase出现了.

Unidirectional Data Flow(UDF)

这是Android最近的应用架构设计的推进方法: https://developer.android.com/topic/architecture?#modern-app-architecture

其中的一部分, 除此之外, 还有Separation of concerns(分离关注点), **Drive UI from data models(模型驱动)以及Single source of truth(SSOT)(单一数据源)**这几个或多或少我们都听说过的理念.

而UDF直译来说就是单项数据流, 在UDF中, 状态仅朝一个方向流动. 修改数据的事件朝相反方向流动.
需要注意的是, UDF常常需要和单一数据源原则同时使用.

而前面提到的MVI则是这个原则下的一个较高完成度的实现, 而现有的MVVM在经过职责的分离改造后, 也是符合这个原则的.

所我说的MVVM仍是官方推荐的架构设计.

这里我们先看一下现阶段Google推荐的应用架构设计的模式

更加详细的内容可以在Google官方的平台下查看. 简单来说, 就是分为了UI Layer, 可选的Domain Layer以及Data Layer.

我们可以先看一下MVVM的架构设计和Google推荐的应用架构的差异性.

这里实际上可以看到. MVVM的各个模块在新的推荐架构中都有着对应的一席之地.

究其原因, 可能是天下苦ViewModel久已. 在传统的MVVM架构中, 我们在ViewModel中加入了太多的功能和规则.
使其变得越来越臃肿和职责不清.
当我们意识到这个的问题的时候, 往往我们又没有办法将其拆分为合适的结果.

下面我们分析和立即一下, 各层的内容和解决的问题.

UI Layer

这里我们先看几张Google提供的图片.

我们认为UI是UI元素和UI状态绑定在一起的结果. 所以UI Layer包含了View/Composeable和ViewModel. 或者说, 我们将原有的ViewModel拆分为页面ViewModel(UI State)和业务ViewModel.

在结合UDF和SSOT的两个原则, 我们可以可以看到, 数据在UI Layer中是一个单向流动的过程.

和MVVM相比, 这一层及承载了View的功能, 又承载了ViewModel中UI Logic部分的功能.

Data Layer

而这一层包含应用数据和业务逻辑. 业务逻辑决定了应用的价值.

和MVVM相比, 这一层倒是和Model的功能基本类似, 都是为上层提供数据的部分.

Domain Layer

这一层负责封装复杂的业务逻辑,或者由多个ViewModel重复使用的简单业务逻辑. 因此, 此层是可选的, 因为并非所有应用都有这类需求.

而这一新增的设定, 其实更偏向于MVVM中ViewModel里的业务逻辑部分.

这这一部分, 则被推荐通过UseCase来实现.

UseCase

简单来说, Domain Layer中的UseCase就是我们处理代码逻辑的部分. 那么我们为什么要大费周章的设计UseCase这个概念呢? 或者说它有什么特点?

  1. 不持有状态
    为了保证UDF和SSOT, 我们并不希望逻辑处理中持有额外的内容, 更希望它可以像一个纯函数一样的进行工作.

  2. 单一职责
    一个UseCase只做一件事. 避免功能的不断扩张引起的文件内容不断扩张.(取而代之是文件数量的增加)

  3. 可有可无
    既然Data Layer是可选的模块, 那么UseCase就是一个可选的部分了. 因为部分的业务场景, 可以通过UI直接方法Repository, 不过也会带来新的问题, 可写可不写的后果就是, 都不写.

说一千道一万, 不如实际使用后理解起来更让人方便. 下面我们就已一个简单的代码例子来进行一次UDF思想下的架构使用.

Code Demo

功能很简单, 用户可以点击对应的爻来改变卦象, 并显示一些基础的卦辞.

代码的目录结构大致如下

下面我们依次查看各个Layer都完成了什么功能

UI Layer

有两个UiState(GuaBaseUiState,GuaExplainUiState)的原因是页面中有两个部分, 一个部分是同步的内容更新, 还有一部分是异步(模拟网络获取)的内容更新.

下面我们着重看一下ViewModel和Composeable的部分

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
@HiltViewModel
class GuaViewModel @Inject constructor(
// model layer层内容
private val defaultDatabaseRepository: DefaultDatabaseRepository,
// domain layer中对应的UseCase
getGuaExplainUseCase: GetGuaExplainUseCase,
getGuaBaseUseCase: GetGuaBaseUseCase,
private val savedStateHandle: SavedStateHandle,
) : ViewModel() {
private val searchQuery = savedStateHandle.getStateFlow(
key = SEARCH_QUERY,
initialValue = SEARCH_MIN_FTS_ENTITY_COUNT
)
private val currentYao = savedStateHandle.getStateFlow(
key = YaoS,
initialValue = YaoS_Default
)

// UI State 状态更新
val getYaoUIState: StateFlow<GuaBaseUiState> =
currentYao.flatMapLatest { query ->
......
}

val getGuaExplainUiState: StateFlow<GuaExplainUiState> =
searchQuery.flatMapLatest { query ->
......
}

// 业务逻辑触发
fun initDatabase(){
defaultDatabaseRepository.guaTableInit()
}

fun onSearchQueryChanged(query: Int) {
savedStateHandle[SEARCH_QUERY] = query
}

fun onYaoChanged(index: Int){
......
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

@Composable
internal fun GuaScreenRoute(
// di注入生对应ViewModel
guaViewModel: GuaViewModel = hiltViewModel()
) {
guaViewModel.initDatabase()
// 通过ViewModel辅助使用UiState
val yaoExplainUiState by guaViewModel.getGuaExplainUiState.collectAsStateWithLifecycle()
val yaoUiState by guaViewModel.getYaoUIState.collectAsStateWithLifecycle()
GuaScreen(
testClick = guaViewModel::onSearchQueryChanged,
yaoChange = guaViewModel::onYaoChanged,
guaExplainUiState = yaoExplainUiState,
yaoUiState = yaoUiState
)
}

@Composable
internal fun GuaScreen(
testClick: (Int) -> Unit = {},
yaoChange: (Int) -> Unit = {},
guaExplainUiState: GuaExplainUiState = GuaExplainUiState.LoadFailed,
yaoUiState: GuaBaseUiState = GuaBaseUiState()
) {
......
}


Data Layer

这里分别模拟从数据库同步获取数据和从网络异步获取数据的情况

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

class DefaultDatabaseRepository @Inject constructor(
private val configUtils: ConfigUtils,
private val guaDao: GuaDao
) :
DatabaseRepository {
override fun guaTableInit() {
......
}

//同步获取数据库中数据
override fun getGuaBaseInfo(imageList: List<Boolean>): Flow<GuaBaseResult> {
var images = ""
imageList.map {
images += if (it) "1" else "0"
}
val guaEntity = guaDao.getGuaByImage(images)
return flowOf(GuaBaseResult(guaEntity.id, guaEntity.name, guaEntity.detail, guaEntity.descGroup))
}
}


internal class DefaultGuaRepository @Inject constructor() :
GuaRepository {
// 异步获取网络中数据
override suspend fun getExplainInfo(index: Int): Flow<GuaExplainResult> {
return flowOf(FakeNetwork.getGuaFakeExplain(index))
}
}

Domain Layer

而这一层的内容反倒是很少, 更像是一种接口的定义.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class GetGuaBaseUseCase @Inject constructor(
private val databaseRepository: DatabaseRepository
) {
operator fun invoke(imageList: List<Boolean>): Flow<GuaBaseResult> =
databaseRepository.getGuaBaseInfo(imageList)
}

class GetGuaExplainUseCase @Inject constructor(
private val guaRepository: GuaRepository
) {
suspend operator fun invoke(index: Int): Flow<GuaExplainResult> =
guaRepository.getExplainInfo(index)
}

参考

Dagger basics

https://developer.android.com/training/dependency-injection/dagger-basics

可以帮助我们实现ViewModel和View间解耦.

总结

“没有银弹, 没有银弹.”

本文中所以介绍的各种架构设计或者架构理念, 受实际项目实际开发者(以及实际心情)影响, 没有一劳永逸的架构设计, 只有适应当下的架构设计.

作为经典的MVVM架构, 也不是一成不变的. 可以尝试将UIState的理念引入, 作为View的辅助工具.

Code

主要参考项目: (https://github.com/android/nowinandroid/tree/main)[https://github.com/android/nowinandroid/tree/main]

Demo项目地址: (https://github.com/clwater/AndroidUDF)[https://github.com/clwater/AndroidUDF]