Android应用架构进化史指南

本篇文章转自clwater的博客,文章主要分享了Android 应用架构的进化过程,相信会对大家有所帮助!

原文地址:

https://juejin.cn/post/7337589994464935936

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

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

我的设计架构认知史

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

混沌初开,啥也不是

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

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

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

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

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

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的建议架构设计(这里我仍然认为一直的原因再后面会详细说明)。

大家在做设计的时候第一个想到的或多或少的就是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最近的应用架构设计的推进方法其中的一部分,除此之外,还有Separation of concerns(分离关注点),Drive UI from data models(模型驱动)以及Single source of truth(SSOT)(单一数据源)这几个或多或少我们都听说过的理念。

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

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

所我说的MVVM仍是官方推荐的架构设计。这里我们先看一下现阶段Google推荐的应用架构设计的模式。

5099fe8a87c769e4bbcf8e09f9935e72.png

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

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

698323ffc1974da33c55a465037c91f4.png

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

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

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

UI Layer

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

a961dbf80e22bae70937d6c7e2af1aa3.png

4bb6adb4600dacac9aa89db69afb016c.png

a88cce8d6971a4ad9b0eb5892058e92e.png

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

在结合UDF和SSOT的两个原则,我们可以可以看到,数据在UI Layer中是一个单向流动的过程。和MVVM相比,这一层及承载了View的功能,又承载了ViewModel中UI Logic部分的功能。

Data Layer

743a60882962abe43c5c77ee1eb45263.png

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

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

Domain Layer

46862af9c6c4a7f1cdf474d0c25b3dfb.png

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

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

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

UseCase

简单来说,Domain Layer中的UseCase就是我们处理代码逻辑的部分。

那么我们为什么要大费周章的设计UseCase这个概念呢?或者说它有什么特点?

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

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

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

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

Code Demo

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

a4c1a2859bba0f7e12f8e864d2b5f611.png

a51874d532eaa6b48cf020df1620c0a3.png

fb66fe756df4063f6905e1aae71eb8d7.png

代码的目录结构大致如下:

be3b1774e1e8665cdbf9ec1cb165aefc.png

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

UI Layer

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

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

@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){
        ......
    }
}
@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

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

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

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

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)
}

总结

"没有银弹,没有银弹。"

本文中所以介绍的各种架构设计或者架构理念,受实际项目实际开发者(以及实际心情)影响,没有一劳永逸的架构设计,只有适应当下的架构设计。作为经典的MVVM架构,也不是一成不变的。可以尝试将UIState的理念引入,作为View的辅助工具。

关注我获取更多知识或者投稿

42f143435fecd35b8d14a07f27961f38.jpeg

00d53700075acf5859a21feaa628b2e5.jpeg

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值