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

10,399 阅读5分钟

前言

  • 本文不讨论协程、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以后对协程有了更友好的实现方式,所以在版本选择上是有要求的。

动手之前

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

基本的网络访问封装

基本操作还是要有的

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有可能会崩溃,或者长时间没有任何回执,体验很差

/**
 * 处理请求层的错误,对可能的已知的错误进行处理
 */
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能有更好的实现方案,或者也可以考虑单向数据流的实现框架。