【思貨】kotlin協程優雅的與Retrofit纏綿-正文

limuyang2發表於2019-06-08

Kotlin已經成為Android開發的Google第一推薦語言,專案中也已經使用了很長時間的kotlin了,加上Kotlin1.3的釋出,kotlin協程也已經穩定了,難免會有一些自己的思考。

對於專案中的網路請求功能,我們也在不停的反思,如何將其寫的優雅、簡潔、快速、安全。相信這也是各位開發者在不停思考的問題。由於我們的專案都是使用的Retrofit作為網路庫,所以,所有的思考都是基於Retrofit展開的。

本篇文章中將會從我的思考進化歷程開始講起。涉及到Kotlin的協程、擴充套件方法、DSL,沒有基礎的小夥伴,先去了解這三樣東西,本篇文章不再進行講解。 DSL可以看看我寫這篇簡介

在網路請求中,我們需要關注的隱式問題就是:頁面生命週期的繫結,關閉頁面後需要關閉未完成的網路請求。為此,各位前輩,是八仙過海、各顯神通。我也是從學習、模仿前輩,到自我理解的轉變。

1. Callback

在最初的學習使用中,Callback非同步方法是Retrofit最基本的使用方式,如下:

介面:

interface DemoService {

    @POST("oauth/login")
    @FormUrlEncoded
    fun login(@Field("name") name: String, @Field("pwd") pwd: String): Call<String>
}
複製程式碼

使用:

val retrofit = Retrofit.Builder()
    .baseUrl("https://baidu.com")
    .client(okHttpClient.build())
    .build()

val api = retrofit.create(DemoService::class.java)
val loginService = api.login("1", "1")
loginService.enqueue(object : Callback<String> {
    override fun onFailure(call: Call<String>, t: Throwable) {

    }

    override fun onResponse(call: Call<String>, response: Response<String>) {

    }
})
複製程式碼

這裡不再細說。

在關閉網路請求的時候,需要在onDestroy中呼叫cancel方法:

override fun onDestroy() {
    super.onDestroy()
    loginService.cancel()
}
複製程式碼

這種方式,容易導致忘記呼叫cancel方法,而且網路操作和關閉請求的操作是分開的,不利於管理。

這當然不是優雅的方法。隨著Rx的火爆,我們專案的網路請求方式,也逐漸轉為了Rx的方式

2. RxJava

此種使用方式,百度一下,到處都是教程講解,可見此種方式起碼是大家較為認可的一種方案。

在Rx的使用中,我們也嘗試了各種各樣的封裝方式,例如自定義Subscriber,將onNext、onCompletedonError進行拆分組合,滿足不同的需求。

首先在Retrofit裡新增Rx轉換器RxJava2CallAdapterFactory.create()

addCallAdapterFactory(RxJava2CallAdapterFactory.create())
複製程式碼

RxJava的使用方式大體如下,先將介面的Call改為Observable

interface DemoService {

    @POST("oauth/login")
    @FormUrlEncoded
    fun login(@Field("name") name: String, @Field("pwd") pwd: String): Observable<String>
}
複製程式碼

使用:(配合RxAndroid繫結宣告週期)

api.login("1","1")
    .subscribeOn(Schedulers.io())
    .observeOn(AndroidSchedulers.mainThread()) //RxAndroid
    .subscribe(object :Observer<String> {
        override fun onSubscribe(d: Disposable) {

        }

        override fun onComplete() {

        }

        override fun onNext(t: String) {

        }

        override fun onError(e: Throwable) {
        }
    })
複製程式碼

這種使用方式確實方便了不少,響應式程式設計的思想也很優秀,一切皆為事件流。通過RxAndroid來切換UI執行緒和繫結頁面生命週期,在頁面關閉的時候,自動切斷向下傳遞的事件流。

RxJava最大的風險即在於記憶體洩露,而RxAndroid確實規避了一定的洩露風險。 並且通過檢視RxJava2CallAdapterFactory的原始碼,發現也確實呼叫了cancel方法,嗯……貌似不錯呢。 但總是覺得RxJava過於龐大,有些大材小用。

3. LiveData

隨著專案的的推進和Google全家桶的釋出。一個輕量化版本的RxJava進入到了我們視線,那就是LiveDataLiveData借鑑了很多RxJava的的設計思想,也是屬於響應式程式設計的範疇。LiveData的最大優勢即在於響應Acitivty的生命週期,不用像RxJava再去繫結宣告週期。

同樣的,我們首先需要新增LiveDataCallAdapterFactory (連結裡是google官方提供的寫法,可直接拷貝到專案中),用於把retrofit的Callback轉換為LiveData

addCallAdapterFactory(LiveDataCallAdapterFactory.create())
複製程式碼

介面改為:

interface DemoService {

    @POST("oauth/login")
    @FormUrlEncoded
    fun login(@Field("name") name: String, @Field("pwd") pwd: String): LiveData<String>
}
複製程式碼

呼叫:

api.login("1", "1").observe(this, Observer {string ->
    
})
複製程式碼

以上就是最基礎的使用方式,在專案中使用時候,通常會自定義Observer,用來將各種資料進行區分。

在上面呼叫的observe方法中,我們傳遞了一個this,這個this指的是宣告週期,一般我們在AppCompatActivity中使用時,直接傳遞其本身就可以了。

下面簡單跳轉原始碼進行說明下。通過檢視原始碼可以發現:

public void observe(@NonNull LifecycleOwner owner, @NonNull Observer<T> observer)
複製程式碼

this本身是傳遞的LifecycleOwner

那麼我們在一層層跳轉AppCompatActivity,會發現AppCompatActivity是繼承於SupportActivity的父類:

public class SupportActivity extends Activity implements LifecycleOwner, Component
複製程式碼

其本身對LifecycleOwner介面進行了實現。也就是說,除非特殊要求,一般我們只需要傳遞其本身就可以了。LiveData會自動處理資料流的監聽和解除繫結。

通常來說:在onCreate中對資料進行一次性的繫結,後面就不需要再次繫結了。

當生命週期走到onStartonResume的時候,LiveData會自動接收事件流;

當頁面處於不活動的時候,將會暫停接收事件流,頁面恢復時恢復資料接收。(例如A跳轉到B,那麼A將會暫停接收。當從B回到A以後,將恢復資料流接收)

當頁面onDestroy時候,會自動刪除觀察者,從而中斷事件流。

可以看出LiveData作為官方套件,使用簡單,生命週期的響應也是很智慧的,一般都不需要額外處理了。

(更高階的用法,可以參考官方Demo,可以對資料庫快取等待都進行一整套的響應式封裝,非常nice。建議學習下官方的封裝思想,就算不用,也是對自己大有裨益)

4. Kotlin協程

上面說了那麼多,這裡步入了正題。大家仔細觀察下會發現,上面均是使用的Retrofitenqueue非同步方法,再使用Callback進行的網路回撥,就算是RxJava和Livedata的轉換器,內部其實也是使用的Callback。在此之前,Retrofit的作者也寫了一個協程的轉換器,地址在這,但內部依然使用的是Callback,本質均為一樣。(目前該庫才被廢棄,其實我也覺得這樣使用協程就沒意義了,Retrofit在最新的2.6.0版本,直接支援了kotlin協程的suspend掛起函式),

之前瞭解Retrofit的小夥伴應該知道,Retrofit是有同步和非同步兩種呼叫方式的。

void enqueue(Callback<T> callback);
複製程式碼

上面這就是非同步呼叫方式,傳入一個Callback,這也是我們最最最常用到的方式。

Response<T> execute() throws IOException;
複製程式碼

上面這種是同步呼叫方法,會阻塞執行緒,返回的直接就是網路資料Response,很少使用。

後來我就在思考,能不能結合kotlin的協程,拋棄Callback,直接使用Retrofit的同步方法,把非同步當同步寫,程式碼順序書寫,邏輯清晰,效率高,同步的寫法就更加方便物件的管理。

說幹就幹。

首先寫一個協程的擴充套件方法:

val api = ……
fun <ResultType> CoroutineScope.retrofit() {
    this.launch(Dispatchers.Main) {
        val work = async(Dispatchers.IO) {
            try {
                api.execute() // 呼叫同步方法
            } catch (e: ConnectException) {
                e.logE()
                println("網路連線出錯")
                null
            } catch (e: IOException) {
                println("未知網路錯誤")
                null
            }
        }
        work.invokeOnCompletion { _ ->
            // 協程關閉時,取消任務
            if (work.isCancelled) {
                api.cancel() // 呼叫 Retrofit 的 cancel 方法關閉網路
            }
        }
        val response = work.await() // 等待io任務執行完畢返回資料後,再繼續後面的程式碼

        response?.let {

            if (response.isSuccessful) {
                println(response.body()) //網路請求成功,獲取到的資料
            } else {
                // 處理 HTTP code
                when (response.code()) {
                    401 -> {
                    }
                    500 -> {
                        println("內部伺服器錯誤")
                    }
                }
                println(response.errorBody()) //網路請求失敗,獲取到的資料
            }

        }
    }
}
複製程式碼

上面就是核心程式碼,主要的意思都寫了註釋。整個工作流程是出於ui協程中,所以可以隨意操作UI控制元件,接著在io執行緒中去同步呼叫網路請求,並且等待io執行緒的執行完畢,接著再拿到結果進行處理,整個流程都是基於同步程式碼的書寫方式,一步一個流程,沒有回掉而導致的程式碼割裂感。那麼繼續,我們想辦法把獲取的資料返回出去。

這裡我們採用DSL方法,首先自定義一個類:

class RetrofitCoroutineDsl<ResultType> {
    var api: (Call<ResultType>)? = null

    internal var onSuccess: ((ResultType?) -> Unit)? = null
        private set
    internal var onComplete: (() -> Unit)? = null
        private set
    internal var onFailed: ((error: String?, code, Int) -> Unit)? = null
        private set

    var showFailedMsg = false

    internal fun clean() {
        onSuccess = null
        onComplete = null
        onFailed = null
    }

    fun onSuccess(block: (ResultType?) -> Unit) {
        this.onSuccess = block
    }

    fun onComplete(block: () -> Unit) {
        this.onComplete = block
    }

    fun onFailed(block: (error: String?, code, Int) -> Unit) {
        this.onFailed = block
    }

}
複製程式碼

此類對外暴露了三個方法:onSuccessonCompleteonFailed,用於分類返回資料。

接著,我們對我們的核心程式碼進行改造,將方法進行傳遞:

fun <ResultType> CoroutineScope.retrofit(
    dsl: RetrofitCoroutineDsl<ResultType>.() -> Unit //傳遞方法,需要哪個,傳遞哪個
) {
    this.launch(Dispatchers.Main) {
        val retrofitCoroutine = RetrofitCoroutineDsl<ResultType>()
        retrofitCoroutine.dsl()
        retrofitCoroutine.api?.let { it ->
            val work = async(Dispatchers.IO) { // io執行緒執行
                try {
                    it.execute()
                } catch (e: ConnectException) {
                    e.logE()
                    retrofitCoroutine.onFailed?.invoke("網路連線出錯", -100)
                    null
                } catch (e: IOException) {
                    retrofitCoroutine.onFailed?.invoke("未知網路錯誤", -1)
                    null
                }
            }
            work.invokeOnCompletion { _ ->
                // 協程關閉時,取消任務
                if (work.isCancelled) {
                    it.cancel()
                    retrofitCoroutine.clean()
                }
            }
            val response = work.await()
            retrofitCoroutine.onComplete?.invoke()
            response?.let {
                    if (response.isSuccessful) {
                        retrofitCoroutine.onSuccess?.invoke(response.body())
                    } else {
                        // 處理 HTTP code
                        when (response.code()) {
                            401 -> {
                            }
                            500 -> {
                            }
                        }
                        retrofitCoroutine.onFailed?.invoke(response.errorBody(), response.code())
                    }
            }
        }
    }
}
複製程式碼

這裡使用DSL傳遞方法,可以更具需要傳遞的,例如只需要onSuccess,那就只傳遞這一個方法,不必三個都傳遞,按需使用。

使用方式:

首先需要按照kotlin的官方文件來改造下activity:

abstract class BaseActivity : AppCompatActivity(), CoroutineScope {

    private lateinit var job: Job // 定義job

    override val coroutineContext: CoroutineContext
        get() = Dispatchers.Main + job // Activity的協程

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        job = Job()
    }

    override fun onDestroy() {
        super.onDestroy()
        job.cancel() // 關閉頁面後,結束所有協程任務
    }
}
複製程式碼

Activity實現CoroutineScope介面,就能直接根據當前的context獲取協程使用。

接下來就是真正的使用,在任意位置即可呼叫此擴充套件方法:

retrofit<String> {
    api = api.login("1","1")

    onComplete {
    }

    onSuccess { str ->
    }

    onFailed { error, code ->
    }
}
複製程式碼

在有的時候,我們只需要處理onSuccess的情況,並不關心其他兩個。那麼直接寫:

retrofit<String> {
    api = api.login("1","1")

    onSuccess { str ->
    }
}
複製程式碼

需要哪個寫哪個,程式碼非常整潔。

可以看出,我們不需要單獨再對網路請求進行生命週期的繫結,在頁面被銷燬的時候,job也就被關閉了,當協程被關閉後,會執行呼叫 Retrofit 的 cancel 方法關閉網路。

5. 小節

協程的開銷是小於Thread多執行緒的,響應速度很快,非常適合輕量化的工作流程。對於協程的使用,還有帶我更深入的思考和學習。協程並不是Thread的替代品,還是多非同步任務多一個補充,我們不能按照慣性思維去理解協程,而是要多從其本身特性入手,開發出它更安逸的使用方式。 而且隨著Retrofit 2.6.0的釋出,自帶了新的協程方案,如下:

@GET("users/{id}")
suspend fun user(@Path("id") long id): User
複製程式碼

增加了suspend掛起函式的支援,可見協程的應用會越來越受歡迎。

上面所說的所有網路處理方法,不論是Rx還是LiveData,都是很好的封裝方式,技術沒有好壞之分。我的協程封裝方式,也許也不是最好的,但是我們不能缺乏思考、探索、實踐三要素,去想去做。

最好的答案,永遠都是自己給出的。

第一次寫這種型別的文章記錄,流程化比較嚴重,記錄不嚴謹,各位見諒。謝謝大家的閱讀

相關文章