Kotlin Coroutine 协程代码使用案例

2,355 阅读4分钟

本人项目中使用协程有一定的心得和积累,这里列举一些常用的案例,说明使用协程之后的好处

官方Activity案例

abstract class BaseCoroutineActivity : Activity(), CoroutineScope {

    protected lateinit var job: Job

    override val coroutineContext: CoroutineContext
        get() = job + Dispatchers.Main

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        job = Job()
    }

    override fun onBeforeDestroy() {
        super.onBeforeDestroy()
        job.cancel()
    }

}

所有 Activity 继承这个类就可以获得 Activity 生命周期管理下的协程 Scope,有几个特点:

  • Activity 本身是一个 CoroutineScope,可以方便 launch/async 等等代码的调用
  • 协程内的运行的代码在 Activity 关闭后会被取消,不会引起不必要的问题
  • 协程默认情况下的代码都运行在主线程,可以更新UI,由 Dispatchers.Main 决定的

Retrofit2

Retrofit2 在新版本中直接支持了声明 suspend 方法,如果使用过都会觉得比较爽,相对于 callback 和 RxJava 的方式要轻松许多

简单 Api

声明一个有 suspend 方法的 Api:

interface UserApi {
    @GET("/user/list/{page}")
    suspend fun fetchPage(page: Int): List<User>
}

在 Activity 调用这个 Api:

class ExampleActivity : BaseCoroutineActivity() {

    override fun onCreate(savedInstanceState: Bundle?, persistentState: PersistableBundle?) {
        super.onCreate(savedInstanceState, persistentState)

        launch {
            val api: UserApi = Retrofit.create(UserApi::class.java)
            val userList = api.fetchPage(0)
            someAdapter.setData(userList)
            someAdapter.notifyDataSetChanged()
        }

    }

}

fetchPage 是个挂起方法,即有挂起点,在没有返回结果回来之前 Activity finish 了,由于协程被取消了,挂起后的代码不再执行,不会引起一些例如NPE的问题,并且按理Retrofit会监控当前 suspend 方法对应协程的 job 运行情况而去取消 Call,节省网络资源的消耗

这里比起 callback 和 RxJava 要爽很多

分页拉取

使用 callback 和 RxJava 的方式在做分页逻辑时或多或少会引起一些难看的代码,但是使用协程不会,而且一看就明白

分页服务端一般会下发数据同时下发一个 hasMore 表示还有没有下一页

data class UserPage(
    val hasMore: Boolean,
    val list: List<User>
)

interface UserApi {
    @GET("/user/list/{page}")
    suspend fun fetchPage(page: Int): UserPage
}
launch {
    val api: UserApi = Retrofit.create(UserApi::class.java)
    var page = 0
    val allUsers = mutableListOf<User>()
    do {
        val userPage = api.fetchPage(page)
        allUsers.addAll(userPage.list)
        page ++
    } while(userPage.hasMore)
    someAdapter.setData(allUsers)
    someAdapter.notifyDataSetChanged()
}

这样的代码,完全不需要注释也能看懂

异步方法转 suspend

仿官方 delay 函数

假设我们再 Activity 中声明这些方法,毕竟需要一个线程环境,这里用 Activity 的 UI 线程,目的是理解方法本身

private val mHandler: Handler = Handler()

suspend fun delay(timeMills: Long) {
    suspendCancellableCoroutine<Unit> { continuation ->
        // 利用 Handler 做一个延迟
        val callback: () -> Unit = {
            continuation.resumeWith(Result.success(Unit))
        }
        mHandler.postDelayed(callback, timeMills)

        // 注意协程的取消逻辑,随时会被外部取消
        continuation.invokeOnCancellation {
            mHandler.removeCallbacks(callback)
        }
    }
}

本质上,上面的代码就是将 Handler 的 postDelayed 的 callback 形式转换为 suspend 方法,这样就可以使用再协程内了

OkHttp 实例

class OkHttpUtil(private val httpClient: OkHttpClient) {

    private suspend fun download(url: String): InputStream {
        // 挂起协程
        return suspendCancellableCoroutine {
            val call = download(url) { error, stream ->
                // 有结果了
                if (error != null) {
                    it.resumeWith(Result.failure(error))
                } else {
                    it.resumeWith(Result.success(stream!!))
                }
            }
            it.invokeOnCancellation {
                call.safeCancel()  // 这里是安全cancel掉
            }
        }
    }

    // 这里是平常的 callback 方式的请求代码
    private fun download(url: String, callback: (error: Throwable?, stream: InputStream?) -> Unit) : Call {
        val request = Request.Builder().url(url)
        val call = httpClient.newCall(request.build())
        call.enqueue(object : Callback {
            override fun onFailure(call: Call, e: IOException) {
                callback(e, null)
            }
            override fun onResponse(call: Call, response: Response) {
                if (!response.isSuccessful) {
                    callback(IOException("Response with ${response.code()} ${response.message()}"), null)
                    return
                }
                try {
                    callback(null, response.body().byteStream())
                } catch (e: Throwable) {
                    callback(e, null)
                }
            }
        })
        return call
    }
    
}

使用代码:


launch {
    val okHttpClient = ...
    val util = OkHttpUtil(okHttpClient)
    val stream = util.download("...some url")
    val file = File("... some file path")
    FileUtils.streamToFile(stream, file)
}

小结

即利用挂起协程(suspendCancellableCoroutine)和 恢复协程(resumeWith)可以实现任意的异步代码转换成 suspend 方法,更有利于我们写外部逻辑

反编译成 Java

Java 里面并没有 suspend 关键字,那么 kotlin 的 suspend 反编译后是什么呢

这里我们利用 AS 的工具来探索一下

首先,先任意新建 kotlin 文件写一个 suspend 函数

suspend fun test(arg: Any): Any {
    return Any()
}

使用下面的步骤得到反编译后的 Java 代码

如下

public final class Test2Kt {
   @Nullable
   public static final Object test(@NotNull Object arg, @NotNull Continuation $completion) {
      return new Object();
   }
}

可以看到,Java 方法比 Kotlin 的方法要多一个参数:Continuation $completion

那么 Retrofit 创建动态代理拦截方法执行时即是利用这个参数来判断一个方法是不是 suspend 方法,再利用 $completionresume 来返回结果, 如果这个 $completion 是一个 CancellableContinuation 的话,还可以利用 invokeOnCancellation 来取消当前的 Call

最后

协程旨在将异步代码简化为同步编码的方式来优化流程化的代码,提高阅读性,当然协程还有利于节省线程资源的消耗。