从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的主张架构规划github.com/android/now… (这儿我依然以为一向的原因再后面会具体说明.)

咱们在做规划的时分第一个想到的或多或少的便是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最近的运用架构规划的推进方法: developer.android.com/topic/archi…

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

而UDF直译来说便是单项数据流, 在UDF中, 状况仅朝一个方向活动. 修正数据的事情朝相反方向活动. 需求注意的是, UDF常常需求和单一数据源准则一起运用.

而前面说到的MVI则是这个准则下的一个较高完成度的完成, 而现有的MVVM在经过责任的别离改造后, 也是符合这个准则的.

所我说的MVVM仍是官方引荐的架构规划.

这儿咱们先看一下现阶段Google引荐的运用架构规划的模式

从UseCase的理念看Android当时的运用架构规划攻略

愈加具体的内容能够在Google官方的平台下查看. 简略来说, 便是分为了UI Layer, 可选的Domain Layer以及Data Layer.

咱们能够先看一下MVVM的架构规划和Google引荐的运用架构的差异性.

从UseCase的理念看Android当时的运用架构规划攻略

这儿实践上能够看到. MVVM的各个模块在新的引荐架构中都有着对应的一席之地.

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

下面咱们分析和当即一下, 各层的内容和处理的问题.

UI Layer

这儿咱们先看几张Google供给的图片.

从UseCase的理念看Android当时的运用架构规划攻略
从UseCase的理念看Android当时的运用架构规划攻略
从UseCase的理念看Android当时的运用架构规划攻略

咱们以为UI是UI元素和UI状况绑定在一起的成果. 所以UI Layer包括了View/Composeable和ViewModel. 或许说, 咱们将原有的ViewModel拆分为页面ViewModel(UI State)和事务ViewModel.

在结合UDF和SSOT的两个准则, 咱们能够能够看到, 数据在UI Layer中是一个单向活动的过程.

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

Data Layer

从UseCase的理念看Android当时的运用架构规划攻略

而这一层包括运用数据和事务逻辑. 事务逻辑决定了运用的价值.

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

Domain Layer

从UseCase的理念看Android当时的运用架构规划攻略

这一层负责封装复杂的事务逻辑,或许由多个ViewModel重复运用的简略事务逻辑. 因而, 此层是可选的, 由于并非所有运用都有这类需求.

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

这这一部分, 则被引荐经过UseCase来完成.

UseCase

简略来说, Domain Layer中的UseCase便是咱们处理代码逻辑的部分. 那么咱们为什么要大费周章的规划UseCase这个概念呢? 或许说它有什么特点?

  1. 不持有状况 为了保证UDF和SSOT, 咱们并不期望逻辑处理中持有额外的内容, 更期望它能够像一个纯函数相同的进行作业.

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

  3. 可有可无 既然Data Layer是可选的模块, 那么UseCase便是一个可选的部分了. 由于部分的事务场景, 能够经过UI直接方法Repository, 不过也会带来新的问题, 可写可不写的结果便是, 都不写.

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

Code Demo

功用很简略, 用户能够点击对应的爻来改动卦象, 并显现一些基础的卦辞.

从UseCase的理念看Android当时的运用架构规划攻略
从UseCase的理念看Android当时的运用架构规划攻略
从UseCase的理念看Android当时的运用架构规划攻略

代码的目录结构大致如下

从UseCase的理念看Android当时的运用架构规划攻略

下面咱们顺次查看各个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)
}

参阅

Dagger basics

developer.android.com/training/de…

能够协助咱们完成ViewModel和View间解耦.

总结

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

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

作为经典的MVVM架构, 也不是原封不动的. 能够测验将UIState的理念引入, 作为View的辅助工具.

Code

首要参阅项目: (github.com/android/now…

Demo项目地址: (github.com/clwater/And…