MVI 架构封装:快速优雅地实现网络请求
Posted 涂程
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了MVI 架构封装:快速优雅地实现网络请求相关的知识,希望对你有一定的参考价值。
好文推荐
作者:RicardoMJiang
前言
网络请求可以说是android
开发中最常见的需求之一,基本上每个页面都需要发起几个网络请求。
因此大家通常都会对网络请求进行一定的封装,解决模板代码过多,重复代码,异常捕获等一些问题。
我们这次一起来看下MVI
架构下如何对网络请求进行封装,以及相对于MVVM
架构有什么优势
本文主要包括以下内容
MVVM
架构下的网络请求封装与问题MVI
架构下封装网络请求MVI
架构与Flow
结合实现网络请求
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")
// 注册监听
mViewModel.userLiveData.observeState(this) {
onLoading {
showLoading()
}
onSuccess {data ->
mBinding.tvContent.text = data.toString()
}
onError {
dismissLoading()
}
}
}
}
如上所示,就是最常见的MVVM
架构下网络请求封装,主要思路如是
- 添加一个
StateLiveData
,一个LiveData
支持多种状态,例如加载中,加载成功,加载失败等 - 在页面中监听
StateLiveData
,在页面中处理onLoading
,onSuccess
,onError
等逻辑
这种封装的本质其实就是将请求的回调逻辑处理迁移到View
层了
这其实并不是我们想要的,我们的理想状况应该是逻辑尽量放在ViewModel
中,View
层只需要监听ViewModel
层并更新UI
既然这种封装其实违背了不在View
层写逻辑的原则,那么为什么还有那么多人用呢?
本质上是因为ViewModel
层与View
层的通信成本比较高
想象一下,如果我们不使用StateLiveData
,针对每个请求就需要新建一个LiveData
来表示请求状态,如果成功或失败后需要弹Toast
或者Dialog
,或者页面中有多个请求,就需要定义更多的LiveData
, 同时为了保证对外暴露的LiveData
不可变,每个状态都需要定义两遍LiveData
这就是为什么这种封装其实违背了不在View
层写逻辑但仍然流行的原因,因为在MVVM
架构中每处理一种状态,就需要添加两个LiveData
,成本较高,大多数人并不愿意支付这个成本
而MVI
架构正解决了这个问题
MVI
架构下封装网络请求
之前已经介绍过了MVI
架构,MVI
架构使用方面我们就不再多说,我们直接来看下MVI
架构下怎么发起一个简单网络请求
简单的网络请求
class NetworkViewModel : ViewModel() {
/**
* 页面请求,通常包括刷新页面loading状态等
*/
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 ->
//监听网络请求状态
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()
}
}
//监听页面数据
state.observeState(this, NetworkViewState::content) {
tv_content.text = it
}
}
//监听一次性事件,如Toast,ShowDialog等
viewModel.viewEvents.observe(this) {
when (it) {
is NetworkViewEvent.ShowToast -> toast(it.message)
is NetworkViewEvent.ShowLoadingDialog -> showLoadingDialog()
is NetworkViewEvent.DismissLoadingDialog -> dismissLoadingDialog()
}
}
}
}
如上,代码很简单
- 页面的所有状态都存储在
NetworkViewState
中,后面如果需要添加状态不需要添加LiveData
,添加属性即可,NetworkViewEvent
中存储了所有一次事件,同理 ViewModel
中发起网络请求并监听网络请求回调,其中viewModelScope.rxLaunch
是我们自定义的扩展方法,后面会再介绍ViewModel
中在请求的onRequest
,onSuccess
,onError
时会通过_viewStates
更新页面,通过_viewEvents
添加一次性事件,如Toast
View
层只需要监听ViewState
与ViewEvent
并更新UI
,页面的逻辑全都在ViewModel
中写
通过使用MVI
架构,所有的逻辑都在ViewModel
中处理,同时添加新状态时不需要添加LiveData
,降低了View
与ViewModel
的通信成本,解决了MVVM
架构下的一些问题
局部网络请求
我们页面中通常会有一些局部网络请求,例如点赞,收藏等,这些网络请求不需要刷新整个页面,只需要处理单个View
的状态或者弹出Toast
下面我们来看下MVI
架构下是如何实现的
/**
* 页面局部请求,例如点赞收藏等,通常需要弹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
,使用起来比较方便
多数据源请求
页面中通常也会有一些多数据源的请求,我们可以利用协程的async
操作符处理
/**
* 多数据源请求
*/
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
就是我们定义的扩展方法,本质就是将协程转化为类RxJava
的回调- 通用的异常处理可写在自定义的
NetworkExceptionHandler
中,如果请求错误则会自动处理 - 处理后的异常将传递到
onError
中,供我们进一步处理
MVI
架构与Flow
结合实现网络请求
我们上面通过自定义扩展函数实现了rxLaunch
,其实是将协程转化为类RXJava
的写法,但其实kotin
协程已经有了自己的RXJava
: Flow
我们完全可以利用Flow
来实现同样的功能,不需要自己自定义
简单的网络请求
/**
* 页面请求,通常包括刷新页面loading状态等
*/
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
中发起网络请求并将结果通过emit
回调 onStart
是请求的开始,这里触发Activity
中的showLoading
- 在
onEach
中获取flow
中emit
的结果,即成功回调,在这里更新请求状态与页面数据 - 在
commonCatch
中捕获异常 - 局部的网络请求与这里类似,并且不需要添加额外的
LiveData
,这里就不缀述了
多数据源网络请求
Flow
中提供了多个操作符,可以将多个Flow
的结果组合起来
/**
* 多数据源请求
*/
private fun multiSourceRequest() {
viewModelScope.launch {
val flow1 = flow {
delay(1000)
emit("数据源1")
}
val flow2 = flow {
delay(2000)
emit("数据源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
中将得到数据源1,数据源2
异常处理
跟上面一样,有时我们需要配置一些能用的异常处理,可以看到,我们在上面调用了commonCatch
,这其实也是我们自定义的一个扩展函数
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("发生网络错误,请稍后重试")
} else {
MyApp.get().toast("请求失败,请重试")
}
action(it)
}
}
如上所示,其实是对Flow.catch
的一个封装,读者可以根据自己的需求封装处理
关于Repository
可以看到,我上面都没有使用到Repository
,都是直接在ViewModel
层中处理
平常在项目开发中也可以发现,一般的页面并没有写Repository
的需要,直接在ViewModel
中处理即可
但如果数据获取比较复杂,比如同时从网络与本地数据获取,或者需要复用网络请求等时,也可以添加一个Repository
我们可以通过Repository
获取数据后,再通过_viewState
更新页面状态,如下所示
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
层监听回调,这种封装方式的问题在于将网络请求回调处理逻辑转移到了View
层,违背了尽量不在View
层写逻辑的原则
但这种写法流行的原因在于MVVM
架构下View
与ViewModel
交互成本较高,如果每个请求的回调都在ViewModel
中处理,则需要定义很多LiveData
,这是很多人不愿意做的
而MVI
架构解决了这个问题,将页面所有状态放在一个ViewState
中,对外也只需要暴露一个LiveData
MVI
配合Flow
或者自定义扩展函数,可以将页面逻辑全部放在ViewModel
中,View
层只需要监听LiveData
的属性并刷新UI
即可
当页面需要添加状态时,只需要给ViewState
添加一个属性而不是添加两个LiveData
,降低了View
与ViewModel
的交互成本
如果你也觉得在View
层监听网络请求回调不是一个很好的设计的话,那么可以尝试使用一下MVI
架构
以上是关于MVI 架构封装:快速优雅地实现网络请求的主要内容,如果未能解决你的问题,请参考以下文章