Kotlin 協程 的實戰

1,前言

一轉眼kotlin已經轉正兩年多,KT的各類語法糖、高階函數、擴展函數等等。真的是讓人愛不釋手。一點都不吹牛逼,剛開始用Kotlin的時候,我徹底不知道 協程 這個概念。後來記得有個朋友問我說:你知道協程嗎?我說我確定知道啊,我從上大學的時候就一直在用,買車票什麼的都是用他買的啊。比12306好用。java

他來了個很無奈的表情說: 不是說你買票的那個攜程,是Kotlin的協程,協程!!協程!!協程。
我心想:What?還有這個東西嗎。我就回了他說: 老子不知道,你能把我咋地。雖然說嘴很硬,可是內心虛啊,趕忙打開瀏覽器,搜索框輸入了: 「協程」兩個字,很用力按下了回車,走你。
哎喲,臥槽,第一個出來的還真就是攜程網的廣告
「放心價格,放心服務」
斜眼笑~~~~。不信的能夠百度試一下,哈哈

2,瞭解

查閱了一番資料事後,才發現 協程 這個概念好多年前就已經有了,近幾年才普遍使用起來,阿里也是開源了好幾個協程相關的框架,據說淘寶就是用協程來渡過雙十一的,以前作項目一直用的Java,Java並無提供對協程的直接支持,對這個概念一直沒有過接觸,像Go、Python都是提供了對協程直接支持的。固然了,今天的主角,Kotlin也是提供了對 協程支持的。git

咱們暫時理解爲:他跟線程差很少,線程的調度是基於CPU算法和優先級,仍是要跟底層打交道的,協程是徹底由應用程序來調用的,可是他仍是要基於線程來運行的。他比線程更經量,開銷很小。github

3,實戰

光說不練假把戲,通過一些嘗試後,分享一下我在項目中的使用,說到異步呢,在咱們Android程序中最經常使用的就是網絡請求吧,UI線程不能進行耗時操做,咱們會把網絡請求、文件讀寫這些耗時操做放在子線程中,如今咱們能夠用協程來實現。算法

3.1網絡請求

說到網絡請求就要說到咱們的網紅庫 Retrofit,好多項目中都是用RxJava+Retrofit來進行網絡請求,自從開始使用協程,也放棄了使用RxJava,在Retrofit 2.6 以前。想用協程配合Retrofit來進行網絡請求,咱們的請求結果還要作一次轉換,對此呢,咱們Android界的大咖 JakeWharton還專門寫了個庫 retrofit2-kotlin-coroutines-adapter 來作轉換,有興趣的能夠看一下。不過,Retrofit 2.6 以後,直接對kotlin 的協程作了支持,也不須要用到這個庫了。咱們來看一下實際代碼,依然使用鴻大大的WanAndroid API來作例子。 好比咱們要獲取Banner圖片 咱們的 XXService:json

/**
     * 玩安卓輪播圖
     */
    @GET("banner/json")
    suspend fun getBanner(): BaseResult<List<BannerBean>>
複製代碼

和以前咱們寫的有什麼區別呢:瀏覽器

  1. 前面多了suspend關鍵字,帶有這個關鍵字的函數,只有在協程中才能調用,在普通函數調用會報錯的,編譯也過不了
  2. 返回結果只直接寫對應的Bean就行了,不須要固定類型來包裝

下邊的用法是在ViewModel中來使用的,若是想在Activity或者Fragment中使用,是同樣的,只不過啓動協程的時候寫法有些不一樣。 下面在咱們的VIewModel中:bash

private val repository by lazy {
        RetrofitClient.getInstance().create(HomeService::class.java)
    }

     fun getBanner() {
        viewModelScope.launch {
            val result = repository.getBanner()
            if (result.errorCode == 0) {
                LogUtils.d(result.data)
            }
        }
    }
複製代碼

這樣一個簡單的網絡請求就完成,viewModelScope.launch {} 這個就是在ViewModel中啓動一個協程,他會在ViewModel銷燬的時候,自動取消他本身和在他內部啓動的全部協程 相對於RxJava來講,咱們每次都要關心生命週期防止內存泄露,是否是加方便些呢,這樣咱們不用關心內存泄露的問題了。因此咱們要啓動子協程,都要寫在他內部,除非有特殊需求,好比頁面銷燬了,要作些其餘工做。不然都儘可能在他內部啓動。
好了,咱們再看上面的代碼,會發現有個問題,**viewModelScope.launch {}**是直接啓動在主線程的,因此協程也會運行在主線程中,那咱們怎麼能讓網絡請求去影響到UI呢,絕對不能忍。咱們能夠在啓動一個子協程讓他運行在IO線程上。修改以下:網絡

viewModelScope.launch {
            val result = withContext(Dispatchers.IO) { repository.getBanner() }
            if (result.errorCode == 0) {
                LogUtils.d(result.data)
            }
        }
複製代碼

這下就正常了,是否是至關方便,代碼也清晰了不少,既然咱們都要在 viewModelScope.launch {} 中啓動協程 咱們就把他再封裝一下作一優化吧,順便加上錯誤處理,咱們在BaseViewModel中加入方法:併發

// 以後咱們 所有在 launchUI 中啓動協程
    fun launchUI(block: suspend CoroutineScope.() -> Unit) {
        viewModelScope.launch { block() }  
    }
 //....
    /**
    * 錯誤處理
    **/
    fun launch(
        block: suspend CoroutineScope.() -> Unit,
        error: suspend CoroutineScope.(Throwable) -> Unit = {},
        complete: suspend CoroutineScope.() -> Unit = {}
    ) {
        launchUI {
            try {
                block()
            } catch (e: Throwable) {
                error(e)
            } finally {
                complete()
            }
        }
    }
複製代碼

那咱們的VIewModel中的getBanner方法這樣寫就行了:框架

fun getBanner() {
        launch({
            val result = repository.getBanner()
            if (result.errorCode == 0) {
                LogUtils.d(result.data)
            }
        })

       // 若是要處理error,以下
         /*launch({
            val result = repository.getBanner()
            if (result.errorCode == 0) {LogUtils.d(result.data)}
        }, {
            //處理error
            LogUtils.d(it.message)
        })*/
    }
複製代碼

又有小夥伴說了,那我想把code不等於0的時候全拋出錯誤,統一處理怎麼辦? 那咱們就再封裝一下,在BaseView中加入: 咱們把統一異常處理先抽出來:

/**
     * 異常統一處理
     */
    private suspend fun <T> handleException(
        block: suspend CoroutineScope.() -> BaseResult<T>,
        success: suspend CoroutineScope.(BaseResult<T>) -> Unit,
        error: suspend CoroutineScope.(ResponseThrowable) -> Unit,
        complete: suspend CoroutineScope.() -> Unit
    ) {
        coroutineScope {
            try {
                success(block())
            } catch (e: Throwable) {
                error(ExceptionHandle.handleException(e))
            } finally {
                complete()
            }
        }
    }
複製代碼

而後再寫一個 executeResponse 方法來過濾:

/**
     * 請求結果過濾
     */
    private suspend fun <T> executeResponse(
        response: BaseResult<T>,
        success: suspend CoroutineScope.(T) -> Unit
    ) {
        coroutineScope {
            if (response.errorCode == 0 ) success(response.data)
            else throw ResponseThrowable(response.errorCode, response.errorMsg)
        }
    }
複製代碼

最後咱們再寫一個 launchOnlyresult 方法把他們結合起來:

fun <T> launchOnlyresult(
        block: suspend CoroutineScope.() -> BaseResult<T>,
        success: (T) -> Unit,
        error: (ResponseThrowable) -> Unit = { },
        complete: () -> Unit = {}
    ) {
       launchUI {
            handleException(
                { withContext(Dispatchers.IO) { block() } },
                { res ->
                    executeResponse(res) { success(it) }
                },
                {
                    error(it)
                },
                {
                    complete()
                }
            )
        }
    }
複製代碼

異常類的代碼就不貼了,沒什麼好說的,末尾會給Demo地址,在裏面看吧,如今咱們獲取Banner數據就變成這樣了:

fun getBanner() {
        launchOnlyresult({ repository.getBanner() }, {
              LogUtils.d(it)  // it是Banner 數據 
        })
     // 處理Error 
        /*launchOnlyresult({ repository.getBanner() }, {
            mBanners.value = it
        },{
            LogUtils.d(it.errMsg)
        })*/
    }
複製代碼

咱們的一個請求已經能夠簡單成這個樣子了,相比於用RxJava的方式是否是更舒服呢。說到這裏有的兄弟可能就說了,單個網絡請求確實很簡單,可是若是多個呢?還有些請求要依賴其餘請求的結果呢?咱們在業務邏輯愈來愈複雜,RxJava有多種操做符來使用,你這個要怎麼搞?
接下來另外一我的物要登場了,帶着這些問題咱們再來講下協程的另外一個東西 Flow 異步流

3.2 Flow

帶着上面的問題咱們看下Flow 能幹什麼,看着名字可能有些陌生,可是咱們瞭解以後確定又會很是熟悉。他翻譯成中文是 意思,咱們在協程中,作異步能夠返回一個值,當咱們想返回多個值的時候,Flow就開始展示他的做用了,咱們看下具體使用場景: 咱們看玩安卓的 導航數據項目列表數據 兩個接口,獲取項目列表的時候須要依賴導航數據接口裏邊的 id,咱們來用Flow實現 首先是Servie:

/**
     * 導航數據
     */
    @GET("project/tree/json")
    suspend fun naviJson(): BaseResult<List<NavTypeBean>>

    /**
     * 項目列表
     * @param page 頁碼,從0開始
     */
    @GET("project/list/{page}/json")
    suspend fun getProjectList(@Path("page") page: Int, @Query("cid") cid: Int): BaseResult<HomeListBean>
複製代碼

ViewModel中的實現:

@ExperimentalCoroutinesApi
    @FlowPreview
    fun getFirstData() {
       launchUI {
            flow { emit(repository.getNaviJson()) }
                .flatMapConcat {
                    return@flatMapConcat if (it.isSuccess()) {
                        // 業務操做 ....
                        // LogUtils.d(it)  // it 是BaseResult<List<NavTypeBean>>
                        // ...
                        flow { emit(repository.getProjectList(page, it.data[0].id)) }
                    } else throw ResponseThrowable(it.errorCode, it.errorMsg)
                }.onStart{
                    // 會在 emit 發射以前調用 
                }
                .flowOn(Dispatchers.IO) // 這個是指煩氣發射的所在協程
                .onCompletion { 
                    // 流執行完畢會調用
                }
                .catch { 
                    // 遇到錯誤時會調用
                }
                .collect { 
                    // 收集 ,FLow只有在咱們
                    LogUtils.d(it)  // it 是BaseResult<HomeListBean>
                }

        }
    }
複製代碼

有的兄弟可能看到上邊代碼會說,似曾相識啊,沒錯跟RxJava是一個思想,Flow只能運行在協程中,上邊的代碼優化事後:是這個樣子的:

@ExperimentalCoroutinesApi
    @FlowPreview
    fun getFirstData() {
        launchUI {
            launchFlow { repository.getNaviJson() }
                .flatMapConcat {
                    return@flatMapConcat if (it.isSuccess()) {
                        navData.addAll(it.data)
                        it.data.forEach { item -> navTitle.add(item.name) }
                        launchFlow { repository.getProjectList(page, it.data[0].id) }
                    } else throw ResponseThrowable(it.errorCode, it.errorMsg)
                }
                .onStart { defUI.showDialog.postValue(null) }
                .flowOn(Dispatchers.IO)
                .onCompletion { defUI.dismissDialog.call() }
                .catch {
                    // 錯誤處理
                    val err = ExceptionHandle.handleException(it)
                    LogUtils.d("${err.code}: ${err.errMsg}")
                }
                .collect {
                    if (it.isSuccess()) items.addAll(it.data.datas)
                }
        }

    }
複製代碼

Demo中使用了LiveData 更新數據,若是把全部東西都貼出來實在有點多,只放了部分代碼。來簡單說下這些操做符的做用吧:

  • flow:構建器,他能夠發射數據多個數據,用**emit()**來發射
  • flatMapConcat :這個是在一個流收集完成以後,再收集下一個流
  • onStart:這個看名字估計也能猜出來,就是在發射以前作一些事情,咱們能夠在這裏再 emit()一個數據,他會在flow裏邊的數據發射以前發射,咱們上邊的例子,是在OnStart裏邊打開了等待框
  • flowOn:這個就是指定咱們的流運行在那個協程裏邊,咱們指定的是 Dispatchers.IO
  • onCompletion :是在全部流都收集完成了,就會觸發,咱們能夠在這裏取消等待框再合適不過了
  • catch:這個就是遇到錯誤的時候會觸發,咱們我錯誤處理就是在這裏來作了
  • collect:這個就是收集器的意思,咱們的結果都在這裏來處理。也只有咱們調用了這個收集方法,數據才真正的開始發射了,這也是官方說的一句話,流是冷的,就是這個意思

臥槽,無情,這TM明明就跟RxJava是孿生兄弟啊,你說的沒錯,FLow,他還有好多操做符供咱們使用。
好比 :
zip 合併流
flatMapMerge 讓流併發進行
transform 轉換操做符
在這裏就不一 一列舉了。 他還提供了 轉換成 響應式流 Reactive Streams(RxJava)的方法。
相信熟悉RxJava的你,分分鐘鍾就能夠上手的

3.2其餘小例子

3.2.1 驗證碼倒計時

有了上面的介紹,咱們對協程確定有了或多或少的瞭解,我在公司項的新項目中也已經開始使用了,再分享個小例子,新項目中一第一個作的就是登陸功能,既然是登陸就少不了驗證碼倒計時,咱們用 協程+LiveData 來實現他:

@ExperimentalCoroutinesApi
 fun getSmsCode(phone: String) {
     viewModelScope.launch {
            flow {
                (60 downTo 0).forEach {
                    delay(1000)
                    emit("$it s")
                }
            }.flowOn(Dispatchers.Default)
                .onStart {
                     // 倒計時開始 ,在這裏可讓Button 禁止點擊狀態
                }
                .onCompletion {
                    // 倒計時結束 ,在這裏可讓Button 恢復點擊狀態
                }
                .collect {
                    // 在這裏 更新LiveData 的值來顯示到UI
                    smsCode.value = it
                }
        }
}
複製代碼

這裏用了一個delay(1000),他和線程的sleep() 相似,可是他是非阻塞的,他不會阻塞線程運行,但他會讓協程進入等待狀態,咱們上面的代碼就是每隔一秒,發射一個值,用LiveData去更新Button的文字顯示。 這樣一個倒計時功能就實現了。

最後

附上Demo地址: github.com/AleynP/MVVM…
有興趣的能夠看一下,我如今也是處於學習的過程,有不足的地方還望多多指點,咱們共同進步。

Demo是一個基於MVVM的快速開發框架,上面的代碼都是用的裏邊的例子,也是用的鴻大大的 WanAndroid 接口。

相關文章
相關標籤/搜索