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

语言: CN / TW / HK

theme: juejin highlight: darcula


一、背景

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

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

二、MVI

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

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

2.1 总体架构

image.png

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.png

2.3 MVI UI层的特点

MVI在UI层相比与MVVM的核心区别是它的两大特性: 1. 唯一可信数据源 2. 数据单向流动

image.png

从图中可以看到, 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是一个意思,都可以用来表示一次事件。

```kotlin @Keep interface IUiState

@Keep interface IUiIntent 然后根据具体逻辑定义页面的UIState和UiIntent。kotlin data class MainState(val bannerUiState: BannerUiState, val detailUiState: DetailUiState) : IUiState

sealed class BannerUiState { object INIT : BannerUiState() data class SUCCESS(val models: List) : 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订阅

```kotlin abstract class BaseViewModel : 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) }
}

} kotlin class MainViewModel : BaseViewModel() {

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 : 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() {

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来做数据防抖 ```kotlin 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事件 kotlin button.setOnClickListener { mViewModel.sendUiIntent(MainIntent.GetBanner) mViewModel.sendUiIntent(MainIntent.GetDetail(0)) }

3.4.3 更新Ui State

调用sendUiState发送Ui State更新

需要注意的是: 在UiState改变时,使用的是copy复制一份原来的UiState,然后修改变动的值。这是为了做到 “可信数据源”,在定义MainState的时候,设置的就是val,是为了避免多线程并发读写,导致线程安全的问题。 ```kotlin class MainViewModel : BaseViewModel() { 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的框架基本就搭建完毕了

3.5运行效果

www.alltoall.net_device-2022-12-15-161207_I_ahtLP5Kj.gif

四、 总结

不管是MVC、MVP、MVVM还是MVI,主要就是View和Model之间的交互关系不同 - MVI的核心是 数据的单向流动 - MVI使用kotlin flow可以很方便的实现 响应式编程 - MV整个View只依赖一个State刷新,这个State就是 唯一可信数据源

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

项目源码地址:Github wanandroid