Plain's Blog

休想打败我的生活🔥

将传统回调方式网络请求改造成 Kotlin 协程(Coroutine)的形式

Plain's Avatar 2020-09-17 Kotlin笔记

传统的网络请求,往往采用回调的方式接收响应结果,比如 onSuccessonFailed 分别对应成功失败的情况,而现在由于 Kotlin 协程的出现,我们只需要一行代码即可实现网络请求,非常简洁。

下面将以和风天气SDK 为例,将其提供的回调方式的网络请求改为协程的方式实现。

先来看一下正常使用和风天气SDK请求天气数据的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* 传统回调方式
*/
fun getNowWeatherWithCallback(
context: Context,
listener: HeWeather.OnResultWeatherNowBeanListener
) {
HeWeather.getWeatherNow(context, "", Lang.ENGLISH, Unit.METRIC, object : HeWeather.OnResultWeatherNowBeanListener {
override fun onSuccess(now: Now?) {
listener.onSuccess(now)
}

override fun onError(error: Throwable) {
listener.onError(error)
}

})
}

代码很简单,通过调用 SDK 中的 HeWeather.getWeatherNow 发起网络请求,通过注册 HeWeather.OnResultWeatherNowBeanListener 接口接收请求结果。

那么改造成协程后效果如何呢!

1
val now = getNowWeatherWithCoroutine(this@HandleActivity)

没错,仅需一行代码,就可以实现网络请求,那么是如何实现的呢?

将传统回调的方式改造成协程的方式,关键使用 suspendCoroutine 方法。

1
2
3
4
5
suspend fun getNowWeather(context: Context): Now? {
return suspendCoroutine { continuation ->

}
}

这个方法提供一个参数 continuation ,先来看下它的实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* Interface representing a continuation after a suspension point that returns a value of type `T`.
*/
@SinceKotlin("1.3")
public interface Continuation<in T> {
/**
* The context of the coroutine that corresponds to this continuation.
*/
public val context: CoroutineContext

/**
* Resumes the execution of the corresponding coroutine passing a successful or failed [result] as the
* return value of the last suspension point.
*/
public fun resumeWith(result: Result<T>)
}

里面有一个关键的方法 resumeWith ,通过注释可以知道,它的作用是:

恢复执行相应的协程,将成功或失败的结果作为上次挂起点的返回值

知道了上面的方法,就可以这么使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
suspend fun getNowWeather(context: Context): Now? {
return suspendCoroutine { continuation ->
HeWeather.getWeatherNow(context, "", Lang.ENGLISH, Unit.METRIC, object : HeWeather.OnResultWeatherNowBeanListener {
override fun onSuccess(now: Now?) {
continuation.resume(now)
}

override fun onError(error: Throwable) {
continuation.resumeWithException(error)
}

})
}
}

可以看到在之前的回调方法 onSuccessonError 中分别调用了 continuation.resume(now)continuation.resumeWithException(error)

其中 resume 是用来恢复成功的情况,它的实现如下:

1
2
3
4
5
6
7
/**
* Resumes the execution of the corresponding coroutine passing [value] as the return value of the last suspension point.
*/
@SinceKotlin("1.3")
@InlineOnly
public inline fun <T> Continuation<T>.resume(value: T): Unit =
resumeWith(Result.success(value))

resumeWithException 是用来恢复失败的情况,它的实现如下:

1
2
3
4
5
6
7
8
/**
* Resumes the execution of the corresponding coroutine so that the [exception] is re-thrown right after the
* last suspension point.
*/
@SinceKotlin("1.3")
@InlineOnly
public inline fun <T> Continuation<T>.resumeWithException(exception: Throwable): Unit =
resumeWith(Result.failure(exception))

可以看到它们分别调用了 ContinuationresumeWith

那么问题来了,上面所说的挂起点是在哪呢,没错就是最开始调用的 suspendCoroutine 的方法,来看下它的实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* Obtains the current continuation instance inside suspend functions and suspends
* the currently running coroutine.
*
* In this function both [Continuation.resume] and [Continuation.resumeWithException] can be used either synchronously in
* the same stack-frame where the suspension function is run or asynchronously later in the same thread or
* from a different thread of execution. Subsequent invocation of any resume function will produce an [IllegalStateException].
*/
@SinceKotlin("1.3")
@InlineOnly
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()
}

好了,既然已经改造完毕了,该如何使用呢?

细心的同学肯定发现了,在 getNowWeather 方法前多了一个新的关键字 suspend ,意味挂起的意思,而带有这个关键字的函数,我们称它为挂起函数,挂起函数有个特殊的地方,它的调用必须在 CoroutineScope 中进行,否则IDE会报错。

一般情况下使用最原始的 CoroutineScope 是没有问题的,但是它并不会帮我们处理生命周期等情况,所以更推荐使用 lifecycleScope ,它是 lifecycle-runtime-ktx 库中提供的扩展函数。

1
2
3
4
5
6
private fun getWeatherWithCoroutine() {
lifecycleScope.launch {
val now = getNowWeather(this@MainActivity)
// do something ...
}
}

可以看到非常简单的就完成了网络请求,并且消除了回调。

那么问题来了,网络请求失败的情况该如何处理呢?答案就在上面回调 onError 时调用的 resumeWithException 方法,它会抛出一个异常,那么自然而然,我们捕获它就可以进行失败的情况的处理。

1
2
3
4
5
6
7
8
9
10
private fun getWeatherWithCoroutine() {
lifecycleScope.launch {
try {
val now = getNowWeather(this@HandleActivity)
// success, do something ...
} catch (e: Throwable) {
// failed, do something ...
}
}
}

❤️ done

本文作者 : Plain
This blog is under a CC BY-NC-SA 3.0 Unported License
本文链接 : https://plain-dev.com/kotlin-callback-to-coroutine/

本文最后更新于 天前,文中所描述的信息可能已发生改变