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

动手之前

因为接入协程的缘故,像以前以回调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")
                }
            }
        })
复制代码

对于DataBinding的双向绑定方式期待后期Google能有更好的实现方案,或者也可以考虑单向数据流的实现框架。

我来评几句
登录后评论

已发表评论数()

相关站点

+订阅
热门文章