2020-02-26 17:31:12

“吹上天”的Kotlin协程 要不看下实战?

12 / 0 / 0 / 0

作者: 鸿洋 来源: 鸿洋

前言

一转眼kotlin已经转正两年多,KT的各种语法糖、高阶函数、扩展函数等等。真的是让人爱不释手。一点都不吹牛逼,刚开始用Kotlin的时候,我完全不知道 协程 这个概念。

后来记得有个朋友问我说:你知道协程吗?

我说我肯定知道啊,我从上大学的时候就一直在用,买车票什么的都是用他买的啊。比12306好用。

他来了个很无奈的表情说:不是说你买票的那个携程,是Kotlin的协程 ,协程!!协程!!协程。

我心想:What?还有这个东西吗。我就回了他说:老子不知道,你能把我咋地。虽说嘴很硬,但是心里虚啊,赶紧打开浏览器,搜索框输入了: “协程”两个字,很用力按下了回车,走你。

哎哟,卧槽,第一个出来的还真就是携程网的广告“放心价格,放心服务”,斜眼笑~~~~。

2 了解

查阅了一番资料过后,才发现 协程 这个概念好多年前就已经有了,近几年才广泛使用起来,阿里也是开源了好几个协程相关的框架,听说淘宝就是用协程来渡过双十一的,之前做项目一直用的Java,Java并没有提供对协程的直接支持,对这个概念一直没有过接触,像Go、Python都是提供了对协程直接支持的。

当然了,今天的主角,Kotlin也是提供了对 协程支持的。

我们暂时理解为:他跟线程差不多,线程的调度是基于CPU算法和优先级,还是要跟底层打交道的,协程是完全由应用程序来调用的,但是他还是要基于线程来运行的。

3 实战

光说不练假把戏,经过一些尝试后,分享一下我在项目中的使用,说到异步呢,在我们Android程序中最常用的就是网络请求吧,UI线程不能进行耗时操作,我们会把网络请求、文件读写这些耗时操作放在子线程中,现在我们可以用协程来实现。

3.1 网络请求

说到网络请求就要说到我们的网红库 Retrofit,好多项目中都是用RxJava+Retrofit来进行网络请求,自从开始使用协程,也放弃了使用RxJava,在Retrofit 2.6 之前,想用协程配合Retrofit来进行网络请求,我们的请求结果还要做一次转换,对此呢,我们Android界的大咖 JakeWharton还专门写了个库 retrofit2-kotlin-coroutines-adapter 来做转换,有兴趣的可以看一下。

https://github.com/JakeWharton/retrofit2-kotlin-coroutines-adapter

不过,Retrofit 2.6 之后,直接对kotlin 的协程做了支持,也不需要用到这个库了。我们来看一下实际代码,依然使用鸿大大的WanAndroid API来做例子。 

比如我们要获取Banner图片 我们的 XXService:

/**  
* 玩安卓轮播图  
*/  
@GET("banner/json")  
suspend fun getBanner(): BaseResult<List<BannerBean>>  

和之前我们写的有什么区别呢:

  1. 前面多了suspend 关键字,带有这个关键字的函数,只有在协程中才能调用,在普通函数调用会报错的,编译也过不了

  2. 返回结果只直接写对应的Bean就好了,不需要固定类型来包装

下边的用法是在ViewModel中来使用的,如果想在Activity或者Fragment中使用,是一样的,只不过启动协程的时候写法有些不同。 

下面在我们的VIewModel中:

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.3 其他小例子

3.3.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地址:

https://github.com/AleynP/MVVMLin

有兴趣的可以看一下,我现在也是处于学习的过程,有不足的地方还望多多指点,我们共同进步。

Demo是一个基于MVVM的快速开发框架,上面的代码都是用的里边的例子,也是用的鸿大大的 WanAndroid 接口。

PS: 如本文对您有疑惑,可加QQ:1752338621 进行讨论。

0 条评论

0
0
官方
微信
官方微信
Q Q
咨询
意见
反馈
返回
顶部