作者: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)勢
本文主要包括以下內容
-
MVVM
架構下的網絡請求封裝與問題 -
MVI
架構下封裝網絡請求 -
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
架構下網絡請求封裝晨另,主要思路如是
- 添加一個
StateLiveData
,一個LiveData
支持多種狀態(tài)潭千,例如加載中,加載成功借尿,加載失敗等 - 在頁面中監(jiān)聽
StateLiveData
刨晴,在頁面中處理onLoading
,onSuccess
,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()
}
}
}
}
如上,代碼很簡單
- 頁面的所有狀態(tài)都存儲在
NetworkViewState
中蹬铺,后面如果需要添加狀態(tài)不需要添加LiveData
尝哆,添加屬性即可,NetworkViewEvent
中存儲了所有一次事件甜攀,同理 -
ViewModel
中發(fā)起網絡請求并監(jiān)聽網絡請求回調秋泄,其中viewModelScope.rxLaunch
是我們自定義的擴展方法,后面會再介紹 -
ViewModel
中在請求的onRequest
规阀,onSuccess
,onError
時會通過_viewStates
更新頁面,通過_viewEvents
添加一次性事件恒序,如Toast
-
View
層只需要監(jiān)聽ViewState
與ViewEvent
并更新UI
,頁面的邏輯全都在ViewModel
中寫
通過使用MVI
架構,所有的邏輯都在ViewModel
中處理谁撼,同時添加新狀態(tài)時不需要添加LiveData
,降低了View
與ViewModel
的通信成本歧胁,解決了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)
}
}
}
如上:
-
rxLaunch
就是我們定義的擴展方法海洼,本質就是將協(xié)程轉化為類RxJava
的回調 - 通用的異常處理可寫在自定義的
NetworkExceptionHandler
中,如果請求錯誤則會自動處理 - 處理后的異常將傳遞到
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()
}
}
- 在
flow
中發(fā)起網絡請求并將結果通過emit
回調 -
onStart
是請求的開始词疼,這里觸發(fā)Activity
中的showLoading
- 在
onEach
中獲取flow
中emit
的結果,即成功回調帘腹,在這里更新請求狀態(tài)與頁面數(shù)據(jù) - 在
commonCatch
中捕獲異常 - 局部的網絡請求與這里類似贰盗,并且不需要添加額外的
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
架構下View
與ViewModel
交互成本較高拗小,如果每個請求的回調都在ViewModel
中處理,則需要定義很多LiveData
樱哼,這是很多人不愿意做的
而MVI
架構解決了這個問題,將頁面所有狀態(tài)放在一個ViewState
中剿配,對外也只需要暴露一個LiveData
MVI
配合Flow
或者自定義擴展函數(shù)搅幅,可以將頁面邏輯全部放在ViewModel
中,View
層只需要監(jiān)聽LiveData
的屬性并刷新UI
即可
當頁面需要添加狀態(tài)時呼胚,只需要給ViewState
添加一個屬性而不是添加兩個LiveData
,降低了View
與ViewModel
的交互成本
如果你也覺得在View
層監(jiān)聽網絡請求回調不是一個很好的設計的話茄唐,那么可以嘗試使用一下MVI
架構
如果本文對你有所幫助,歡迎點贊關注Star
~
項目地址
本文所有代碼可見:github.com/shenzhen201…