阅读 118

Retrofit + Kotlin + MVVM 的网络请求框架的封装尝试

1、前言

之前在学习郭霖《第一行代码》时按部就班地写过一个彩云天气 App,对里面的网络请求框架的封装印象非常深刻,很喜欢这种 Retrofit + Kotlin + 协程的搭配使用。随后也在自己的项目里参考了这部分的代码。但随着代码的深入编写和功能的复杂,原来的框架已经无法满足我的使用了。原主要有如下的痛点:

  • 缺少失败的回调

  • 显示加载中动画比较麻烦

后面我自己试着努力去封装一个简单易用的框架,可惜个人能力有限,自己封装的框架总是不如人意。好在还有很多优秀的博客和代码可供参考。在此基础上,对彩云天气 App中的网络请求框架做了一些修改,尽可能地做到简单易用。以请求玩安卓的登录接口为例(用户名和密码是我自己申请的,见代码),页面上有一个按钮,点击按钮后就发起登录请求。

先来看看发起请求后的回调怎么写:

viewModel.loginLiveData.observeState(this) {     onStart {         LoadingDialog.show(activity)         Log.d(TAG, "请求开始")     }     onSuccess {         Log.d(TAG, "请求成功")         showToast("登录成功")         binding.tvResult.text = it.toString()     }     onEmpty {         showToast("数据为空")     }     onFailure {         Log.d(TAG, "请求失败")         showToast(it.errorMsg.orEmpty())         binding.tvResult.text = it.toString()     }     onFinish {         LoadingDialog.dismiss(activity)         Log.d(TAG, "请求结束")     } } 复制代码

回调一共有五种,会在下文详细介绍。这里采用了DSL的写法,如果你喜欢传统的写法,可以调用另外一个扩展方法observeResponse(),由于它最后一个参数就是请求成功的回调,所以借助 Lambda 表达式的特性,可以简洁地写成如下的形式:

viewModel.loginLiveData.observeResponse(this){     binding.tvResult.text = it.toString() } 复制代码

如果还需要其他回调,可以使用具名参数加上,如下所示:

viewModel.loginLiveData.observeResponse(this, onStart = {     LoadingDialog.show(this) }, onFinish = {     LoadingDialog.dismiss(activity) }) {     binding.tvResult.text = it.toString() } 复制代码

2、框架搭建

开始之前必须说明,这个框架是基于《第一行代码》(第三版)中的彩云天气 App的,它的架构图如下所示,如果你阅读过《第一行代码》或者谷歌的相关文档,那么想必对此不会陌生。

MVVM架构图.png

2.1 添加依赖库

//简化在 Activity 中声明 ViewModel 的代码 implementation "androidx.activity:activity-ktx:1.3.1" // lifecycle def lifecycle_version = "2.3.1" implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version" implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version" // retrofit2 def retrofit_version = "2.9.0" implementation "com.squareup.retrofit2:retrofit:$retrofit_version" implementation "com.squareup.retrofit2:adapter-rxjava2:$retrofit_version" implementation "com.squareup.retrofit2:converter-gson:$retrofit_version" implementation 'io.reactivex.rxjava2:rxandroid:2.1.1' // okhttp def okhttp_version = "4.8.1" implementation "com.squareup.okhttp3:okhttp:$okhttp_version" //日志拦截器 implementation('com.github.ihsanbal:LoggingInterceptor:3.1.0') {     exclude group: 'org.json', module: 'json' } 复制代码

2.2 Retrofit构建器

Retrofit构建器这里做了分层,基类做了一些基本的配置,子类继承后可以添加新的配置,并配置自己喜欢的日志拦截器。

private const val TIME_OUT_LENGTH = 8L private const val BASE_URL = "https://www.wanandroid.com/" abstract class BaseRetrofitBuilder {     private val okHttpClient: OkHttpClient by lazy {         val builder = OkHttpClient.Builder()             .callTimeout(TIME_OUT_LENGTH, TimeUnit.SECONDS)             .connectTimeout(TIME_OUT_LENGTH, TimeUnit.SECONDS)             .readTimeout(TIME_OUT_LENGTH, TimeUnit.SECONDS)             .writeTimeout(TIME_OUT_LENGTH, TimeUnit.SECONDS)             .retryOnConnectionFailure(true)         initLoggingInterceptor()?.also {             builder.addInterceptor(it)         }         handleOkHttpClientBuilder(builder)         builder.build()     }     private val retrofit = Retrofit.Builder()         .baseUrl(BASE_URL)         .addConverterFactory(GsonConverterFactory.create())         .client(okHttpClient)         .build()     fun <T> create(serviceClass: Class<T>): T = retrofit.create(serviceClass)     inline fun <reified T> create(): T = create(T::class.java)     /**      * 子类自定义 OKHttpClient 的配置      */     abstract fun handleOkHttpClientBuilder(builder: OkHttpClient.Builder)     /**      * 配置日志拦截器      */     abstract fun initLoggingInterceptor(): Interceptor? } 复制代码

RetrofitBuilder

private const val LOG_TAG_HTTP_REQUEST = "okhttp_request" private const val LOG_TAG_HTTP_RESULT = "okhttp_result" object RetrofitBuilder : BaseRetrofitBuilder() {     override fun handleOkHttpClientBuilder(builder: OkHttpClient.Builder) {}     override fun initLoggingInterceptor()= LoggingInterceptor         .Builder()         .setLevel(Level.BASIC)         .log(Platform.INFO)         .request(LOG_TAG_HTTP_REQUEST)         .response(LOG_TAG_HTTP_RESULT)         .build() } 复制代码

2.3 全局异常处理

请求时可能会遇到诸如网络断开、Json 解析失败等意外情况,如果我们每次请求都要处理一遍这些异常,那也未免太麻烦了。正确的做法是把异常集中到一起处理。

创建一个定义各种异常的枚举类:

enum class HttpError(val code: Int, val message: String){     UNKNOWN(-100,"未知错误"),     NETWORK_ERROR(1000, "网络连接超时,请检查网络"),     JSON_PARSE_ERROR(1001, "Json 解析失败")     //······ } 复制代码

创建一个文件,在里面定义一个全局方法,用于处理各种异常:

fun handleException(throwable: Throwable) = when (throwable) {     is UnknownHostException -> RequestException(HttpError.NETWORK_ERROR, throwable.message)     is HttpException -> {         val errorModel = throwable.response()?.errorBody()?.string()?.run {             Gson().fromJson(this, ErrorBodyModel::class.java)         } ?: ErrorBodyModel()         RequestException(errorMsg = errorModel.message, error = errorModel.error)     }     is JsonParseException -> RequestException(HttpError.JSON_PARSE_ERROR, throwable.message)     is RequestException -> throwable     else -> RequestException(HttpError.UNKNOWN, throwable.message) } 复制代码

实际项目中遇到的异常当然不止这几个,这里只是作为举例写了少部分,实际开放中把它丰富完善即可。

2.4 回调状态监听

回调状态一共有四种:

  • onStart():请求开始(可在此展示加载动画)

  • onSuccess():请求成功

  • onEmpty():请求成功,但datanull或者data是集合类型但为空

  • onFailure():请求失败

  • onFinish():请求结束(可在此关闭加载动画)

这里要注意onSuccess的标准:并不仅仅是 Http 请求的结果码(status code)等于 200,而且要达到Api请求成功的标准,以玩安卓的Api 为例,errorCode 为 0时,发起的请求才是执行成功;否则,都应该归为onFailure()的情况(可以参考文章附带的思维导图)。

理清楚有几种回调状态后,就可以实施监听了。那么在哪里监听呢?LiveDataobserve()方法的第二个函数可以传入Observer参数。Observer是一个接口,我们继承它自定义一个Oberver,借此我们就可以监听LiveData的值的变化。

interface IStateObserver<T> : Observer<BaseResponse<T>> {     override fun onChanged(response: BaseResponse<T>?) {         when (response) {             is StartResponse -> {                 //onStart()回调后不能直接就调用onFinish(),必须等待请求结束                 onStart()                 return             }             is SuccessResponse -> onSuccess(response.data)             is EmptyResponse -> onEmpty()             is FailureResponse -> onFailure(response.exception)         }         onFinish()     }     /**      * 请求开始      */     fun onStart()     /**      * 请求成功,且 data 不为 null      */     fun onSuccess(data: T)     /**      * 请求成功,但 data 为 null 或者 data 是集合类型但为空      */     fun onEmpty()     /**      * 请求失败      */     fun onFailure(e: RequestException)     /**      * 请求结束      */     fun onFinish() } 复制代码

接下来我们准备一个HttpRequestCallback类,用于实现DSL的回调形式:

typealias OnSuccessCallback<T> = (data: T) -> Unit typealias OnFailureCallback = (e: RequestException) -> Unit typealias OnUnitCallback = () -> Unit class HttpRequestCallback<T> {     var startCallback: OnUnitCallback? = null     var successCallback: OnSuccessCallback<T>? = null     var emptyCallback: OnUnitCallback? = null     var failureCallback: OnFailureCallback? = null     var finishCallback: OnUnitCallback? = null     fun onStart(block: OnUnitCallback) {         startCallback = block     }     fun onSuccess(block: OnSuccessCallback<T>) {         successCallback = block     }     fun onEmpty(block: OnUnitCallback) {         emptyCallback = block     }     fun onFailure(block: OnFailureCallback) {         failureCallback = block     }     fun onFinish(block: OnUnitCallback) {         finishCallback = block     } } 复制代码

然后声明新的监听方法,考虑到某些时候需要自定义的LiveData(比如为了解决数据倒灌的问题),这里采用扩展函数的写法,便于扩展。

/**  * 监听 LiveData 的值的变化,回调为 DSL 的形式  */ inline fun <T> LiveData<BaseResponse<T>>.observeState(     owner: LifecycleOwner,     crossinline callback: HttpRequestCallback<T>.() -> Unit ) {     val requestCallback = HttpRequestCallback<T>().apply(callback)     observe(owner, object : IStateObserver<T> {         override fun onStart() {             requestCallback.startCallback?.invoke()         }         override fun onSuccess(data: T) {             requestCallback.successCallback?.invoke(data)         }         override fun onEmpty() {             requestCallback.emptyCallback?.invoke()         }         override fun onFailure(e: RequestException) {             requestCallback.failureCallback?.invoke(e)         }         override fun onFinish() {             requestCallback.finishCallback?.invoke()         }     }) } /**  * 监听 LiveData 的值的变化  */ inline fun <T> LiveData<BaseResponse<T>>.observeResponse(     owner: LifecycleOwner,     crossinline onStart: OnUnitCallback = {},     crossinline onEmpty: OnUnitCallback = {},     crossinline onFailure: OnFailureCallback = { e: RequestException -> },     crossinline onFinish: OnUnitCallback = {},     crossinline onSuccess: OnSuccessCallback<T> ) {     observe(owner, object : IStateObserver<T> {         override fun onStart() {             onStart()         }         override fun onSuccess(data: T) {             onSuccess(data)         }         override fun onEmpty() {             onEmpty()         }         override fun onFailure(e: RequestException) {             onFailure(e)         }         override fun onFinish() {             onFinish()         }     }) } 复制代码

2.5 Repository 层的封装

Repository层作为数据的来源,有个两个渠道:网络请求和数据库。这里暂时只处理了网络请求。

基类Repository

abstract class BaseRepository {     protected fun <T> fire(         context: CoroutineContext = Dispatchers.IO,         block: suspend () -> BaseResponse<T>     ): LiveData<BaseResponse<T>> = liveData(context) {         this.runCatching {             emit(StartResponse())             block()         }.onSuccess {             //status code 为200,继续判断 errorCode 是否为 0             emit(                 when (it.success) {                     true -> checkEmptyResponse(it.data)                     false -> FailureResponse(handleException(RequestException(it)))                 }             )         }.onFailure { throwable ->             emit(FailureResponse(handleException(throwable)))         }     }     /**      * data 为 null,或者 data 是集合类型,但是集合为空都会进入 onEmpty 回调      */     private fun <T> checkEmptyResponse(data: T?): ApiResponse<T> =         if (data == null || (data is List<*> && (data as List<*>).isEmpty())) {             EmptyResponse()         } else {             SuccessResponse(data)         } } 复制代码

子类Repository:

object Repository : BaseRepository() {     fun login(pwd: String) = fire {         NetworkDataSource.login(pwd)     } } 复制代码

网络请求数据源,在这里调用网络接口:

object NetworkDataSource {     private val apiService = RetrofitBuilder.create<ApiService>()     suspend fun login(pwd: String) = apiService.login(password = pwd) } 复制代码

2.6 ViewModel层的封装

ViewModel基本遵循了《第一行代码》中的写法,创建了两个LiveData。用户点击按钮时,loginAction的值就会发生改变,触发switchMap中的代码,从而达到请求数据的目的。

class MainViewModel : ViewModel() {     private val loginAction = MutableLiveData<Boolean>()     /**      * loginAction 在这里只传递布尔值,不传递密码,在实际项目中,会使用 DataBinding 绑定 xml 布局和 ViewModel,      * 不需要从 Activity 或者 Fragment 中把密码传入 ViewModel      */     val loginLiveData = loginAction.switchMap {         if (it) {             Repository.login("PuKxVxvMzBp2EJM")         } else {             Repository.login("123456")         }     }     /**      * 点击登录      */     fun login() {         loginAction.value = true     }     fun loginWithWrongPwd() {         loginAction.value = false     } } 复制代码

注意:这种写法通常不从View向ViewModel层传递数据,是需要搭配DataBinding 的。如果你不想这样写,可以修改BaseRepository中的返回值,直接返回BaseResponse

3、思维导图及源码

最后,用一张思维导图总结本文:

网络请求框架1.0.png

源码地址:GitHub (注意分支要选择 dev1.0)


作者:士兵24
链接:https://juejin.cn/post/7027675216080240647


文章分类
代码人生
版权声明:本站是系统测试站点,无实际运营。本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 XXXXXXo@163.com 举报,一经查实,本站将立刻删除。
相关推荐