Retrofit + Kotlin + MVVM 的网络请求框架的封装尝试
1、前言
之前在学习郭霖《第一行代码》时按部就班地写过一个彩云天气 App,对里面的网络请求框架的封装印象非常深刻,很喜欢这种 Retrofit
+ Kotlin
+ 协程的搭配使用。随后也在自己的项目里参考了这部分的代码。但随着代码的深入编写和功能的复杂,原来的框架已经无法满足我的使用了。原主要有如下的痛点:
- 缺少失败的回调
- 显示加载中动画比较麻烦
先来看看发起请求后的回调怎么写:
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的,它的架构图如下所示,如果你阅读过《第一行代码》或者谷歌的相关文档,那么想必对此不会陌生。
文章图片
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 = 8Lprivate 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 create(serviceClass: Class): T = retrofit.create(serviceClass)inline fun 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()
:请求成功,但data
为null
或者data
是集合类型但为空onFailure()
:请求失败onFinish()
:请求结束(可在此关闭加载动画)
onSuccess
的标准:并不仅仅是 Http 请求的结果码(status code)等于 200,而且要达到Api请求成功的标准,以玩安卓的Api 为例,errorCode
为 0时,发起的请求才是执行成功;否则,都应该归为onFailure()
的情况(可以参考文章附带的思维导图)。理清楚有几种回调状态后,就可以实施监听了。那么在哪里监听呢?
LiveData
的observe()
方法的第二个函数可以传入Observer
参数。Observer
是一个接口,我们继承它自定义一个Oberver
,借此我们就可以监听LiveData
的值的变化。interface IStateObserver : Observer> {override fun onChanged(response: BaseResponse?) {
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 = (data: T) -> Unit
typealias OnFailureCallback = (e: RequestException) -> Unit
typealias OnUnitCallback = () -> Unitclass HttpRequestCallback {var startCallback: OnUnitCallback? = null
var successCallback: OnSuccessCallback? = null
var emptyCallback: OnUnitCallback? = null
var failureCallback: OnFailureCallback? = null
var finishCallback: OnUnitCallback? = nullfun onStart(block: OnUnitCallback) {
startCallback = block
}fun onSuccess(block: OnSuccessCallback) {
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 LiveData>.observeState(
owner: LifecycleOwner,
crossinline callback: HttpRequestCallback.() -> Unit
) {
val requestCallback = HttpRequestCallback().apply(callback)
observe(owner, object : IStateObserver {
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 LiveData>.observeResponse(
owner: LifecycleOwner,
crossinline onStart: OnUnitCallback = {},
crossinline onEmpty: OnUnitCallback = {},
crossinline onFailure: OnFailureCallback = { e: RequestException -> },
crossinline onFinish: OnUnitCallback = {},
crossinline onSuccess: OnSuccessCallback
) {
observe(owner, object : IStateObserver {
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 fire(
context: CoroutineContext = Dispatchers.IO,
block: suspend () -> BaseResponse
): LiveData> = 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 checkEmptyResponse(data: T?): ApiResponse =
if (data =https://www.it610.com/article/= 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()suspend fun login(pwd: String) = apiService.login(password = pwd)
}
2.6
ViewModel
层的封装
ViewModel
基本遵循了《第一行代码》中的写法,创建了两个LiveData
。用户点击按钮时,loginAction
的值就会发生改变,触发switchMap
中的代码,从而达到请求数据的目的。class MainViewModel : ViewModel() {private val loginAction = MutableLiveData()/**
* loginAction 在这里只传递布尔值,不传递密码,在实际项目中,会使用 DataBinding 绑定 xml 布局和 ViewModel,
* 不需要从 Activity 或者 Fragment 中把密码传入 ViewModel
*/
val loginLiveData = https://www.it610.com/article/loginAction.switchMap {
if (it) {
Repository.login("PuKxVxvMzBp2EJM")
} else {
Repository.login("123456")
}
}/**
* 点击登录
*/
fun login() {
loginAction.value = https://www.it610.com/article/true
}fun loginWithWrongPwd() {
loginAction.value = false
}
}
注意:这种写法通常不从3、思维导图及源码 最后,用一张思维导图总结本文:View
向ViewModel层传递数据,是需要搭配DataBinding
的。如果你不想这样写,可以修改BaseRepository
中的返回值,直接返回BaseResponse
。
文章图片
源码地址:GitHub (注意分支要选择 dev1.0)
参考
- JetpackMvvm
- FastJetpack
推荐阅读
- android防止连续点击的简单实现(kotlin)
- retrofit2-kotlin-coroutines-adapter|retrofit2-kotlin-coroutines-adapter 超时引起的崩溃
- Kotlin泛型的高级特性(六)
- Kotlin基础(10)-代理模式在kotlin中的使用
- Android|Android Kotlin实现AIDL跨进程通信
- 更完整的单例模式(java|更完整的单例模式(java, kotlin)
- Kotlin学习笔记——BroadCast
- kotlin|kotlin example
- 一张图带你走进Retrofit源码世界
- RxJava结合Retrofit使用