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