一轉眼kotlin已經轉正兩年多,KT的各類語法糖、高階函數、擴展函數等等。真的是讓人愛不釋手。一點都不吹牛逼,剛開始用Kotlin的時候,我徹底不知道 協程 這個概念。後來記得有個朋友問我說:你知道協程嗎?我說我確定知道啊,我從上大學的時候就一直在用,買車票什麼的都是用他買的啊。比12306好用。java
他來了個很無奈的表情說: 不是說你買票的那個攜程,是Kotlin的協程,協程!!協程!!協程。查閱了一番資料事後,才發現 協程 這個概念好多年前就已經有了,近幾年才普遍使用起來,阿里也是開源了好幾個協程相關的框架,據說淘寶就是用協程來渡過雙十一的,以前作項目一直用的Java,Java並無提供對協程的直接支持,對這個概念一直沒有過接觸,像Go、Python都是提供了對協程直接支持的。固然了,今天的主角,Kotlin也是提供了對 協程支持的。git
咱們暫時理解爲:他跟線程差很少,線程的調度是基於CPU算法和優先級,仍是要跟底層打交道的,協程是徹底由應用程序來調用的,可是他仍是要基於線程來運行的。他比線程更經量,開銷很小。github
光說不練假把戲,通過一些嘗試後,分享一下我在項目中的使用,說到異步呢,在咱們Android程序中最經常使用的就是網絡請求吧,UI線程不能進行耗時操做,咱們會把網絡請求、文件讀寫這些耗時操做放在子線程中,如今咱們能夠用協程來實現。算法
說到網絡請求就要說到咱們的網紅庫 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>>
複製代碼
和以前咱們寫的有什麼區別呢:瀏覽器
下邊的用法是在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 異步流
帶着上面的問題咱們看下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 更新數據,若是把全部東西都貼出來實在有點多,只放了部分代碼。來簡單說下這些操做符的做用吧:
臥槽,無情,這TM明明就跟RxJava是孿生兄弟啊,你說的沒錯,FLow,他還有好多操做符供咱們使用。
好比 :
zip 合併流
flatMapMerge 讓流併發進行
transform 轉換操做符
在這裏就不一 一列舉了。 他還提供了 轉換成 響應式流 Reactive Streams(RxJava)的方法。
相信熟悉RxJava的你,分分鐘鍾就能夠上手的
有了上面的介紹,咱們對協程確定有了或多或少的瞭解,我在公司項的新項目中也已經開始使用了,再分享個小例子,新項目中一第一個作的就是登陸功能,既然是登陸就少不了驗證碼倒計時,咱們用 協程+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 接口。