Kotlin 协程+Retrofit+MVVM 搭建网络请求实现纪要

前言

  • 本文不讨论协程、Retrofit、MVVM的原理以及基本使用,须要的能够在其余博主那儿找到很好的文章。
  • 本文没有选择DataBinding的双向绑定方式,由于我的以为DataBinding污染了xml,而且在定位错误问题上比较麻烦
  • 也没有采用Flux、Redux、ReKotlin这样的框架,由于目前还不太熟。
  • 能够把本文看做是一篇实现过程纪要,欢迎交流分享,提出建议。

过程与思考

基本依赖

  • 生命周期组件相关
implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0-beta01'
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0-beta01"
复制代码
  • 协程
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.0"
复制代码
  • 网络
implementation 'com.squareup.retrofit2:retrofit:2.6.0'
implementation 'com.squareup.retrofit2:converter-gson:2.6.0'
implementation 'com.squareup.okhttp3:logging-interceptor:3.11.0'
复制代码

备注:Retrofit 在2.6之后对协程有了更友好的实现方式,因此在版本选择上是有要求的。java

动手以前

由于接入协程的缘故,像之前以回调onResponse,onFailure的回调方式是不太符合协程设计的。Kotlin协程对于Retrofit的onFailure处理是直接以Trowable进行抛出的,因此在一开始就要构建好对执行Retrofit的挂机代码块的try..catch设计。android

基本的网络访问封装

基本操做仍是要有的api

abstract class BaseRetrofitClient {

    companion object CLIENT {
        private const val TIME_OUT = 5
    }

    protected val client: OkHttpClient
        get() {
            val builder = OkHttpClient.Builder()
            val logging = HttpLoggingInterceptor()
            if (BuildConfig.DEBUG) {
                logging.level = HttpLoggingInterceptor.Level.BODY
            } else {
                logging.level = HttpLoggingInterceptor.Level.BASIC
            }
            builder.addInterceptor(logging)
                .connectTimeout(TIME_OUT.toLong(), TimeUnit.SECONDS)
            handleBuilder(builder)
            return builder.build()
        }
        
    /** * 以便对builder能够再扩展 */
    abstract fun handleBuilder(builder: OkHttpClient.Builder)

    open fun <Service> getService(serviceClass: Class<Service>, baseUrl: String): Service {
        return Retrofit.Builder()
            .client(client)
            .addConverterFactory(GsonConverterFactory.create())
            .baseUrl(baseUrl)
            .build()
            .create(serviceClass)
    }
}
复制代码

定义基本的Api返回类服务器

/* 服务器返回数剧 */
data class ApiResponse<out T>(val code: Int, /*val errorMsg: String?,*/ val data: T?)
/* 登陆回执 */
data class LoginRes(val token: String)
/* 请求 */
data class LoginReq(val phoneNumber: String, val password: String)

复制代码

定义一个Api以便于测试网络

interface UserApi {

    companion object {
        const val BASE_URL = "https://xxx.com"      // 可自行找一些公开api进行测试
    }

    @POST("/auth/user/login/phone")
    suspend fun login(@Body body: RequestBody): ApiResponse<LoginRes>

}

复制代码

封装BaseViewModel

网络请求必须在子线程中进行,这是Android开发常理,使用协程进行网络请求在代码上可让异步代码看起来是同步执行,这很大得提升了代码得可读性,不过理解挂起的确须要时间。BaseViewModel中最终得事情就是要搭建关于协程对于Retrofit网络请求代码块得try..catch。架构

  • 重要得try..catch
/** * @param tryBlock 尝试执行的挂起代码块 * @param catchBlock 捕获异常的代码块 "协程对Retrofit的实如今失败、异常时没有onFailure的回调而是直接已Throwable的形式抛出" * @param finallyBlock finally代码块 */
private suspend fun tryCatch( tryBlock: suspend CoroutineScope.() -> Unit,
    catchBlock: suspend CoroutineScope.(e: Throwable) -> Unit,
    finallyBlock: suspend CoroutineScope.() -> Unit
) {
    coroutineScope {
        try {
            tryBlock()
        } catch (e: Throwable) {
            catchBlock(e)
        } finally {
            finallyBlock()
        }
    }
}
复制代码

将捕获到得异常进行下放保证执行过程当中得状况都是可控得。框架

  • main线程
/** * 在主线程中开启 * catchBlock、finallyBlock 并非必须,不一样的业务对于错误的处理也可能不一样想要彻底统一的处理是很牵强的 */
fun launchOnMain( tryBlock: suspend CoroutineScope.() -> Unit,
    catchBlock: suspend CoroutineScope.(e: Throwable) -> Unit = {},             // 默认空实现,可根据具体状况变化
    finallyBlock: suspend CoroutineScope.() -> Unit = {}
) {
    viewModelScope.launch {
        tryCatch(tryBlock, catchBlock, finallyBlock)
    }
}
复制代码
  • IO线程
/** * 在IO线程中开启,修改成Dispatchers.IO */
fun launchOnIO( tryBlock: suspend CoroutineScope.() -> Unit,
    catchBlock: suspend CoroutineScope.(e: Throwable) -> Unit = {},
    finallyBlock: suspend CoroutineScope.() -> Unit = {}
) {
    viewModelScope.launch(Dispatchers.IO) {
        tryCatch(tryBlock, catchBlock, finallyBlock)
    }
}
复制代码
  • 不要忘记onCleared
override fun onCleared() {
    super.onCleared()
    viewModelScope.cancel()
}
复制代码

错误处理

错误处理分为1.请求异常(及trycatch中的异常),2.服务器返回的响应体中定义的异常,这些异常只要是带有网络访问性质的APP上都是常见的,因此对NetWork的异常处理我定义了一个NetWorkError.kt文件,里面的函数为顶级函数,这样方便在项目的其余位置直接访问而不须要经过类名或者实例化操做就能够访问。异步

try catch异常处理

像通常触发的连接超时、解析异常均可以作出处理,若是不try catch,那么APP有可能会崩溃,或者长时间没有任何回执,体验不好ide

/** * 处理请求层的错误,对可能的已知的错误进行处理 */
fun handlingExceptions(e: Throwable) {
    when (e) {
        is CancellationException -> {}
        is SocketTimeoutException -> {}
        is JsonParseException -> {}
        else -> {}
    }
}

复制代码

服务器定义的响应异常

通常服务器对于请求都存在响应码,客户端根据响应码去作响应的处理,不一样的错误码会有不一样的日志回馈或者提示,但这都是创建在请求成功上的。这里通常无非为成功和失败。函数

  • Http请求响应封装
// 简单说明:密封类结合when让可能状况都是已知的,代码维护性更高。
sealed class HttpResponse

data class Success<out T>(val data: T) : HttpResponse()
data class Failure(val error: HttpError) : HttpResponse()
复制代码
  • 错误枚举
enum class HttpError(val code: Int, val errorMsg: String?) {
    USER_EXIST(20001, "user does not exist"),
    PARAMS_ERROR(20002, "params is error")
    // ...... more
}
复制代码
  • 错误处理
/** * 处理响应层的错误 */
fun handlingApiExceptions(e: HttpError) {
    when (e) {
        HttpError.USER_EXIST -> {}
        HttpError.PARAMS_ERROR -> {}
        // .. more
    }
}

复制代码
  • 对HttpResponse进行处理
/** * 处理HttpResponse * @param res * @param successBlock 成功 * @param failureBlock 失败 */
fun <T> handlingHttpResponse( res: HttpResponse, successBlock: (data: T) -> Unit,
    failureBlock: ((error: HttpError) -> Unit)? = null
) {
    when (res) {
        is Success<*> -> {
            successBlock.invoke(res.data as T)
        }
        is Failure -> {
            with(res) {
                failureBlock?.invoke(error) ?: defaultErrorBlock.invoke(error)
            }
        }
    }
}


// 默认的处理方案
val defaultErrorBlock: (error: HttpError) -> Unit = { error ->
    UiUtils.showToast(error.errorMsg ?: "${error.code}")            // 能够根据是否为debug进行拆分处理 
}
复制代码

这里是直接对HttpRespoonse进行处理,还须要对当前的响应内容有一个转换

  • 转换服务器响应
fun <T : Any> ApiResponse<T>.convertHttpRes(): HttpResponse {
    return if (this.code == HTTP_SUCCESS) {
        data?.let {
            Success(it)
        } ?: Success(Any())
    } else {
        Failure(HttpError.USER_EXIST)
    }
}
复制代码

暂时定义为一个扩展函数,方便结合this使用。基本封装完成之后,开始搞一个测试类来进行测试。

测试

  • client
object UserRetrofitClient : BaseRetrofitClient() {

    val service by lazy { getService(UserApi::class.java, UserApi.BASE_URL) }

    override fun handleBuilder(builder: OkHttpClient.Builder) {
    }

}
复制代码
  • model
class LoginRepository {

    suspend fun doLogin(phone: String, pwd: String) = UserRetrofitClient.service.login(
        LoginReq(phone, pwd).toJsonBody()
    )

}
复制代码
  • viewModel
class LoginViewModel : BaseViewModel() {

    private val repository by lazy { LoginRepository() }

    companion object {
        const val LOGIN_STATE_SUCCESS = 0
        const val LOGIN_STATE_FAILURE = 1
    }

    // 登陆状态
    val loginState: MutableLiveData<Int> = MutableLiveData()

    fun doLogin(phone: String, pwd: String) {
        launchOnIO(
            tryBlock = {
                repository.doLogin(phone, pwd).run {
                    // 进行响应处理
                    handlingHttpResponse<LoginRes>(
                        convertHttpRes(),
                        successBlock = {
                            loginState.postValue(LOGIN_STATE_SUCCESS)
                        },
                        failureBlock = { ex ->
                            loginState.postValue(LOGIN_STATE_FAILURE)
                            handlingApiExceptions(ex)
                        }
                    )
                }
            },
            // 请求异常处理
            catchBlock = { e ->
                handlingExceptions(e)
            }
        )
    }
}
复制代码
  • 最后在LoginAct对loginState实现监听
vm.loginState.observe(this, Observer { state ->
            when(state){
                LoginViewModel.LOGIN_STATE_SUCCESS ->{
                    UiUtils.showToast("success")
                }
                LoginViewModel.LOGIN_STATE_FAILURE ->{
                    UiUtils.showToast("failure")
                }
            }
        })
复制代码

总结

这是目前本身可以想到的一些方式,我的以为Kotlin的确带来很大的改观,特别是在可读性和维护性上。虽然在架构和总体设计这件事情上,原本就没有标准的方式,这些问题都是相对的。

对于DataBinding的双向绑定方式期待后期Google能有更好的实现方案,或者也能够考虑单向数据流的实现框架。
相关文章
相关标签/搜索