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

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

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

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

建立CoroutineScope

在前面的文章中我寫到CoroutineScope.launch方法是一個很經常使用的協程構建器。所以使用協程必須先得建立一個CoroutineScope對象,代碼以下:git

CoroutineScope(Dispatchers.Main + Job())
複製代碼

上面的代碼建立了一個CoroutineScope對象,爲其協程指定了在主線程中執行,同時分配了一個Jobgithub

在demo中我使用的是MVP模式寫的,因此我將CoroutineScope的建立放到了BasePresenter中,代碼以下:api

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方法也能夠取消該任務對應的協程,那我這裏爲何不使用這種方式呢?app

很明顯,若是使用Job.cancel()方式取消協程,那我建立每一個協程的時候都必須保存返回的Job對象,而後再去取消,顯然要更復雜點,而使用CoroutineScope.cancel()則能夠一次性取消該協程上下文建立的全部協程和子協程,該代碼也能夠很方便的提取到基類中,這樣後面在寫業務代碼時也就不用關心協程與View的生命週期的問題。異步

其實你們看源碼的話也能夠發現CoroutineScope.cancel()最終使用的也是Job.cancel()取消協程async

擴展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的地址見文章開頭,有興趣的能夠看看。



下面是個人我的公衆號,歡迎關注交流
相關文章
相關標籤/搜索