MVI 架構封裝:快速優(yōu)雅地實現(xiàn)網絡請求

作者:RicardoMJiang
掘金:MVI 架構封裝:快速優(yōu)雅地實現(xiàn)網絡請求
原文鏈接:https://juejin.cn/post/70278153

前言

網絡請求可以說是Android開發(fā)中最常見的需求之一课梳,基本上每個頁面都需要發(fā)起幾個網絡請求芒帕。
因此大家通常都會對網絡請求進行一定的封裝煌张,解決模板代碼過多,重復代碼焰檩,異常捕獲等一些問題

前面我們介紹了MVI架構的主要原理與更佳實踐
MVVM 進階版:MVI 架構了解一下~
MVI 架構更佳實踐:支持 LiveData 屬性監(jiān)聽
我們這次一起來看下MVI架構下如何對網絡請求進行封裝,以及相對于MVVM架構有什么優(yōu)勢

本文主要包括以下內容

  1. MVVM架構下的網絡請求封裝與問題
  2. MVI架構下封裝網絡請求
  3. MVI架構與Flow結合實現(xiàn)網絡請求

MVVM架構下的網絡請求封裝與問題

相信大家都看過不少MVVM架構下的網絡請求封裝讨勤,一般是這樣寫的

# MainViewModel
class MainViewModel {
    val userLiveData = StateLiveData<User?>()
    fun login(username: String, password: String) {
        viewModelScope.launch {
            userLiveData.value = repository.login(username, password)
        }
    }
}

class MainActivity : AppCompatActivity() {
    fun initViewModel(){
    // 請求網絡
    mViewModel.login("username", "password")
    // 注冊監(jiān)聽
    mViewModel.userLiveData.observeState(this) {
            onLoading {
        showLoading()
            }
            onSuccess {data ->
        mBinding.tvContent.text = data.toString()
            }
            onError {
                dismissLoading()
            }
    }
    }
}

如上所示,就是最常見的MVVM架構下網絡請求封裝晨另,主要思路如是

  1. 添加一個StateLiveData,一個LiveData支持多種狀態(tài)潭千,例如加載中,加載成功借尿,加載失敗等
  2. 在頁面中監(jiān)聽StateLiveData刨晴,在頁面中處理onLoadingonSuccess,onError等邏輯

這種封裝的本質其實就是將請求的回調邏輯處理遷移到View層了
這其實并不是我們想要的路翻,我們的理想狀況應該是邏輯盡量放在ViewModel中狈癞,View層只需要監(jiān)聽ViewModel層并更新UI

既然這種封裝其實違背了不在View層寫邏輯的原則,那么為什么還有那么多人用呢?
本質上是因為ViewModel層與View層的通信成本比較高
想象一下茂契,如果我們不使用StateLiveData蝶桶,針對每個請求就需要新建一個LiveData來表示請求狀態(tài),如果成功或失敗后需要彈Toast或者Dialog掉冶,或者頁面中有多個請求真竖,就需要定義更多的LiveData, 同時為了保證對外暴露的LiveData不可變厌小,每個狀態(tài)都需要定義兩遍LiveData

這就是為什么這種封裝其實違背了不在View層寫邏輯但仍然流行的原因恢共,因為在MVVM架構中每處理一種狀態(tài),就需要添加兩個LiveData璧亚,成本較高讨韭,大多數(shù)人并不愿意支付這個成本
MVI架構正解決了這個問題

MVI架構下封裝網絡請求

之前已經介紹過了MVI架構,MVI架構使用方面我們就不再多說,我們直接來看下MVI架構下怎么發(fā)起一個簡單網絡請求

簡單的網絡請求

class NetworkViewModel : ViewModel() {
    /**
     * 頁面請求透硝,通常包括刷新頁面loading狀態(tài)等
     */
    private fun pageRequest() {
        viewModelScope.rxLaunch<String> {
            onRequest = {
                _viewStates.setState { copy(pageStatus = PageStatus.Loading) }
                delay(2000)
                "頁面請求成功"
            }
            onSuccess = {
                _viewStates.setState { copy(content = it, pageStatus = PageStatus.Success) }
                _viewEvents.setEvent(NetworkViewEvent.ShowToast("請求成功"))
            }
            onError = {
                _viewStates.setState { copy(pageStatus = PageStatus.Error(it)) }
            }
        }
    }
}

# Activity層
class MainActivity : AppCompatActivity() {
    private fun initViewModel() {
        viewModel.viewStates.let { state ->
            //監(jiān)聽網絡請求狀態(tài)
            state.observeState(this, NetworkViewState::pageStatus) {
                when (it) {
                    is PageStatus.Success -> state_layout.showContent()
                    is PageStatus.Loading -> state_layout.showLoading()
                    is PageStatus.Error -> state_layout.showError()
                }
            }
            //監(jiān)聽頁面數(shù)據(jù)
            state.observeState(this, NetworkViewState::content) {
                tv_content.text = it
            }
        }
        //監(jiān)聽一次性事件吉嚣,如Toast,ShowDialog等   
        viewModel.viewEvents.observe(this) {
            when (it) {
                is NetworkViewEvent.ShowToast -> toast(it.message)
                is NetworkViewEvent.ShowLoadingDialog -> showLoadingDialog()
                is NetworkViewEvent.DismissLoadingDialog -> dismissLoadingDialog()
            }
        }
    }
}   

如上,代碼很簡單

  1. 頁面的所有狀態(tài)都存儲在NetworkViewState中蹬铺,后面如果需要添加狀態(tài)不需要添加LiveData尝哆,添加屬性即可,NetworkViewEvent中存儲了所有一次事件甜攀,同理
  2. ViewModel中發(fā)起網絡請求并監(jiān)聽網絡請求回調秋泄,其中viewModelScope.rxLaunch是我們自定義的擴展方法,后面會再介紹
  3. ViewModel中在請求的onRequest规阀,onSuccess,onError時會通過_viewStates更新頁面,通過_viewEvents添加一次性事件恒序,如Toast
  4. View層只需要監(jiān)聽ViewStateViewEvent并更新UI,頁面的邏輯全都在ViewModel中寫

通過使用MVI架構,所有的邏輯都在ViewModel中處理谁撼,同時添加新狀態(tài)時不需要添加LiveData,降低了ViewViewModel的通信成本歧胁,解決了MVVM架構下的一些問題

局部網絡請求

我們頁面中通常會有一些局部網絡請求,例如點贊厉碟,收藏等喊巍,這些網絡請求不需要刷新整個頁面,只需要處理單個View的狀態(tài)或者彈出Toast
下面我們來看下MVI架構下是如何實現(xiàn)的

    /**
     * 頁面局部請求箍鼓,例如點贊收藏等崭参,通常需要彈dialog或toast
     */
    private fun partRequest() {
        viewModelScope.rxLaunch<String> {
            onRequest = {
                _viewEvents.setEvent(NetworkViewEvent.ShowLoadingDialog)
                delay(2000)
                "點贊成功"
            }
            onSuccess = {
                _viewEvents.setEvent(NetworkViewEvent.DismissLoadingDialog)
                _viewEvents.setEvent(NetworkViewEvent.ShowToast(it))
                _viewStates.setState { copy(content = it) }
            }
            onError = {
                _viewEvents.setEvent(NetworkViewEvent.DismissLoadingDialog)
            }
        }
    }

如上,針對局部網絡請求款咖,我們也是通過_viewStates_viewEvents更新UI何暮,并不需要添加額外的LiveData,使用起來比較方便

多數(shù)據(jù)源請求

頁面中通常也會有一些多數(shù)據(jù)源的請求铐殃,我們可以利用協(xié)程的async操作符處理

    /**
     * 多數(shù)據(jù)源請求
     */
    private fun multiSourceRequest() {
        viewModelScope.rxLaunch<String> {
            onRequest = {
                _viewEvents.setEvent(NetworkViewEvent.ShowLoadingDialog)
                coroutineScope {
                    val source1 = async { source1() }
                    val source2 = async { source2() }
                    val result = source1.await() + "," + source2.await()
                    result
                }
            }
            onSuccess = {
                _viewEvents.setEvent(NetworkViewEvent.DismissLoadingDialog)
                _viewEvents.setEvent(NetworkViewEvent.ShowToast(it))
                _viewStates.setState { copy(content = it) }
            }
            onError = {
                _viewEvents.setEvent(NetworkViewEvent.DismissLoadingDialog)
            }
        }
    }

異常處理

我們的APP中通常需要一些通用的異常處理,我們可以封裝在rxLaunch擴展方法中

class CoroutineScopeHelper<T>(private val coroutineScope: CoroutineScope) {
    fun rxLaunch(init: LaunchBuilder<T>.() -> Unit): Job {
        val result = LaunchBuilder<T>().apply(init)
        val handler = NetworkExceptionHandler {
            result.onError?.invoke(it)
        }
        return coroutineScope.launch(handler) {
            val res: T = result.onRequest()
            result.onSuccess?.invoke(res)
        }
    }
}

如上:

  1. rxLaunch就是我們定義的擴展方法海洼,本質就是將協(xié)程轉化為類RxJava的回調
  2. 通用的異常處理可寫在自定義的NetworkExceptionHandler中,如果請求錯誤則會自動處理
  3. 處理后的異常將傳遞到onError中,供我們進一步處理

MVI架構與Flow結合實現(xiàn)網絡請求

我們上面通過自定義擴展函數(shù)實現(xiàn)了rxLaunch富腊,其實是將協(xié)程轉化為類RXJava的寫法坏逢,但其實kotin協(xié)程已經有了自己的RXJava : Flow
我們完全可以利用Flow來實現(xiàn)同樣的功能,不需要自己自定義

簡單的網絡請求

    /**
     * 頁面請求蟹肘,通常包括刷新頁面loading狀態(tài)等
     */
    private fun pageRequest() {
        viewModelScope.launch {
            flow {
                delay(2000)
                emit("頁面請求成功")
            }.onStart {
                _viewStates.setState { copy(pageStatus = PageStatus.Loading) }
            }.onEach {
                _viewStates.setState { copy(content = it, pageStatus = PageStatus.Success) }
                _viewEvents.setEvent(NetworkViewEvent.ShowToast(it))
            }.commonCatch {
                _viewStates.setState { copy(pageStatus = PageStatus.Error(it)) }
            }.collect()
        }
    }
  1. flow中發(fā)起網絡請求并將結果通過emit回調
  2. onStart是請求的開始词疼,這里觸發(fā)Activity中的showLoading
  3. onEach中獲取flowemit的結果,即成功回調帘腹,在這里更新請求狀態(tài)與頁面數(shù)據(jù)
  4. commonCatch中捕獲異常
  5. 局部的網絡請求與這里類似贰盗,并且不需要添加額外的LiveData,這里就不綴述了

多數(shù)據(jù)源網絡請求

Flow中提供了多個操作符阳欲,可以將多個Flow的結果組合起來

    /**
     * 多數(shù)據(jù)源請求
     */
    private fun multiSourceRequest() {
        viewModelScope.launch {
            val flow1 = flow {
                delay(1000)
                emit("數(shù)據(jù)源1")
            }
            val flow2 = flow {
                delay(2000)
                emit("數(shù)據(jù)源2")
            }
            flow1.zip(flow2) { a, b ->
                "$a,$b"
            }.onStart {
                _viewEvents.setEvent(NetworkViewEvent.ShowLoadingDialog)
            }.onEach {
                _viewEvents.setEvent(NetworkViewEvent.DismissLoadingDialog)
                _viewEvents.setEvent(NetworkViewEvent.ShowToast(it))
                _viewStates.setState { copy(content = it) }
            }.commonCatch {
                _viewEvents.setEvent(NetworkViewEvent.DismissLoadingDialog)
            }.collect()
        }
    }

如上舵盈,我們通過zip操作符組合兩個Flow陋率,它將合并兩個Flow的結果并回調,我們在onEach中將得到數(shù)據(jù)源1,數(shù)據(jù)源2

異常處理

跟上面一樣,有時我們需要配置一些能用的異常處理秽晚,可以看到瓦糟,我們在上面調用了commonCatch,這其實也是我們自定義的一個擴展函數(shù)

fun <T> Flow<T>.commonCatch(action: suspend FlowCollector<T>.(cause: Throwable) -> Unit): Flow<T> {
    return this.catch {
        if (it is UnknownHostException || it is SocketTimeoutException) {
            MyApp.get().toast("發(fā)生網絡錯誤赴蝇,請稍后重試")
        } else {
            MyApp.get().toast("請求失敗菩浙,請重試")
        }
        action(it)
    }
}

如上所示,其實是對Flow.catch的一個封裝句伶,讀者可以根據(jù)自己的需求封裝處理

關于Repository

可以看到劲蜻,我上面都沒有使用到Repository,都是直接在ViewModel層中處理
平常在項目開發(fā)中也可以發(fā)現(xiàn)考余,一般的頁面并沒有寫Repository的需要先嬉,直接在ViewModel中處理即可

但如果數(shù)據(jù)獲取比較復雜,比如同時從網絡與本地數(shù)據(jù)獲取楚堤,或者需要復用網絡請求等時疫蔓,也可以添加一個Repository
我們可以通過Repository獲取數(shù)據(jù)后,再通過_viewState更新頁面狀態(tài)身冬,如下所示

    private fun fetchNews() {
        viewModelScope.launch {
            flow {
                emit(repository.getMockApiResponse())
            }.onStart {
                _viewStates.setState { copy(fetchStatus = FetchStatus.Fetching) }
            }.onEach {
                _viewStates.setState { copy(fetchStatus = FetchStatus.Fetched, newsList = it.data)}
            }.commonCatch {
                _viewStates.setState { copy(fetchStatus = FetchStatus.Fetched) }
            }.collect()
        }
    }

總結

MVVM架構下一般使用StateLiveData來進行網絡架構封裝衅胀,并在View層監(jiān)聽回調,這種封裝方式的問題在于將網絡請求回調處理邏輯轉移到了View層吏恭,違背了盡量不在View層寫邏輯的原則
但這種寫法流行的原因在于MVVM架構下ViewViewModel交互成本較高拗小,如果每個請求的回調都在ViewModel中處理,則需要定義很多LiveData樱哼,這是很多人不愿意做的

MVI架構解決了這個問題,將頁面所有狀態(tài)放在一個ViewState中剿配,對外也只需要暴露一個LiveData
MVI配合Flow或者自定義擴展函數(shù)搅幅,可以將頁面邏輯全部放在ViewModel中,View層只需要監(jiān)聽LiveData的屬性并刷新UI即可
當頁面需要添加狀態(tài)時呼胚,只需要給ViewState添加一個屬性而不是添加兩個LiveData,降低了ViewViewModel的交互成本

如果你也覺得在View層監(jiān)聽網絡請求回調不是一個很好的設計的話茄唐,那么可以嘗試使用一下MVI架構

如果本文對你有所幫助,歡迎點贊關注Star~

項目地址

本文所有代碼可見:github.com/shenzhen201…

?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末蝇更,一起剝皮案震驚了整個濱河市沪编,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌年扩,老刑警劉巖蚁廓,帶你破解...
    沈念sama閱讀 221,635評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異厨幻,居然都是意外死亡相嵌,警方通過查閱死者的電腦和手機腿时,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,543評論 3 399
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來饭宾,“玉大人批糟,你說我怎么就攤上這事】疵” “怎么了徽鼎?”我有些...
    開封第一講書人閱讀 168,083評論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀的道長弹惦。 經常有香客問我否淤,道長,這世上最難降的妖魔是什么肤频? 我笑而不...
    開封第一講書人閱讀 59,640評論 1 296
  • 正文 為了忘掉前任叹括,我火速辦了婚禮,結果婚禮上宵荒,老公的妹妹穿的比我還像新娘汁雷。我一直安慰自己,他們只是感情好报咳,可當我...
    茶點故事閱讀 68,640評論 6 397
  • 文/花漫 我一把揭開白布侠讯。 她就那樣靜靜地躺著,像睡著了一般暑刃。 火紅的嫁衣襯著肌膚如雪厢漩。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,262評論 1 308
  • 那天岩臣,我揣著相機與錄音溜嗜,去河邊找鬼。 笑死架谎,一個胖子當著我的面吹牛炸宵,可吹牛的內容都是我干的。 我是一名探鬼主播谷扣,決...
    沈念sama閱讀 40,833評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼土全,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了会涎?” 一聲冷哼從身側響起裹匙,我...
    開封第一講書人閱讀 39,736評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎末秃,沒想到半個月后概页,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經...
    沈念sama閱讀 46,280評論 1 319
  • 正文 獨居荒郊野嶺守林人離奇死亡蛔溃,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 38,369評論 3 340
  • 正文 我和宋清朗相戀三年绰沥,在試婚紗的時候發(fā)現(xiàn)自己被綠了篱蝇。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,503評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡徽曲,死狀恐怖零截,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情秃臣,我是刑警寧澤涧衙,帶...
    沈念sama閱讀 36,185評論 5 350
  • 正文 年R本政府宣布,位于F島的核電站奥此,受9級特大地震影響弧哎,放射性物質發(fā)生泄漏。R本人自食惡果不足惜稚虎,卻給世界環(huán)境...
    茶點故事閱讀 41,870評論 3 333
  • 文/蒙蒙 一撤嫩、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧蠢终,春花似錦序攘、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,340評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至祭钉,卻和暖如春瞄沙,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背慌核。 一陣腳步聲響...
    開封第一講書人閱讀 33,460評論 1 272
  • 我被黑心中介騙來泰國打工距境, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人垮卓。 一個月前我還...
    沈念sama閱讀 48,909評論 3 376
  • 正文 我出身青樓肮疗,卻偏偏與公主長得像,于是被迫代替她去往敵國和親扒接。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 45,512評論 2 359

推薦閱讀更多精彩內容