MVI 架构封装:快速优雅地实现网络请求

开发 架构
我们这次一起来看下MVI架构下如何对网络请求进行封装,以及相对于MVVM架构有什么优势。

前言  

网络请求可以说是Android开发中最常见的需求之一,基本上每个页面都需要发起几个网络请求。因此大家通常都会对网络请求进行一定的封装,解决模板代码过多,重复代码,异常捕获等一些问题。

前面我们介绍了MVI架构的主要原理与更佳实践。相关文章如下所示:

MVVM进阶版:MVI架构了解一下

我们这次一起来看下MVI架构下如何对网络请求进行封装,以及相对于MVVM架构有什么优势。

本文主要包括以下内容:

  1. MVVM架构下的网络请求封装与问题
  2. MVI架构下封装网络请求
  3. MVI架构与Flow结合实现网络请求

MVVM架构下的网络请求封装与问题

相信大家都看过不少MVVM架构下的网络请求封装,一般是这样写的。

# MainViewModel
class MainViewModel {
private val _userLiveData = MutableStateLiveData<User?>()
val userLiveData : StateLiveData<User?> = _userLiveData
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架构下网络请求封装,主要思路如是:

  1. 添加一个StateLiveData,一个LiveData支持多种状态,例如加载中,加载成功,加载失败等
  2. 在页面中监听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()
}
}
}
}

如上,代码很简单:

  1. 页面的所有状态都存储在NetworkViewState中,后面如果需要添加状态不需要添加LiveData,添加属性即可,NetworkViewEvent中存储了所有一次事件,同理
  2. ViewModel中发起网络请求并监听网络请求回调,其中viewModelScope.rxLaunch是我们自定义的扩展方法,后面会再介绍
  3. ViewModel中在请求的onRequest,onSuccess,onError时会通过_viewStates更新页面,通过_viewEvents添加一次性事件,如Toast
  4. 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)
}
}
}

 如上:

  1. rxLaunch就是我们定义的扩展方法,本质就是将协程转化为类RxJava的回调
  2. 通用的异常处理可写在自定义的NetworkExceptionHandler中,如果请求错误则会自动处理
  3. 处理后的异常将传递到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()
}
}

  1. 在flow中发起网络请求并将结果通过emit回调
  2. onStart是请求的开始,这里触发Activity中的showLoading
  3. 在onEach中获取flow中emit的结果,即成功回调,在这里更新请求状态与页面数据
  4. 在commonCatch中捕获异常
  5. 局部的网络请求与这里类似,并且不需要添加额外的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架构。

责任编辑:庞桂玉 来源: 安卓开发精选
相关推荐

2022-06-07 08:59:58

hookuseRequestReact 项目

2022-08-03 07:07:10

Spring数据封装框架

2011-08-16 15:06:43

IOS开发异步请求

2021-05-12 22:07:43

并发编排任务

2020-12-08 08:08:51

Java接口数据

2024-03-11 00:00:00

应用架构开发

2024-01-30 12:08:31

Go框架停止服务

2021-09-26 06:43:07

封装网络请求

2023-06-06 08:51:06

2021-03-24 10:20:50

Fonts前端代码

2020-07-07 07:33:12

Java单元集成

2024-11-13 16:37:00

Java线程池

2022-03-04 15:44:45

MVI 架构LiveData代码

2021-06-17 09:32:39

重复请求并发请求Java

2020-03-26 11:04:00

Linux命令光标

2021-01-18 13:17:04

鸿蒙HarmonyOSAPP

2023-11-22 13:05:12

Pytest测试

2022-05-13 21:20:23

组件库样式选择器

2020-03-27 15:10:23

SpringJava框架

2021-07-07 07:47:10

浏览器CSS兼容
点赞
收藏

51CTO技术栈公众号