android MVI到底是什么

前言

本篇文章的閱讀對象是為了感覺好像了解MVI但是又不知道這玩意到底是個啥的讀者
想理解MVI 需要提前理解幾個東西
1.為什么推薦使用MVI,android 的MVI是基于什么提出的
2.android 的MVI是基于什么實現的,為什么要用這些

以上三點我先用最簡短的語言以自己的理解先做一個解答

1,為什么推薦使用MVI,MVI是基于什么提出的

答:主要為了ViewModel層和View層的交互由雙向轉化為單向,并且規范交互數據傳輸

android端由mvc到mvp再到mvvm最后到mvi,每一次的變化都讓代碼分層更加清晰,目前MVVM的缺點是ViewModel和view的交互還是屬于雙向交互,viewModel和Model的處理界限也比較模糊,所以提出MVI,MVI其實是基于MVVM, 在View和ViewModel中增加了Intent來作為中間傳輸,通過響應編程更新UI實現的。這樣不僅規范View與ViewModel交互,且將交互順序由View—>ViewModel->View 的雙向交互變為View->Intent->ViewModel->State->View的環形交互,通過Intent和State來解決ViewModel與Model的界限模糊問題。
也就是說ViewModel現在可以不關心如何被view觸發,如何刷新UI,也不關心當前有多少數據模型,只用來維護Intent和state管理(再直白些就是intent就是view調用viewModel的中間層,state就是viewModel回調view的中間層,model通過intent和state去管理,看起來會更加簡潔)

2,android 的MVI是基于什么實現的

目前android主流的MVI是基于協程+flow+viewModel去實現的
kotlin協程就不說了,省去接口回調,控制代碼執行順序,線程切換kotlin的協程功不可沒
flow:中文翻譯成流和Stream容易混淆,flow是響應式流,會有配備一個生產者和一個消費者(android可以理解成類似handler里的message,處理方式相似但是原理不同)
viewModel:jetpack家族,本來也可以自己寫,但是jetpack提供了可以管理生命周期的viewModel不比自己寫香么?

下面兩個文章看看更加有助理解mvi

kotlin 響應式編程flow
https://juejin.cn/post/7034379406730592269
這篇文字幾乎和官方文檔寫的詳細程度差不多,但是解釋會更加友好

MVVM使用
http://www.lxweimin.com/p/f9d0688b241e
不喜歡看思路的可以通過這篇文章感受mvvm代碼的層次結構

正片

這篇文章看完了能學會啥?
1.flow在UI中簡單用法
2.Intent是個啥
3.state是個啥
4.原來MVI這么簡單

1:flow在UI中簡單用法

為啥我看MVI要先看flow?
因為沒有flow就沒有MVI的I的靈魂(如果你用rxjava或者自己創建監聽者當我沒說)
首先如果不知道flow怎么用的同學,我得說說你了,kotlin好好學學,mvvm都用kotlin寫了,mvi還想著java是不是太過分了!(只針對android)

首先掏出官方例子

//所有的collect方法都是suspend修飾的,所以扔了協程里
runBlocking {
//創建一個流
     flow {

//用循環定義一個生產者
        for (i in 1..10) {
//生產者發10個數
            emit(i)
       }  
    }.collect {//注冊這個流消費者
//消費者打印
           println(it)
   }
}

這個流很簡單就是創建一個流,然后消費打印,用這段代碼中兩個方法比較重要,emit和collect,源碼就不分析了就是emit是生產者發送數據,collect是消費者接受數據
然后我們把這個例子稍微復雜化一點放到例子里
ViewModel代碼

class EnglishVM : ViewModel() {
    var flow=flow<Int> {
        for (i in 1..10) {
            emit(i)
        }
    }         
}

這是activity代碼

class MVIEnglishActivity :BaseActivity() {
    val viewModel :EnglishVM by viewModels<EnglishVM>()
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent(R.layout.act_mvi_english_class)
        setTitle("MVI學習")
        runBlocking {
            viewMode.flow.collect {
//將數字打印到textview上
                tvClass addText "$it"
            }
        }

    }
//做了個直接打印到textview的快捷方法,可以忽略
    infix fun  TextView.addText(text: String) {
       this.text = "${this.text?.toString()}$text\n";
    }
}

來看執行結果

執行結果

現在通過flow將文字展示到了UI上,但是有個問題,我們的業務場景一般是觸發某個事件以后才會刷新UI,而且刷新UI我們只有一個或幾個結果,不是一連串的數字,所以我們在這個基礎上再次升級
首先flow這個方法已經不是那么好用了,我們引入一個新的概念StateFlow(我可以點)
StateFlow由兩個API構成MutableStateFlow和StateFlow,主要用來通過狀態類的變化來發送狀態變化流。原理大體就是通過get,set去監聽狀態state變化,然后發送流,這里就不展開了,可以看各個不同版本的源碼

然后將viewModel中的flow改為StateFlow并加入兩個刷新UI的方法

class EnglishVM : BaseViewModel() {
//MutableStateFlow需要默認傳入一個狀態,我們隨便傳個1代表默認狀態
   val state = MutableStateFlow<Int>(1)
//將狀態改為2代表正在加載
    fun doLoading(){
        state.value = 2
    }
//將狀態改為3代表加載完畢
    fun finishLoading(){
        state.value = 3
    }
}

然后給activity增加兩個按鈕,添加點擊事件,分別調用doLoading和finishLoading

class MVIEnglishActivity :BaseActivity() {
    val viewModel :EnglishVM by viewModels<EnglishVM>()
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent(R.layout.act_mvi_english_class)
        setTitle("MVI學習")
        btnFinishLoading.setOnClickListener {
            tvClass addText "btnFinishLoading 被點擊"

            viewMode.finishLoading()
        }
        btnLoading.setOnClickListener {
            tvClass addText "btnLoading 被點擊"
            viewMode.doLoading()
        }

         GlobalScope.launch  {
            viewMode.state.collect {
                tvClass addText "$it"
            }
        }

    }
    infix fun  TextView.addText(text: String) {
       this.text = "${this.text?.toString()}$text\n";
    }
}

運行并分別點擊LOADING和FINISH


運行結果

好的一個簡單的通過flow更新UI的效果已經完畢了,下面開始實現MVI

2:Intent是個啥

我可以很負責的告訴你,Intent就是個枚舉,而且是個特殊的枚舉,在kotlin中可以通過sealed關鍵字來生成封閉類,這個關鍵字生成的封閉類在when語句中可以不用謝else,而且由于是封閉類,所以可以通過數據對象來實現各種騷操作
比如下面的代碼

//寫個英語的意圖
sealed class EngLishIntent {
//用數據類表示加載英語方法
    data class doLoadingEnglish(val num:Int):EngLishIntent()
//用匿名對象表示完成加載方法
    object finishLoading:EngLishIntent()
}

但是怎么用這個Intent呢?又涉及到一個kotlin的概念Channel(我可以點)
channel本來是用來做協程之間通訊的,而我們的view層的觸發操作和viewModel層獲取數據這個流程恰巧應該是需要完全分離的,并且channel具備flow的特性,所以用channel來做view和viewModel的通訊非常適合
我們通過再把上面的例子,通過Intent來處理下

意圖代碼如下

sealed class EngLishIntent {
//用數據類表示加載英語方法
    data class DoLoadingEnglish(val num:Int):EngLishIntent()
//用匿名對象表示完成加載方法
    object FinishLoading:EngLishIntent()
}

viewModel將Intent引入

class EnglishVM : BaseViewModel() {
    val englishIntent = Channel<EngLishIntent>(Channel.UNLIMITED)
     val state = MutableStateFlow<Int>(1)
//初始化的時候將channel的消費者綁定
    init {
        handleIntent();
    }
//注冊消費者
    private fun handleIntent() {
        viewModelScope.launch {
//將Channel轉化為flow,并且注冊消費者
            englishIntent.consumeAsFlow().collect {
//這里的it和Channel<EngLishIntent>泛型保持一致,所以it是封閉類(特殊枚舉類)
                when(it){
//判斷是FinishLoading 將state.value=3
                    is EngLishIntent.FinishLoading->{state.value=3}
//判斷是DoLoadingEnglish 將state.value=1

                    is EngLishIntent.DoLoadingEnglish->{
                        //此處可以通過 it. 拿到DoLoadingEnglish的入參 后面會演示
                        state.value=2}
                }
            }
        }
    }

然后再把Activity改改

class MVIEnglishActivity :BaseActivity() {
    val viewModel :EnglishVM by viewModels<EnglishVM>()
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent(R.layout.act_mvi_english_class)
        setTitle("MVI學習")
        btnFinishLoading.setOnClickListener {
            tvClass addText "btnFinishLoading 被點擊"
//協程方法統一提取,方便日后修改
            doLaunch{
                tvClass addText "send(EngLishIntent.FinishLoading)"
//拿到viewMode的englishIntent去傳遞意圖
                viewMode.englishIntent.send(EngLishIntent.FinishLoading)
            }
        }
        btnLoading.setOnClickListener {
            tvClass addText "btnLoading 被點擊"
            doLaunch{
                tvClass addText "send(EngLishIntent.DoLoadingEnglish)"
                viewMode.englishIntent.send(EngLishIntent.DoLoadingEnglish(5))
            }
        }

        GlobalScope.launch {
            viewMode.state.collect {
                tvClass addText "$it"
            }
        }

    }
    fun doLaunch(block: suspend CoroutineScope.() -> Unit){
        GlobalScope.launch {
            block.invoke(this)
        }
    }
    infix fun  TextView.addText(text: String) {
       this.text = "${this.text?.toString()}$text\n";
    }
}

然后看下點擊兩個按鈕后的運行結果


運行結果

結果和上次的結果沒什么太大的區別,而且感覺代碼還變復雜了,為什么要這么做?
注意看下面兩個圖


原始方法

Intent

之前是直接使用viewModel提供的方法的,現在變成了傳輸intent里的枚舉,徹底將View和ViewModel解耦了,現在唯一耦合的就是viewModel持有的Intent了,實現了業務解耦,很棒棒

既然知道了通過intent能實現view發起事件對viewModel的解耦,那能不能實現ViewModel刷新view的解耦呢?
其實上面的代碼我們已經通過flow實現了一大半了,現在把int類型轉換成一個枚舉讓代碼更加嚴謹就能完全解耦了,此時就能引入MVI的最后一個概念state了

3:state是個啥

state是個和Intent一樣的枚舉,但是不同的是intent是個事件流,state是個狀態流
首先我們先定義一個和Intent差不多的封裝類state

sealed class EnglishState {
    object BeforeLoading:EnglishState()
    object Loading:EnglishState()
    object FinishLoading:EnglishState()
}

然后我們把之前的MutableStateFlow封裝起來,不給view層修改權限,已保證我們業務邏輯不會寫在UI層,并且把1、2、3等狀態改為剛剛創建的EnglishState

class EnglishVM : BaseViewModel() {
    val englishIntent = Channel<EngLishIntent>(Channel.UNLIMITED)
    private val _state = MutableStateFlow<EnglishState>(EnglishState.BeforeLoading)
    val state: StateFlow<EnglishState>
        get() = _state

    init {
        handleIntent();

    }

    private fun handleIntent() {
        viewModelScope.launch {
            englishIntent.consumeAsFlow().collect {
                when(it){
                   is EngLishIntent.FinishLoading->{
                        _state.value=EnglishState.FinishLoading
                    }
                    is EngLishIntent.DoLoadingEnglish->{
                        //此處可以通過 it. 拿到DoLoadingEnglish的入參 后面會演示
                        _state.value=EnglishState.Loading
                    }
                }
            }

        }
    }
}

然后把Activity的打印UI更新部分通過state做不同的邏輯處理

class MVIEnglishActivity :BaseActivity() {
    val viewModel :EnglishVM by viewModels<EnglishVM>()
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent(R.layout.act_mvi_english_class)
        setTitle("MVI學習")
        btnFinishLoading.setOnClickListener {
            tvClass addText "btnFinishLoading 被點擊"
            doLaunch{
                tvClass addText "send(EngLishIntent.FinishLoading)"
                viewModel.englishIntent.send(EngLishIntent.FinishLoading)
            }
        }
        btnLoading.setOnClickListener {
            tvClass addText "btnLoading 被點擊"
            doLaunch{
                tvClass addText "send(EngLishIntent.DoLoadingEnglish)"

                viewModel.englishIntent.send(EngLishIntent.DoLoadingEnglish(5))
            }
        }

        lifecycleScope.launch {
            viewModel.state.collect {
                when(it){
                    is EnglishState.BeforeLoading->{
                        tvClass addText "初始化頁面"

                    }
                    is EnglishState.Loading ->{
                        tvClass addText "加載中..."

                    }
                    is EnglishState.FinishLoading ->{
                        tvClass addText "加載完畢..."

                    }
                }
            }
        }

    }
    fun doLaunch(block: suspend CoroutineScope.() -> Unit){
        GlobalScope.launch {
            block.invoke(this)
        }
    }
    infix fun  TextView.addText(text: String) {
       this.text = "${this.text?.toString()}$text\n";
    }
}

分別點擊按鈕結果如下


image.png

到這里,一個基本的MVI就已經成型了,我們結合實際請求,稍稍做些許改動

4.原來MVI這么簡單

我們先將ViewModel賦予真正的請求能力,提供一個基類(可以通過各種方法來)

open class BaseViewModel : ViewModel() {
    var getClient: () -> Urls = {
        val client = OkHttpClient.Builder()
            .connectTimeout(30, TimeUnit.SECONDS) //設置超時時間
            .retryOnConnectionFailure(true)
        val logInterceptor = HttpLoggingInterceptor()
//        if (BuildConfig.DEBUG) {
//            //顯示日志
//            logInterceptor.setLevel(HttpLoggingInterceptor.Level.BODY)
//        } else {
//            logInterceptor.setLevel(HttpLoggingInterceptor.Level.NONE)
//        }
        client.addInterceptor(GsonInterceptor())
        Retrofit.Builder()
            .client(client.build())
            .baseUrl("https://route.showapi.com/")
            .addConverterFactory(ViewModelGsonConverterFactory())
            .build().create(Urls::class.java)
    }
//向協程提供一個全局異常,用來處理異常UI
    fun <T> errorContext(err: (errorMessage:Throwable) -> Unit):CoroutineExceptionHandler {
       return CoroutineExceptionHandler { _, e ->
           err.invoke(e)
       }
    }
}

intent 修改修改,加一個請求類型

sealed class EngLishIntent {
    //獲取英語句子數據
    data class DoLoadingEnglish(val num:Int):EngLishIntent()
    //獲取新聞數據
    object DoLoadingNews:EngLishIntent()
}

State也改改,新增幾個數據狀態

sealed class EnglishState {
    object BeforeLoading:EnglishState()
    object Loading:EnglishState()
    object FinishLoading:EnglishState()

    data class EnglishData(val list:List<EnglishKey>):EnglishState()
    data class NewsData(val list:List<NewsListKey>):EnglishState()

    data class ErrorData(val error:String):EnglishState();


}

viewmodel改改,帶有真正的網絡請求

class EnglishVM : BaseViewModel() {
    val englishIntent = Channel<EngLishIntent>(Channel.UNLIMITED)
    private val _state = MutableStateFlow<EnglishState>(EnglishState.BeforeLoading)
    val state: StateFlow<EnglishState>
        get() = _state
    init {
        handleIntent();
    }
    private fun handleIntent() {
        viewModelScope.launch {
            englishIntent.consumeAsFlow().collect {
                //這兩種寫法太冗余了
//                    is EngLishIntent.DoLoadingEnglish -> loadingEnglish()
//                    is EngLishIntent.DoLoadingNews -> loadingEnglish()
                commentLoading(it)
            }
        }
    }
   suspend fun intentToState(intent:EngLishIntent):EnglishState{
        when (intent) {
            //加載英語句子
            is EngLishIntent.DoLoadingEnglish ->
                return EnglishState.EnglishData(getClient.invoke().getEnglishWordsByLaunch(5))
            //加載新聞句子
            is EngLishIntent.DoLoadingNews ->
                return EnglishState.NewsData(getClient.invoke().getNewsListKeyByLaunch())
        }
    }

    ////加載英語句子
//    private fun loadingEnglish() {
//        viewModelScope.launch(context = (errorContext {
//            _state.value = EnglishState.FinishLoading
//            _state.value = EnglishState.ErrorData(it.message?:"請求異常")
//        } + Dispatchers.Main)) {
//            _state.value = EnglishState.Loading
//            _state.value = EnglishState.EnglishData(getClient.invoke().getEnglishWordsByLaunch(5))
//            _state.value = EnglishState.FinishLoading
//        }
//    }
    //加載新聞
//    private fun loadingNews() {
//        viewModelScope.launch(context = (errorContext {
//            _state.value = EnglishState.FinishLoading
//            _state.value = EnglishState.ErrorData(it.message?:"請求異常")
//        } + Dispatchers.Main)) {
//            _state.value = EnglishState.Loading
//            _state.value = EnglishState.NewsData(getClient.invoke().getNewsListKeyByLaunch())
//            _state.value = EnglishState.FinishLoading
//        }
//    }
    private fun commentLoading(intent:EngLishIntent) {
        viewModelScope.launch(context = (errorContext {
            _state.value = EnglishState.FinishLoading
            _state.value = EnglishState.ErrorData(it.message?:"請求異常")
        } + Dispatchers.Main)) {
            _state.value = EnglishState.Loading
            _state.value = intentToState(intent)
            _state.value = EnglishState.FinishLoading
        }
    }
}

最后把activity的按鈕改改,UI刷新邏輯改改變成這樣

class MVIEnglishActivity :BaseActivity() {
    val viewModel :EnglishVM by viewModels<EnglishVM>()
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent(R.layout.act_mvi_english_class)
        setTitle("MVI學習")
        btnLoadingNews.setOnClickListener {
            tvClass addText "btnLoadingNews 被點擊"
            doLaunch{
                tvClass addText "send(EngLishIntent.DoLoadingNews)"
                viewModel.englishIntent.send(EngLishIntent.DoLoadingNews)
            }
        }
        btnLoadingEnglish.setOnClickListener {
            tvClass addText "btnLoadingEnglish 被點擊"
            doLaunch{
                tvClass addText "send(EngLishIntent.DoLoadingEnglish)"

                viewModel.englishIntent.send(EngLishIntent.DoLoadingEnglish(5))
            }
        }
//這里注意改成有生命周期的lifecycleScope 否則網絡請求回來這里管道就銷毀了
        lifecycleScope.launch {
            viewModel.state.collect {
                when(it){
                    is EnglishState.BeforeLoading->{
                        tvClass addText "初始化頁面"

                    }
                    is EnglishState.Loading ->{
                        tvClass addText "加載中..."

                    }
                    is EnglishState.FinishLoading ->{
                        tvClass addText "加載完畢..."

                    }
                    is EnglishState.EnglishData->{
                        for (key in it.list){
                            tvClass addText key.english addText key.chinese

                        }

                    }
                    is EnglishState.NewsData->{
                        for (key in it.list){
                            tvClass addText "標題:${key.title}" addText "摘要:${key.summary}" addText "省份:${key.provinceName} 時間:${key.updateTime}"


                        }
                    }
                }
            }
        }

    }
    fun doLaunch(block: suspend CoroutineScope.() -> Unit){
        GlobalScope.launch {
            block.invoke(this)
        }
    }
    infix fun  TextView.addText(text: String) :TextView{
       this.text = "${this.text?.toString()}$text\n";
        return this
    }
}

最后附上接口

interface Urls {



    @GET("/1211-1")
   suspend fun getEnglishWordsByLaunch(
        @Query("count") count: Int?,
        @Query("showapi_appid") id: String = "測試id",
        @Query("showapi_sign") showapi_sign: String = "showapi_sign",
    ): ArrayList<EnglishKey>

    @GET("/2217-4")
    suspend fun getNewsListKeyByLaunch(
        @Query("showapi_appid") id: String = "測試id",
        @Query("showapi_sign") showapi_sign: String = "showapi_sign",
    ): ArrayList<NewsListKey>

點擊兩次按鈕后結果入下


image.png

一個簡單的MVI網絡請求架構到此結束

結尾

MVI其實主要思想是通過Intent將view和業務實現層分離,達到通過意圖傳遞邏輯方法。所以不一定非要基于MVVM,也適用于MVP,這次分享就到此結束了
最后感謝
https://blog.csdn.net/vitaviva/article/details/109406873
這篇文章提供的清晰簡單的思路,代碼思路均由這篇文章獲取

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容