更优雅的使用回调函数 —— Kotlin 协程

简断截说,上代码!

举例:

1
2
3
4
5
6
7
8
9
10
11
fun login(name:String,pass:String){
HttpMethods.webService.login(name,pass){
override fun onSuccess(bean: LoginBean) {
connect(bean.token)
}
override fun onError(e: Exception) {
}
}
fun connect(token:String){
......//调用方法,从回调函数获取连接状态
}

上面的代码是我们比较常见的一种逻辑,发起登录请求,登录成功后使用 token 去连接其他服务,在连接成功后,继续执行其他逻辑。如果逻辑再复杂一些,比如连接失败后根据错误码去重新登录等,我们就会陷入“回调地狱”,使得代码的可读性大大降低。


就这一情况我们可以使用 Kotlin 协程来改造我们的代码:

1
2
3
4
5
6
7
8
9
suspend fun login(name:String,pass:String):LoginBean = suspendCancellableCoroutine { ctn->
HttpMethods.webService.login(name,pass){
override fun onSuccess(bean: LoginBean) {
ctn.resume(bean) //协程恢复,返回结果
}
override fun onError(e: Exception) {
ctn.resumeWithException(e) //协程恢复,抛出异常
}
}

connect 函数的改造与 login 函数的改造思路相同,即:在获取到我们需要的数据的位置使用 continuation.resume 函数,在需要抛出异常的位置使用 continuation.resumeWithException 函数。


那么我们如何使用这两个改造完成的函数呢?代码示例如下:

1
2
3
4
5
launch{
val bean = login(name,pass) //在获取结果之前,协程是挂起状态不会执行下一步
val result = connect(bean.token)

}

相比一开始的代码,此时的代码是不是看着逻辑更为清晰更为优雅。

改造普通函数变成挂起函数,最常用的两个函数是 suspendCoroutinesuspendCancellableCoroutine,这两者几乎大同小异,都是接受一个lambda表达式作为参数。区别是,前者传入的Continuation<T>,后者传入的是CancellableContinuation<T>。后者可以执行continuation.invokeOnCancellation { } 函数,该函数会在协程被取消时执行。可用于一些如资源需要关闭、或者网络请求等场景,实现在协程被取消时,关闭资源or取消网络请求。


扩展阅读+温故知新:

上面我们提到了 suspend 关键字,用于修饰挂起函数,挂起函数会将当前协程挂起(可以理解类似线程阻塞),直至continuation.resume 函数执行后才能继续执行后续代码。

这无疑会导致运行速度降低,如果我们后面的部分代码不需要挂起函数的返回值,需要实现类似并行的效果,直到我们需要挂起函数结果时才挂起当前的协程该如何操作呢?

答案是 async 函数:

1
2
3
4
5
6
7
8
launch{
//被async函数包含,不会挂起当前协程,相当于在其他协程执行代码
val bean = async{ login(name,pass) }
...... //其他逻辑
//执行至此需要上一步挂起函数的返回值时,执行await()函数
val result = connect(bean.await().token)

}

async 函数的返回值是 Deferred,该接口下的await函数是一个挂起函数。执行至此时会挂起协程,直至 async 函数内的挂起函数执行完毕获得返回值。