本文主要介绍以下,对于现有项目中的接口回调逻辑,和Rxjava中的订阅逻辑代码,修改成suspend挂起函数的形式。
1 接口回调的改写
一般接口回调的场景,我们都可以改成挂起函数的形式,比如网络接口的成功与失败的回调,动态权限的申请回调,或者一下其他的异步回调接口等。
(1)使用suspendCoroutine
以OkHttp中网络请求Call为例子,参考Kotlin Coroutines(协程) 完全解析(三),封装异步回调、协程间关系及协程的取消一文,我们可以对Call对象添加一个await()
扩展函数,代码如下:
suspend fun <T> Call<T>.await(): T = suspendCoroutine { cont ->
enqueue(object : Callback<T> {
override fun onResponse(call: Call<T>, response: Response<T>) {
if (response.isSuccessful) {
cont.Continuation(response.body()!!)
} else {
cont.resumeWithException(ErrorResponse(response))
}
}
override fun onFailure(call: Call<T>, t: Throwable) {
cont.resumeWithException(t)
}
})
}
即通过suspendCoroutine
或者suspendCancellableCoroutine
,使用其内部的Continuation
或者CancellableContinuation
的resume
方法来正常恢复协程,或者通过resumeWithException
来实现原理的异常error回调。
public suspend inline fun <T> suspendCoroutine(crossinline block: (Continuation<T>) -> Unit): T =
suspendCoroutineUninterceptedOrReturn { c: Continuation<T> ->
val safe = SafeContinuation(c.intercepted())
block(safe)
safe.getOrThrow()
}
public suspend inline fun <T> suspendCancellableCoroutine(
crossinline block: (CancellableContinuation<T>) -> Unit
): T =
suspendCoroutineUninterceptedOrReturn { uCont ->
val cancellable = CancellableContinuationImpl(uCont.intercepted(), resumeMode = MODE_CANCELLABLE)
// 和 suspendCoroutine 的区别就在这里,如果协程已经被取消或者已完成,就会抛出 CancellationException 异常
cancellable.initCancellability()
block(cancellable)
cancellable.getResult()
}
转化成挂起函数的思路也很简单,就是在原来的接口success和error回调上封装了一层,这样使得调用方可以使用协程的形式来减少接口嵌套的情况。
(2)使用suspendCancellableCoroutine
上面介绍了suspendCoroutine
的使用,现在介绍一下suspendCancellableCoroutine
的使用,两者的区别,就是suspendCancellableCoroutine
可以通过Job.cancel()
来取消(会抛出CancellationException
),suspendCancellableCoroutine
返回的CancellableContinuation
对象,我们可以调用其resume、resumeWithException和抛出CancellationException
来处理协程逻辑。
MainScope().launch {
try {
val user = fetchUser()
//由于fetchUser中产生来异常,所以下面的updateUser不会被调用到
updateUser(user)
} catch (exception: Exception) {
// Use try-catch or CoroutinesExceptionHandler to handle exceptions.
Log.d("demo", "$exception") // Prints "java.io.IOException".
}
// 如果上面使用了try-catch来捕获异常,那么下面的代码仍然可以执行到
doSomething()
}
// Fetches the user data from server.
private suspend fun fetchUser(): User = suspendCancellableCoroutine {
cancellableContinuation ->
fetchUserFromNetwork(object : Callback {
override fun onSuccess(user: User) {
cancellableContinuation.resume(user)
}
override fun onFailure(exception: Exception) {
// Invokes this line since the callback onFailure() is invoked.
cancellableContinuation.resumeWithException(exception)
}
})
}
private fun fetchUserFromNetwork(callback: Callback) {
Thread {
Thread.sleep(3_000)
//callback.onSuccess(User())
// Invokes onFailure() callback with "IOException()".
callback.onFailure(IOException())
}.start()
}
private fun updateUser(user: User) {
// Updates UI with [User] data.
}
interface Callback {
fun onSuccess(user: User)
fun onFailure(exception: Exception)
}
class User
2 RxJava的订阅回调转换成挂起函数
现在大多数项目都有使用RxJava,虽然很多文字宣传可以使用协程来替换RxJava的使用场景,但是一方面协程API还在不断的发展,另一方面现在的RxJava代码尚能接受,全盘替换成本和风险高,所以两者一起使用不失为一个最佳选择。
为了方便将RxJava的调用转为协程的挂起函数形式,jetbrains官方专门给出了实现,即使用kotlinx-coroutines-rx2
:
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-rx2:1.3.2"
以使用Single为例子,调用Single.await()可以帮我们将RxJava转换成一个suspendCancellableCoroutine
,流程跟我们上面的转换其实是一样的,源码如下:
使用示例:
MainScope().launch {
CoroutineScope(Dispatchers.Main).launch {
try {
val user = fetchUserFromServer().await()
updateUser(user)
} catch (e: Exception) {
Log.d("demo", "(4) {$e}, ${Thread.currentThread()}")
}
}
}
private fun fetchUserFromServer(): Single<User> =
Single.create<User> {
Log.d("demo", "(1) fetchUserFromServer start, ${Thread.currentThread()}")
Thread.sleep(3_000)
it.onSuccess(User())
Log.d("demo", "(2) fetchUserFromServer onSuccess, ${Thread.currentThread()}")
}.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
private fun updateUser(user: User) {
Log.d("demo", "(3) updateUser, ${Thread.currentThread()}")
}
class User
也就是我们原来的方法不需要做修改,直接在调用处使用kotlinx-coroutines-rx2
中定义的await函数就行,它会挂起当前协程的执行,然后在RxJava返回结果后恢复协程的执行。当然别忘了调用的时候要添加异常try-catch。
对于RxJava中的Maybe,Observable,kotlinx-coroutines-rx2
都提供了相应的扩展函数来供我们使用,赶紧在你的代码里尝试一下吧!
总结:
将回调函数或者RxJava的调用转换成协程的挂起函数调用,原理还是比较简单的,关于suspendCoroutine
和suspendCancellableCoroutine
的原理,
它们的关键实现都是调用suspendCoroutineUninterceptedOrReturn()
函数,它的作用是获取当前协程的实例,并且挂起当前协程或者不挂起直接返回结果。
协程中还有两个常见的挂起函数使用到了suspendCoroutineUninterceptedOrReturn()
函数,分别是delay()
和yield()
。
delay()
大家已经很熟悉了,yield()
就是让出当前协程的执行权,交给其他协程执行,可以实现类似交替执行的效果,暂时还没想到适用场景。
具体的源码分析可以参考文末的第一篇文章。
在一个Scope中调用其他的挂起函数时,我们还是要加上try-catch(注意应该加在协程体内部,在整个Scope上包try-catch是无法捕获异常的)
或者使用CoroutinesExceptionHandler
处理逻辑的,虽然kotlin宣称是没有受检异常checked exceptions的,但是为了流程的逻辑处理和代码的正常运行,我们还是要在调用处去捕获处理。
参考:
Kotlin Coroutines(协程) 完全解析(三),封装异步回调、协程间关系及协程的取消
Kotlin Coroutines in Android — Suspending Functions