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能有更好的實現方案,或者也能夠考慮單向數據流的實現框架。
相關文章
相關標籤/搜索