Android中用Kotlin Coroutine(協程)和Retrofit進行網路請求和取消請求

武漢老胡發表於2019-04-22

Kotlin Coroutine(協程)系列:
1. Kotlin Coroutine(協程) 簡介
2. Kotlin Coroutine(協程) 基本知識
3. Android中用Kotlin Coroutine(協程)和Retrofit進行網路請求和取消請求

前面兩篇文章介紹了協程的一些基本概念和基本知識,這篇則介紹在Android中如何使用協程配合Retrofit發起網路請求,同時介紹在使用協程時如何優雅的取消已經發起的網路請求。

此篇文章的Demo地址:https://github.com/huyongli/AndroidKotlinCoroutine

建立CoroutineScope

在前面的文章中我寫到CoroutineScope.launch方法是一個很常用的協程構建器。因此使用協程必須先得建立一個CoroutineScope物件,程式碼如下:

CoroutineScope(Dispatchers.Main + Job())
複製程式碼

上面的程式碼建立了一個CoroutineScope物件,為其協程指定了在主執行緒中執行,同時分配了一個Job

在demo中我使用的是MVP模式寫的,所以我將CoroutineScope的建立放到了BasePresenter中,程式碼如下:

interface MvpView

interface MvpPresenter<V: MvpView> {

    @UiThread
    fun attachView(view: V)

    @UiThread
    fun detachView()
}

open class BasePresenter<V: MvpView> : MvpPresenter<V> {
    lateinit var view: V
    val presenterScope: CoroutineScope by lazy {
        CoroutineScope(Dispatchers.Main + Job())
    }

    override fun attachView(view: V) {
        this.view = view
    }

    override fun detachView() {
        presenterScope.cancel()
    }
}
複製程式碼

使用CoroutineScope.cancel()取消協程

大家應該可以看到上面BasePresenter.detachView中呼叫了presenterScope.cancel(),那這個方法有什麼作用呢,作用就是取消掉presenterScope建立的所有協程和其子協程。

前面的文章我也介紹過使用launch建立協程時會返回一個Job物件,通過Job物件的cancel方法也可以取消該任務對應的協程,那我這裡為什麼不使用這種方式呢?

很明顯,如果使用Job.cancel()方式取消協程,那我建立每個協程的時候都必須儲存返回的Job物件,然後再去取消,顯然要更復雜點,而使用CoroutineScope.cancel()則可以一次性取消該協程上下文建立的所有協程和子協程,該程式碼也可以很方便的提取到基類中,這樣後面在寫業務程式碼時也就不用關心協程與View的生命週期的問題。

其實大家看原始碼的話也可以發現CoroutineScope.cancel()最終使用的也是Job.cancel()取消協程

擴充套件Retrofit.Call適配協程

interface ApiService {
    @GET("data/iOS/2/1")
    fun getIOSGank(): Call<GankResult>

    @GET("data/Android/2/1")
    fun getAndroidGank(): Call<GankResult>
}

class ApiSource {
    companion object {
        @JvmField
        val instance = Retrofit.Builder()
            .baseUrl("http://gank.io/api/")
            .addConverterFactory(GsonConverterFactory.create())
            .build().create(ApiService::class.java)
    }
}
複製程式碼

大家可以看到上面的api介面定義應該很熟悉,我們可以通過下面的程式碼發起非同步網路請求

ApiSource.instance.getAndroidGank().enqueue(object : Callback<T> {
    override fun onFailure(call: Call<T>, t: Throwable) {
        
    }

    override fun onResponse(call: Call<T>, response: Response<T>) {
        
    }
})
複製程式碼

前面的文章介紹過協程可以讓非同步程式碼像寫同步程式碼那樣方便,那上面這段非同步程式碼能不能使用協程改造成類似寫同步程式碼塊那樣呢?很顯然是可以的,具體改造程式碼如下:

//擴充套件Retrofit.Call類,為其擴充套件一個await方法,並標識為掛起函式
suspend fun <T> Call<T>.await(): T {
    return suspendCoroutine {
        enqueue(object : Callback<T> {
            override fun onFailure(call: Call<T>, t: Throwable) {
                //請求失敗,丟擲異常,手動結束當前協程
                it.resumeWithException(t)
            }

            override fun onResponse(call: Call<T>, response: Response<T>) {
                if(response.isSuccessful) {
                   //請求成功,將請求結果拿到並手動恢復所在協程
                   it.resume(response.body()!!)
                } else{
                   //請求狀態異常,丟擲異常,手動結束當前協程
                   it.resumeWithException(Throwable(response.toString()))
                }
            }
        })
    }
}
複製程式碼

上面的程式碼擴充套件了一個掛起函式await,執行該方法時,會執行Retrofit.Call的非同步請求同時在協程中掛起該函式,直到非同步請求成功或者出錯再重新恢復所在協程。

suspendCoroutine

全域性函式,此函式可以獲取當前方法所在協程上下文,並將當前協程掛起,直到某個時機再重新恢復協程執行,但是這個時機其實是由開發者自己控制的,就像上面程式碼中的it.resumeit.resumeWithException

發起請求,寫法一

//使用CoroutineScope.launch建立一個協程,此協程在主執行緒中執行
presenterScope.launch {
    val time = System.currentTimeMillis()
    view.showLoadingView()
    try {
        val ganks = queryGanks()
        view.showLoadingSuccessView(ganks)
    } catch (e: Throwable) {
        view.showLoadingErrorView()
    } finally {
        Log.d(TAG, "耗時:${System.currentTimeMillis() - time}")
    }
}

suspend fun queryGanks(): List<Gank> {
    //此方法執行執行緒和呼叫者保持一致,因此也是在主執行緒中執行
    return try {
        //先查詢Android列表,同時當前協程執行流程掛起在此處
        val androidResult = ApiSource.instance.getAndroidGank().await()
        
        //Android列表查詢完成之後恢復當前協程,接著查詢IOS列表,同時將當前協程執行流程掛起在此處
        val iosResult = ApiSource.instance.getIOSGank().await()

        //Android列表和IOS列表都查詢結束後,恢復協程,將兩者結果合併,查詢結束
        val result = mutableListOf<Gank>().apply {
            addAll(iosResult.results)
            addAll(androidResult.results)
        }
        result
    } catch (e: Throwable) {
        //處理協程中的異常,否則程式會崩掉
        e.printStackTrace()
        throw e
    }
}
複製程式碼

從上面的程式碼大家可以發現,協程中對異常的處理使用的是try-catch的方式,初學,我也暫時只想到了這種方式。所以在使用協程時,最好在業務的適當地方使用try-catch捕獲異常,否則一旦協程執行出現異常,程式就崩掉了。

另外上面的程式碼的寫法還有一個問題,因為掛起函式執行時會掛起當前協程,所以上述兩個請求是依次順序執行,因此上面的queryGanks()方法其實是耗費了兩次網路請求的時間,因為請求Android列表和請求ios列表兩個請求不是並行的,所以這種寫法肯定不是最優解。

發起請求,寫法二

下面我們再換另外一種寫法。

suspend fun queryGanks(): List<Gank> {
    /**
     * 此方法執行執行緒和呼叫者保持一致,因此也在主執行緒中執行
     * 因為網路請求本身是非同步請求,同時async必須在協程上下文中執行,所以此方法實現中採用withContext切換執行執行緒到主執行緒,獲取協程上下文物件
     */
    return withContext(Dispatchers.Main) {
        try {
            //在當前協程中建立一個新的協程發起Android列表請求,但是不會掛起當前協程
            val androidDeferred = async {
                val androidResult = ApiSource.instance.getAndroidGank().await()
                androidResult
            }

            //發起Android列表請求後,立刻又在當前協程中建立了另外一個子協程發起ios列表請求,也不會掛起當前協程
            val iosDeferred = async {
                val iosResult = ApiSource.instance.getIOSGank().await()
                iosResult
            }

            val androidResult = androidDeferred.await().results
            val iosResult = iosDeferred.await().results

            //兩個列表請求並行執行,等待兩個請求結束之後,將請求結果進行合併
            //此時當前方法的執行時間實際上兩個請求中耗時時間最長的那個,而不是兩個請求所耗時間的總和,因此此寫法優於上面一種寫法
            val result = mutableListOf<Gank>().apply {
                addAll(iosResult)
                addAll(androidResult)
            }
            result
        } catch (e: Throwable) {
            e.printStackTrace()
            throw e
        }
    }
}
複製程式碼

這種寫法與前一種寫法的區別是採用async構建器建立了兩個子協程分別去請求Android列表和IOS列表,同時因為async構建器執行的時候不會掛起當前協程,所以兩個請求是並行執行的,因此效率較上一個寫法要高很多。

發起請求,寫法三

第三個寫法就是在RetorfitCallAdapter上做文章,通過自定義實現CallAdapterFactory,將api定義時的結果Call直接轉換成Deferred,這樣就可以同時發起Android列表請求和IOS列表請求,然後通過Deferred.await獲取請求結果,這種寫法是寫法一寫法二的結合。

這種寫法JakeWharton大神早已為我們實現了,地址在這github.com/JakeWharton…

這裡我就不說這種方案的具體實現了,感興趣的同學可以去看其原始碼。

寫法三的具體程式碼如下:

val instance = Retrofit.Builder()
        .baseUrl("http://gank.io/api/")
        .addCallAdapterFactory(CoroutineCallAdapterFactory())
        .addConverterFactory(GsonConverterFactory.create())
        .build().create(CallAdapterApiService::class.java)
        
suspend fun queryGanks(): List<Gank> {
    return withContext(Dispatchers.Main) {
        try {
            val androidDeferred = ApiSource.callAdapterInstance.getAndroidGank()

            val iosDeferred = ApiSource.callAdapterInstance.getIOSGank()

            val androidResult = androidDeferred.await().results

            val iosResult = iosDeferred.await().results

            val result = mutableListOf<Gank>().apply {
                addAll(iosResult)
                addAll(androidResult)
            }
            result
        } catch (e: Throwable) {
            e.printStackTrace()
            throw e
        }
    }
}
複製程式碼

上面的第三種寫法看起來更簡潔,也是並行請求,耗時為請求時間最長的那個請求的時間,和第二種差不多。

具體實現demo的地址見文章開頭,有興趣的可以看看。

相關文章