Kotlin 协程: callback如何转为suspend函数

1,974 阅读3分钟

作为前端开发者,我们对 JavaScript 的 Promiseasync/await 已经非常熟悉,它们极大地改善了异步编程的体验。当我们转向 Kotlin 开发时,会发现 Kotlin 的协程 (coroutines) 提供了类似的功能,甚至更加强大。本文将帮助你快速理解 Kotlin 协程,并学会如何将传统的回调函数转换为协程。

前端异步的痛点

在 JavaScript 中,回调函数是处理异步操作的常见方式。但是,当异步操作嵌套过多时,就会陷入“回调地狱”,代码难以维护和理解。Promiseasync/await 的出现,让异步代码看起来更像同步代码,大大提高了代码的可读性和可维护性。

Kotlin 协程:异步编程的新选择

Kotlin 协程是一种轻量级的并发编程解决方案。它允许你编写看起来像同步的代码,但实际上是异步执行的。Kotlin 协程的核心概念是 suspend 函数。suspend 函数可以暂停执行,并在稍后恢复执行,而不会阻塞线程。这使得编写异步代码变得更加简单和高效。

将回调函数转换为协程

假设我们有一个需要回调的函数,例如模拟网络请求:

import kotlin.random.Random
import kotlinx.coroutines.*
fun fetchDataCallback(callback: (String?, Throwable?) -> Unit) = CoroutineScope(Dispatchers.Default).launch {
    delay(Random.nextLong(1000, 3000)) // 模拟耗时操作
    if (Random.nextBoolean()) {
        val result = "Data fetched successfully"
        callback(result, null)
    } else {
        val error = RuntimeException("Failed to fetch data")
        callback(null, error)
    }
}

这个 fetchDataCallback 函数现在包含了一个错误处理的回调,为了将这个回调函数转换为协程,我们需要使用 suspendCancellableCoroutine 函数,并处理可能出现的异常:

import kotlinx.coroutines.*

suspend fun fetchData(): String {
    return suspendCancellableCoroutine { continuation ->
        fetchDataCallback { result, error ->
            if (error != null) {
                continuation.resumeWithException(error)
            } else {
                continuation.resume(result!!)
            }
        }
    }
}

suspendCancellableCoroutine 函数会创建一个协程,并将回调函数的结果或异常传递给 continuation.resume()continuation.resumeWithException()。现在,fetchData() 函数就是一个 suspend 函数,我们可以在协程中使用它,并使用 try-catch 处理异常:

fun main() = runBlocking {
    try {
        val data = fetchData()
        println("Data: $data")
    } catch (e: Exception) {
        println("Error: ${e.message}")
    }
}

错误处理

在 Kotlin 协程中,错误处理通常使用 try-catch 块。当 suspend 函数抛出异常时,可以使用 try-catch 块捕获并处理它。在上面的例子中,如果 fetchData() 函数抛出异常,catch 块会捕获并打印错误信息。

类似 Promise.all 的并发执行

在前端开发中,我们经常需要并发执行多个 Promise,并等待它们全部完成,Promise.all 可以满足这个需求。在 Kotlin 协程中,我们可以使用 asyncawait 来实现类似的功能:

suspend fun fetchUserData(id: Int): String {
    delay(Random.nextLong(500, 1500))
    return "User $id data"
}

suspend fun fetchProductData(id: Int): String {
    delay(Random.nextLong(500, 1500))
    return "Product $id data"
}

fun main() = runBlocking {
    val time = kotlin.system.measureTimeMillis {
        val userDeferred = async { fetchUserData(1) }
        val productDeferred = async { fetchProductData(1) }

        val userResult = userDeferred.await()
        val productResult = productDeferred.await()

        println("User data: $userResult")
        println("Product data: $productResult")
    }
    println("耗时: $time ms")
}

在这个例子中,async 函数会启动一个新的协程,并返回一个 Deferred 对象,它类似于 Promise。我们可以使用 await 函数等待 Deferred 对象完成并获取结果。这里 async 启动的两个协程是并发执行的。

如果需要等待多个协程全部完成,可以使用 awaitAll 函数:

fun main() = runBlocking {
    val time = kotlin.system.measureTimeMillis {
        val deferreds = (1..3).map {
            async { fetchUserData(it) }
        }
        val results = deferreds.awaitAll()
        results.forEach { println(it) }
    }
    println("耗时: $time ms")
}

返回 Deferred 对象

有时候,我们可能需要返回一个 Deferred 对象,而不是直接返回结果,这类似于返回一个 Promise 对象。例如,我们可能需要在某个时刻取消异步操作,或者在多个地方等待同一个异步操作的结果:

fun fetchUserDataAsync(id: Int): Deferred<String> = CoroutineScope(Dispatchers.Default).async {
    delay(Random.nextLong(500, 1500))
    "User $id data"
}

fun main() = runBlocking {
    val deferred = fetchUserDataAsync(2)
    println("Doing something else...")
    val result = deferred.await()
    println("User data: $result")
}

在这个例子中,fetchUserDataAsync 函数返回一个 Deferred 对象,调用方可以在需要的时候使用 await 获取结果。

总结

Kotlin 协程为我们提供了一种优雅的方式来处理异步操作,它类似于 JavaScript 的 async/await,但更加强大和灵活。通过 suspendCancellableCoroutine,我们可以轻松地将传统的回调函数转换为协程,并使用 try-catch 处理异常。同时,我们还可以使用 asyncawait 实现并发执行,以及返回 Deferred 对象来更灵活地管理异步操作。如果你是前端开发者,正在学习 Kotlin 和安卓开发,那么协程绝对是你需要掌握的重要概念。