关键词:
好文推荐
作者: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
架构
两种方式封装retrofit+协程,实现优雅快速的网络请求(代码片段)
目的简单调用、少写重复代码不依赖第三方库(只含Retrofit+Okhttp+协程)完全不懂协程也能立马上手(模板代码)用Kotlin的方式写Kotlin代码,什么意思呢?对比一下下面2个代码就知道了:mViewModel.w... 查看详情
优雅设计封装基于okhttp3的网络框架:httpheader接口设计实现及responserequest封装实现(代码片段)
...是网络框架编写工作并没有完成,此篇将完成Http核心架构,编写的新功能还是围绕在http请求上,涉及到的知识点:httpHeader的接口定义和实现http请求头和响应头访问编写http状态码定义http中的response封装、request接... 查看详情
compose+mvi+navigation快速实现wanandroid客户端(代码片段)
...ose应该是一个比较好的方式。本文主要基于Compose,MVI架构,单Activity架构等,快速实现一个wanAndroid客户端,如果对您有所帮助可以点个Star:wanAndroid-compose效果图首先看下效果图主要实现介绍各个页面的具体实现可以查... 查看详情
优雅设计封装基于okhttp3的网络框架(完):原生httpurlconnction请求多线程分发及数据转换(代码片段)
...下载,而这两篇文章实现另一大模块------Http基本框架封装,在上一篇博文中完成了HttpHeader的接口定义和实现、状态码定义及response、request接口封装和实现,定义了许多接口和抽象类,在接下来编码过程中会体现... 查看详情
androidjetpack系列之mvi架构(代码片段)
文章目录写在前面MVIvsMVVM新旧架构对比差异1、LiveData<T>改为Flow<UIState>差异2、交互规范MVI实战示例图定义UIState&编写ViewModelRepository数据支持View层总结完整示例代码资料写在前面在之前介绍MVVM的文章中,介绍了常... 查看详情
谈一谈在两个商业项目中使用mvi架构后的感悟(代码片段)
...,随着业务急速膨胀,代码越发混乱。试图用MVI架构+单向流形成掣肘带来一致风格。但这种做法不够以人为本,最终采用“在MVP的基础上进行了适当改造+设计约定的方式”解决了问题,并未将MVI投入到商业... 查看详情
谈一谈在两个商业项目中使用mvi架构后的感悟(代码片段)
...,随着业务急速膨胀,代码越发混乱。试图用MVI架构+单向流形成掣肘带来一致风格。但这种做法不够以人为本,最终采用“在MVP的基础上进行了适当改造+设计约定的方式”解决了问题,并未将MVI投入到商业... 查看详情
轻松搞定androidmvp架构okhttp网络模块封装的项目(代码片段)
CommonMvpcommonMvp能做什么?1、mvp实现modelviewpresenter业务和界面解耦2、整合网络请求3、简化网络调用流程4、整合状态栏和标题栏实现沉浸式状态栏5、Activity、Fragment中使用方法一致接口式封装生命周期1、有问题请提交isuue/(QQ:19... 查看详情
springbootjava优雅地实现接口数据校验(代码片段)
在工作中写过Java程序的朋友都知道,目前使用Java开发服务最主流的方式就是通过SpringMVC定义一个Controller层接口,并将接口请求或返回参数分别定义在一个Java实体类中,这样SpringMVC在接收到Http请求(POST/GET)后,就... 查看详情
优雅地处理重复请求(并发请求)(代码片段)
点击上方关注“终端研发部”设为“星标”,和你一起掌握更多数据库知识目录利用唯一请求编号去重业务参数去重计算请求参数的摘要作为参数标识继续优化,考虑剔除部分时间因子请求去重工具类,Java实现总结... 查看详情
优雅地处理重复请求(并发请求)(代码片段)
点击上方关注“终端研发部”设为“星标”,和你一起掌握更多数据库知识目录利用唯一请求编号去重业务参数去重计算请求参数的摘要作为参数标识继续优化,考虑剔除部分时间因子请求去重工具类,Java实现总结... 查看详情
springcloudribbon快速搭建(代码片段)
...板请求自动转换成客户端负载均衡的服务调用。在微服务架构中,业务都会被拆分成一个独立的服务,服务与服务的通讯是基于httprestful的。Springcloud有两种服务调用方式,一种是ribbon+rest 查看详情
androidmvi模式的封装实现(基于kotlinflow和viewmodel)(代码片段)
...w-Intent的缩写,它也是一种响应式+流式处理思想的架构。MVI的Model代表一种可订阅的状态模型的概念,添加了Intent概念来代表用户行为,采用单向数据流来控制数据流动和各层依赖关系。MVI中的单项数据流工作流程... 查看详情
androidmvi模式的封装实现(基于kotlinflow和viewmodel)(代码片段)
...w-Intent的缩写,它也是一种响应式+流式处理思想的架构。MVI的Model代表一种可订阅的状态模型的概念,添加了Intent概念来代表用户行为,采用单向数据流来控制数据流动和各层依赖关系。MVI中的单项数据流工作流程... 查看详情
轻松搞定androidmvp架构okhttp网络模块封装的项目(代码片段)
CommonMvpcommonMvp能做什么?1、mvp实现modelviewpresenter业务和界面解耦2、整合网络请求3、简化网络调用流程4、整合状态栏和标题栏实现沉浸式状态栏5、Activity、Fragment中使用方法一致接口式封装生命周期1、有问题请提交isuue/(QQ:19... 查看详情
从数据流角度管窥moya的实现:构建请求(代码片段)
相信大家都封装过网络层。虽然系统提供的网络库以及一些著名的第三方网络库(AFNetworking, Alamofire)已经能满足各种 HTTP/HTTPS的网络请求,但直接在代码里用起来,终归是比较晦涩,不是那么的顺手。所以我们都会倾向... 查看详情
如何优雅地关闭资源(代码片段)
很多时候我们都会用到io资源,比如文件、网络、各种连接等。比如有时候我们需要从一个文本文件中读取数据,一般的步骤是:用FileReader打开文件包装成BufferReader循环地从BufferReader中读取内容,直接读出来的内容为空关闭Buffer... 查看详情
google推荐使用mvi架构?卷起来了~
...itory,后续有多个数据源时再做拆分。数据层跟之前的MVVM架构下的数据层并没用什么区别, 查看详情