之前在网上找关于kotlin协程的教程,基本都是1.0及以下的版本。(现在的版本是1.3.2)很多api都被改掉了,或者是过时了。
后面也有一些优秀的文章,使自己结合着官方的文档,才渐渐搞懂
感谢:https://www.jianshu.com/p/32a2066b8710(如果你还完全不懂协程,请先看这个,很详细)
https://www.kotlincn.net/docs/reference/coroutines/basics.html(看完上面的,再看看这个官网,最新写法,略有不同)
https://www.jianshu.com/p/6e6835573a9c(这位大佬,玩转retorfit+协程,打造网络请求,深受启发,我借鉴改造了一下,写了文章后面的网络请求)
https://www.jianshu.com/p/b0b9e04d8793
官方github地址:https://github.com/Kotlin/kotlinx.coroutines
emm。。使用协程有什么好处呢?
我们可以用顺序编程的方式实现异步以及并发任务,这比响应式流的链式调用更加直观,给了我们同步编程的体验;其次,在 IO 密集型任务中,协程的挂起操作相比老式的线程阻塞操作大大提高了性能,这成为了协程的杀手级特性之一。协程挂起几乎无代价,无需上下文切换或涉及OS。
协程并不是为了取代线程,协程对线程进行抽象,你可以看成协程是一个异步调用的框架,解决了之前线程间协作代码繁琐的问题
前奏
在app下的build.gradle添加依赖。并且保证ext.kotlin_version = '1.3.50'为这个版本
dependencies {
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.2'
}
然后就可以在代码里这样写啦,其中可以将launch换成async。这个两个的区别是async有返回值
GlobalScope.launch {
Log.d("GlobalScope : ", "thread:: ${Thread.currentThread().name}")
}
CoroutineScope(Dispatchers.Main).launch {
Log.d("CoroutineScope : ", "thread:: ${Thread.currentThread().name}")
}
MainScope().launch {
Log.d("MainScope ", "thread:: ${Thread.currentThread().name}")
}
log是这样打印的。可以看到CoroutineScope 我们是指定在Main里的,所以他所属的线程是在我们的主线程里;而MainScope也是如此;而GlobalScope顾名思义全局的,运行在后台共享的线程中。当然这都是上下文决定的。 可以看看 MainScope看看源码, 里面指定了上下文Dispatchers.Main。CoroutineScope 或者GlobalScope,不指定的话,都是默认Dispatchers.Default
协程上下文包含一个 协程调度器 (参见 CoroutineDispatcher)它确定了哪些线程或与线程相对应的协程执行。协程调度器可以将协程限制在一个特定的线程执行,或将它分派到一个线程池,亦或是让它不受限地运行。所有的协程构建器诸如 launch 和 async 接收一个可选的 CoroutineContext 参数,它可以被用来显式的为一个新协程或其它上下文元素指定一个调度器。(容我引用官网的一描述)
21:42:07.250 12446-12471/com.nativeproject D/GlobalScope :: thread:: DefaultDispatcher-worker-119-11-03
21:42:07.330 12446-12446/com.nativeproject D/CoroutineScope :: thread:: main
2019-11-03 21:42:07.330 12446-12446/com.nativeproject D/MainScope: thread:: main
协程执行耗时操作,完毕后更新ui
知道了这些有啥用呢?当然是有用啦,比如我们要做某些耗时操作,比如网络或者io等,就可以在协程里操作啦。以前我们可能会new Thread开线程就干,各种回调。现在看看协程优雅操作并更新UI的吧
CoroutineScope(Dispatchers.Main).launch {
val result = async(Dispatchers.IO) {
//使用okhttp使用同步请求,完事将response返回
val request = Request.Builder().url("http://www.baidu.com").build()
val response= OkHttpClient().newCall(request).execute()
response
}
//等待异步执行的结果
val response = result.await()
//返回的结果,直接显示在sample_text这个textview上,也就是更新UI
sample_text.text = response.body()?.string()
Log.e("----", "CoroutineScope : ${Thread.currentThread().name}")
}
一气呵成,没有半点不妥。当然这里使用了async开启了新的协程,也可以使用withContext直接切换上下文。效果也是一样的
CoroutineScope(Dispatchers.Main).launch {
val result = withContext(Dispatchers.IO) {
val request = Request.Builder().url("http://www.baidu.com").build()
val call = OkHttpClient().newCall(request)
val execute = call.execute()
return@withContext execute.body()?.string()
}
sample_text.text = result
Log.e("----", "CoroutineScope : ${Thread.currentThread().name}")
}
至于这两个怎么选择使用,看以看看这篇文章async vs withContext 有大佬已经用实验比较了一波
需要并发时使用async,不需要则用withContext更高效
异步中更新ui
val request = Request.Builder().url("http://www.baidu.com").build()
val call = OkHttpClient().newCall(request)
call.enqueue(object : Callback{
override fun onResponse(call: Call, response: Response) {
response.body()?.string()?.let { threadToMain(it) }
}
override fun onFailure(call: Call, e: IOException) {
}
})
fun threadToMain(string: String){
MainScope().launch {
sample_text.text = string
}
}
这里则是用异步的网络请求,这个场景,表示一些第三方的框架可能回到异步,我们也是可以用协程去直接更新UI的。(不一定是okhttp的异步请求,这里只是演示场景)
主菜,用协程打造通用的网络请求
先准备个基本的实体类,一般json格式都是这样的,这里用泛型封装了一下
class BaseResult<T> {
var errorCode: Int = 0
var errorMsg: String = ""
var data: T? = null
}
再弄一个,成功或者失败,完成接口回调
class RetrofitCoroutineDSL<T> {
var api: (Call<ResponseBody>)? = null
internal var onSuccess: ((BaseResult<T>) -> Unit)? = null
private set
internal var onFail: ((msg: String, errorCode: Int) -> Unit)? = null
private set
internal var onComplete: (() -> Unit)? = null
private set
internal var onSuccessList: ((BaseResult<List<T>>) -> Unit)? = null
private set
/**
* 获取数据成功,json数据是数组的情况
* @param block (T) -> Unit
*/
fun onSuccessList(block: (BaseResult<List<T>>) -> Unit) {
this.onSuccessList = block
}
/**
* 获取数据成功
* @param block (T) -> Unit
*/
fun onSuccess(block: (BaseResult<T>) -> Unit) {
this.onSuccess = block
}
/**
* 获取数据失败
* @param block (msg: String, errorCode: Int) -> Unit
*/
fun onFail(block: (msg: String, errorCode: Int) -> Unit) {
this.onFail = block
}
/**
* 访问完成
* @param block () -> Unit
*/
fun onComplete(block: () -> Unit) {
this.onComplete = block
}
internal fun clean() {
onSuccess = null
onComplete = null
onFail = null
}
}
接下来就是统一请求模版
private val scope = MainScope()
fun <T> retrofit(clazz: Class<T>, dsl: RetrofitCoroutineDSL<T>.() -> Unit) {
//在主线程中开启协程
scope.launch(Dispatchers.Main) {
val coroutine = RetrofitCoroutineDSL<T>().apply(dsl)
coroutine.api?.let { call ->
//async 并发执行 在IO线程中
val deferred = async(Dispatchers.IO) {
try {
call.execute() //已经在io线程中了,所以调用Retrofit的同步方法
} catch (e: ConnectException) {
coroutine.onFail?.invoke("网络连接出错", -1)
null
} catch (e: IOException) {
coroutine.onFail?.invoke("未知网络错误", -1)
null
}
}
//当协程取消的时候,取消网络请求
deferred.invokeOnCompletion {
if (deferred.isCancelled) {
call.cancel()
coroutine.clean()
}
}
//await 等待异步执行的结果
val response = deferred.await()
if (response == null) {
coroutine.onFail?.invoke("返回为空", -1)
} else {
val t = response.body()?.string()
try {
//Gson解析泛型报错的处理
val type = ParameterizedTypeImpl(BaseResult<T>().javaClass, arrayOf<Class<*>>(clazz))
val data = Gson().fromJson<BaseResult<T>>(t, type)
coroutine.onSuccess?.invoke(data)
}catch (e : JsonSyntaxException){
coroutine.onFail?.invoke("解析数据错误",-1)
}
}
coroutine.onComplete?.invoke()
}
}
}
//当activity被销毁时,取消任务
fun destory(){
scope.cancel()
}
因为我们封装了,基础的bean类,所以中间用Gson解析时要做一下处理,不然会报 java.lang.ClassCastException: com.google.gson.internal.LinkedTreeMap cannot be cast to XXXX(参考这个处理https://www.jianshu.com/p/d62c2be60617)
下面就是直接使用啦,其实也没什么特别说明的,注意activity销毁取消任务,完整的dome在git上
https://github.com/WoBuShuo/MainScope
github上已经搞成了mvp模式,喜欢的同学可以点个start哦
retrofit(RankBean::class.java) {
api = Retrofit.Builder().baseUrl("https://www.wanandroid.com/").build()
.create(HttpServer::class.java).rankData()
onSuccess {
val bean = it.data as RankBean
Log.e("-----", "data: " + bean.total)
}
//失败回调
onFail { msg, errorCode ->
}
//完成回调
onComplete {
}
}