mvi架构封装:快速优雅地实现网络请求(代码片段)

涂程 涂程     2023-02-16     676

关键词:

好文推荐
作者:RicardoMJiang

前言

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

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

本文主要包括以下内容

  1. MVVM架构下的网络请求封装与问题
  2. MVI架构下封装网络请求
  3. 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架构下网络请求封装,主要思路如是

  1. 添加一个StateLiveData,一个LiveData支持多种状态,例如加载中,加载成功,加载失败等
  2. 在页面中监听StateLiveData,在页面中处理onLoadingonSuccess,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中在请求的onRequestonSuccess,onError时会通过_viewStates更新页面,通过_viewEvents添加一次性事件,如Toast
  4. View层只需要监听ViewStateViewEvent并更新UI,页面的逻辑全都在ViewModel中写

通过使用MVI架构,所有的逻辑都在ViewModel中处理,同时添加新状态时不需要添加LiveData,降低了ViewViewModel的通信成本,解决了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中获取flowemit的结果,即成功回调,在这里更新请求状态与页面数据
  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架构下ViewViewModel交互成本较高,如果每个请求的回调都在ViewModel中处理,则需要定义很多LiveData,这是很多人不愿意做的

MVI架构解决了这个问题,将页面所有状态放在一个ViewState中,对外也只需要暴露一个LiveData
MVI配合Flow或者自定义扩展函数,可以将页面逻辑全部放在ViewModel中,View层只需要监听LiveData的属性并刷新UI即可
当页面需要添加状态时,只需要给ViewState添加一个属性而不是添加两个LiveData,降低了ViewViewModel的交互成本

如果你也觉得在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架构下的数据层并没用什么区别, 查看详情