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

Kotlin已經成爲Android開發的Google第一推薦語言,項目中也已經使用了很長時間的kotlin了,加上Kotlin1.3的發佈,kotlin協程也已經穩定了,不免會有一些本身的思考。java

對於項目中的網絡請求功能,咱們也在不停的反思,如何將其寫的優雅、簡潔、快速、安全。相信這也是各位開發者在不停思考的問題。因爲咱們的項目都是使用的Retrofit做爲網絡庫,因此,全部的思考都是基於Retrofit展開的。android

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

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

1. Callback

在最初的學習使用中,Callback異步方法是Retrofit最基本的使用方式,以下:數據庫

接口:編程

interface DemoService {

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

使用:api

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,都是很好的封裝方式,技術沒有好壞之分。個人協程封裝方式,也許也不是最好的,可是咱們不能缺少思考、探索、實踐三要素,去想去作。

最好的答案,永遠都是本身給出的。

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

相關文章
相關標籤/搜索