Retrofit + Kotlin + MVVM 的網路請求框架的封裝嘗試

lindroid發表於2021-11-08

1、前言

之前在學習郭霖《第一行程式碼》時按部就班地寫過一個彩雲天氣 App,對裡面的網路請求框架的封裝印象非常深刻,很喜歡這種 Retrofit + Kotlin + 協程的搭配使用。隨後也在自己的專案裡參考了這部分的程式碼。但隨著程式碼的深入編寫和功能的複雜,原來的框架已經無法滿足我的使用了。原主要有如下的痛點:

  • 缺少失敗的回撥
  • 顯示載入中動畫比較麻煩

後面我自己試著努力去封裝一個簡單易用的框架,可惜個人能力有限,自己封裝的框架總是不如人意。好在還有很多優秀的部落格和程式碼可供參考。在此基礎上,對彩雲天氣 App中的網路請求框架做了一些修改,儘可能地做到簡單易用。以請求玩安卓的登入介面為例(使用者名稱和密碼是我自己申請的,見程式碼),頁面上有一個按鈕,點選按鈕後就發起登入請求。

先來看看發起請求後的回撥怎麼寫:

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的,它的架構圖如下所示,如果你閱讀過《第一行程式碼》或者谷歌的相關文件,那麼想必對此不會陌生。

MVVM架構圖.png

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 = 8L

private 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 <T> create(serviceClass: Class<T>): T = retrofit.create(serviceClass)

    inline fun <reified T> 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():請求成功,但datanull或者data是集合型別但為空
  • onFailure():請求失敗
  • onFinish():請求結束(可在此關閉載入動畫)

這裡要注意onSuccess的標準:並不僅僅是 Http 請求的結果碼(status code)等於 200,而且要達到Api請求成功的標準,以玩安卓的Api 為例,errorCode 為 0時,發起的請求才是執行成功;否則,都應該歸為onFailure()的情況(可以參考文章附帶的思維導圖)。

理清楚有幾種回撥狀態後,就可以實施監聽了。那麼在哪裡監聽呢?LiveDataobserve()方法的第二個函式可以傳入Observer引數。Observer是一個介面,我們繼承它自定義一個Oberver,藉此我們就可以監聽LiveData的值的變化。

interface IStateObserver<T> : Observer<BaseResponse<T>> {

    override fun onChanged(response: BaseResponse<T>?) {
        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<T> = (data: T) -> Unit
typealias OnFailureCallback = (e: RequestException) -> Unit
typealias OnUnitCallback = () -> Unit

class HttpRequestCallback<T> {

    var startCallback: OnUnitCallback? = null
    var successCallback: OnSuccessCallback<T>? = null
    var emptyCallback: OnUnitCallback? = null
    var failureCallback: OnFailureCallback? = null
    var finishCallback: OnUnitCallback? = null

    fun onStart(block: OnUnitCallback) {
        startCallback = block
    }

    fun onSuccess(block: OnSuccessCallback<T>) {
        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 <T> LiveData<BaseResponse<T>>.observeState(
    owner: LifecycleOwner,
    crossinline callback: HttpRequestCallback<T>.() -> Unit
) {
    val requestCallback = HttpRequestCallback<T>().apply(callback)
    observe(owner, object : IStateObserver<T> {
        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 <T> LiveData<BaseResponse<T>>.observeResponse(
    owner: LifecycleOwner,
    crossinline onStart: OnUnitCallback = {},
    crossinline onEmpty: OnUnitCallback = {},
    crossinline onFailure: OnFailureCallback = { e: RequestException -> },
    crossinline onFinish: OnUnitCallback = {},
    crossinline onSuccess: OnSuccessCallback<T>
) {
    observe(owner, object : IStateObserver<T> {
        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 <T> fire(
        context: CoroutineContext = Dispatchers.IO,
        block: suspend () -> BaseResponse<T>
    ): LiveData<BaseResponse<T>> = 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 <T> checkEmptyResponse(data: T?): ApiResponse<T> =
        if (data == 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<ApiService>()

    suspend fun login(pwd: String) = apiService.login(password = pwd)
}

2.6 ViewModel層的封裝

ViewModel基本遵循了《第一行程式碼》中的寫法,建立了兩個LiveData。使用者點選按鈕時,loginAction的值就會發生改變,觸發switchMap中的程式碼,從而達到請求資料的目的。

class MainViewModel : ViewModel() {

    private val loginAction = MutableLiveData<Boolean>()

    /**
     * loginAction 在這裡只傳遞布林值,不傳遞密碼,在實際專案中,會使用 DataBinding 繫結 xml 佈局和 ViewModel,
     * 不需要從 Activity 或者 Fragment 中把密碼傳入 ViewModel
     */
    val loginLiveData = loginAction.switchMap {
        if (it) {
            Repository.login("PuKxVxvMzBp2EJM")
        } else {
            Repository.login("123456")
        }
    }

    /**
     * 點選登入
     */
    fun login() {
        loginAction.value = true
    }

    fun loginWithWrongPwd() {
        loginAction.value = false
    }
}
注意:這種寫法通常不從View向ViewModel層傳遞資料,是需要搭配DataBinding 的。如果你不想這樣寫,可以修改BaseRepository中的返回值,直接返回BaseResponse

3、思維導圖及原始碼

最後,用一張思維導圖總結本文:

網路請求框架1.0.png

原始碼地址:GitHub (注意分支要選擇 dev1.0)

參考

相關文章