Android App封装 ——架构(MVI + kotlin + Flow)

原文地址

项目搭建经历记录

  1. Android App封装 ——架构(MVI + kotlin + Flow)
  2. Android App封装 —— ViewBinding
  3. Android App封装 —— DI框架 Hilt?Koin?
  4. Android App封装 —— 实现自己的EventBus

一、背景

最近看了好多MVI的文章,原理大多都是参照google发布的 应用架构指南,但是实现方式有很多种,就想自己封装一套自己喜欢用的MVI架构,以供以后开发App使用。

说干就干,准备对标“玩Android”,利用提供的数据接口,搭建一个自己习惯使用的一套App项目,项目地址:Github wanandroid

二、MVI

先简单说一下MVI,从MVC到MVP到MVVM再到现在的MVI,google是为了一直解决痛点所以不断推出新的框架,具体的发展流程就不多做赘诉了,网上有好多,我们可以选择性适合自己的。

应用架构指南中主要的就是两个架构图:

2.1 总体架构

[图片上传失败...(image-ad7268-1673343374642)]

Google推荐的是每个应用至少有两层:

  • UI Layer 界面层: 在屏幕上显示应用数据
  • Data Layer 数据层: 提供所需要的应用数据(通过网络、文件等)
  • Domain Layer(optional)领域层/网域层 (可选):主要用于封装数据层的逻辑,方便与界面层的交互,可以根据User Case

图中主要的点在于各层之间的依赖关系是单向的,所以方便了各层之间的单元测试

2.2 UI层架构

UI简单来说就是拿到数据并展示,而数据是以state表示UI不同的状态传送给界面的,所以UI架构分为

  • UI elements层:UI元素,由activity、fragment以及包含的控件组成
  • State holders层: state状态的持有者,这里一般是由viewModel承担

[图片上传失败...(image-60cfc6-1673343374643)]

2.3 MVI UI层的特点

MVI在UI层相比与MVVM的核心区别是它的两大特性:

  1. 唯一可信数据源
  2. 数据单向流动

[图片上传失败...(image-8de122-1673343374643)]

从图中可以看到,

  1. 数据从Data Layer -> ViewModel -> UI,数据是单向流动的。ViewModel将数据封装成UI State传输到UI elements中,而UI elements是不会传输数据到ViewModel的。
  2. UI elements上的一些点击或者用户事件,都会封装成events事件,发送给ViewModel

2.4 搭建MVI要注意的点

了解了MVI的原理和特点后,我们就要开始着手搭建了,其中需要解决的有以下几点

  1. 定义UI Stateevents
  2. 构建UI State单向数据流UDF
  3. 构建事件流events
  4. UI State的订阅和发送

三、搭建项目

3.1 定义UI Stateevents

我们可以用interface先定义一个抽象的UI Stateeventseventintent是一个意思,都可以用来表示一次事件。

@Keep
interface IUiState

@Keep
interface IUiIntent

然后根据具体逻辑定义页面的UIState和UiIntent。

data class MainState(val bannerUiState: BannerUiState, val detailUiState: DetailUiState) : IUiState

sealed class BannerUiState {
    object INIT : BannerUiState()
    data class SUCCESS(val models: List<BannerModel>) : BannerUiState()
}

sealed class DetailUiState {
    object INIT : DetailUiState()
    data class SUCCESS(val articles: ArticleModel) : DetailUiState()
}

通过MainState将页面的不同状态封装起来,从而实现唯一可信数据源

3.2 构建单向数据流UDF

在ViewModel中使用StateFlow构建UI State流。

  • _uiStateFlow用来更新数据
  • uiStateFlow用来暴露给UI elements订阅
abstract class BaseViewModel<UiState : IUiState, UiIntent : IUiIntent> : ViewModel() {

    private val _uiStateFlow = MutableStateFlow(initUiState())
    val uiStateFlow: StateFlow<UiState> = _uiStateFlow

    protected abstract fun initUiState(): UiState

    protected fun sendUiState(copy: UiState.() -> UiState) {
        _uiStateFlow.update { copy(_uiStateFlow.value) }
    }
}
class MainViewModel : BaseViewModel<MainState, MainIntent>() {

    override fun initUiState(): MainState {
        return MainState(BannerUiState.INIT, DetailUiState.INIT)
    }
}

3.3 构建事件流

在ViewModel中使用 Channel构建事件流

  1. _uiIntentFlow用来传输Intent
  2. 在viewModelScope中开启协程监听uiIntentFlow,在子ViewModel中只用重写handlerIntent方法就可以处理Intent事件了
  3. 通过sendUiIntent就可以发送Intent事件了
abstract class BaseViewModel<UiState : IUiState, UiIntent : IUiIntent> : ViewModel() {

    private val _uiIntentFlow: Channel<UiIntent> = Channel()
    val uiIntentFlow: Flow<UiIntent> = _uiIntentFlow.receiveAsFlow()
    
    fun sendUiIntent(uiIntent: UiIntent) {
        viewModelScope.launch {
            _uiIntentFlow.send(uiIntent)
        }
    }

    init {
        viewModelScope.launch {
            uiIntentFlow.collect {
                handleIntent(it)
            }
        }
    }

    protected abstract fun handleIntent(intent: IUiIntent)
class MainViewModel : BaseViewModel<MainState, MainIntent>() {

    override fun handleIntent(intent: IUiIntent) {
        when (intent) {
            MainIntent.GetBanner -> {
                requestDataWithFlow()
            }
            is MainIntent.GetDetail -> {
                requestDataWithFlow()
            }
        }
    }
}

3.4 UI State的订阅和发送

3.4.1 订阅UI State

在Activity中订阅UI state的变化

  1. lifecycleScope中开启协程,collect uiStateFlow
  2. 使用map 来做局部变量的更新
  3. 使用distinctUntilChanged来做数据防抖
class MainActivity : BaseMVIActivity() {

    private fun registerEvent() {
        lifecycleScope.launchWhenStarted {
            mViewModel.uiStateFlow.map { it.bannerUiState }.distinctUntilChanged().collect { bannerUiState ->
                when (bannerUiState) {
                    is BannerUiState.INIT -> {}
                    is BannerUiState.SUCCESS -> {
                        bannerAdapter.setList(bannerUiState.models)
                    }
                }
            }
        }
        lifecycleScope.launchWhenStarted {
            mViewModel.uiStateFlow.map { it.detailUiState }.distinctUntilChanged().collect { detailUiState ->
                when (detailUiState) {
                    is DetailUiState.INIT -> {}
                    is DetailUiState.SUCCESS -> {
                        articleAdapter.setList(detailUiState.articles.datas)
                    }
                }

            }
        }
    }
}

3.4.2 发送Intent

直接调用sendUiIntent就可以发送Intent事件

button.setOnClickListener {
    mViewModel.sendUiIntent(MainIntent.GetBanner)
    mViewModel.sendUiIntent(MainIntent.GetDetail(0))
}

3.4.3 更新Ui State

调用sendUiState发送Ui State更新

需要注意的是: 在UiState改变时,使用的是copy复制一份原来的UiState,然后修改变动的值。这是为了做到 “可信数据源”,在定义MainState的时候,设置的就是val,是为了避免多线程并发读写,导致线程安全的问题。

class MainViewModel : BaseViewModel<MainState, MainIntent>() {
    private val mWanRepo = WanRepository()

    override fun initUiState(): MainState {
        return MainState(BannerUiState.INIT, DetailUiState.INIT)
    }

    override fun handleIntent(intent: IUiIntent) {
        when (intent) {
            MainIntent.GetBanner -> {
                requestDataWithFlow(showLoading = true,
                    request = { mWanRepo.requestWanData() },
                    successCallback = { data -> sendUiState { copy(bannerUiState = BannerUiState.SUCCESS(data)) } },
                    failCallback = {})
            }
            is MainIntent.GetDetail -> {
                requestDataWithFlow(showLoading = false,
                    request = { mWanRepo.requestRankData(intent.page) },
                    successCallback = { data -> sendUiState { copy(detailUiState = DetailUiState.SUCCESS(data)) } })
            }
        }
    }
}

其中 requestDataWithFlow 是封装的一个网络请求的方法

protected fun <T : Any> requestDataWithFlow(
    showLoading: Boolean = true,
    request: suspend () -> BaseData<T>,
    successCallback: (T) -> Unit,
    failCallback: suspend (String) -> Unit = { errMsg ->
        //默认异常处理
    },
) {
    viewModelScope.launch {
        val baseData: BaseData<T>
        try {
            baseData = request()
            when (baseData.state) {
                ReqState.Success -> {
                    sendLoadUiState(LoadUiState.ShowMainView)
                    baseData.data?.let { successCallback(it) }
                }
                ReqState.Error -> baseData.msg?.let { error(it) }
            }
        } catch (e: Exception) {
            e.message?.let { failCallback(it) }
        }
    }
}

至此一个MVI的框架基本就搭建完毕了

四、 总结

不管是MVC、MVP、MVVM还是MVI,主要就是View和Model之间的交互关系不同

  • MVI的核心是 数据的单向流动
  • MVI使用kotlin flow可以很方便的实现 响应式编程
  • MV整个View只依赖一个State刷新,这个State就是 唯一可信数据源

目前搭建了基础框架,后续还会在此项目的基础上继续封装jetpack等更加完善这个项目。

项目源码地址:Github wanandroid

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,504评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,434评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,089评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,378评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,472评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,506评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,519评论 3 413
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,292评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,738评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,022评论 2 329
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,194评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,873评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,536评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,162评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,413评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,075评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,080评论 2 352

推荐阅读更多精彩内容