1. 简介

Promises(承诺) 是一种管理异步代码的优秀方式,适用于我们期望获得结果但愿意等待其可用的场景。

在本教程中,我们将了解 Kovenant 如何为 Kotlin 引入 Promises 的概念,帮助我们更优雅地处理异步逻辑。


2. 什么是 Promise

最基础的理解是,Promise 是一个尚未发生的操作结果的占位符。例如,某段代码可能返回一个 Promise,代表一个复杂的计算过程,或是一个网络资源的获取操作。这段代码承诺(Promise)了结果最终会可用,但可能不是立刻就能获取。

在很多方面,Promise 与 Java 中的 Futures 类似。但正如我们将看到的,Promise 更加灵活和强大,它支持失败处理、链式调用以及其他组合方式。


3. Maven 依赖

Kovenant 是一个标准的 Kotlin 库,同时也提供了一些适配模块,便于与其他库协同工作。

在使用 Kovenant 之前,我们需要添加对应的依赖。Kovenant 提供了一个 pom 类型的依赖,可以一次性引入所有核心组件:

<dependency>
    <groupId>nl.komponents.kovenant</groupId>
    <artifactId>kovenant</artifactId>
    <type>pom</type>
    <version>3.3.0</version>
</dependency>

此外,Kovenant 还为其他平台和库提供了额外的模块,比如 RxKotlin 或 Android 平台。完整组件列表可参考 Kovenant 官网


4. 创建 Promise

使用 Kovenant 的第一步是创建一个 Promise。创建方式有多种,但它们的最终目标是一致的:返回一个代表未来结果的 Promise。

4.1. 手动创建 Deferred 操作

我们可以使用 deferred<V, E> 方法手动创建一个延迟操作。该方法返回一个 Deferred<V, E> 类型的对象,其中 V 表示成功时的值类型,E 表示错误类型:

val def = deferred<Long, Exception>()

创建后,我们可以根据执行结果选择 resolve 或 reject:

try {
    def.resolve(someOperation())
} catch (e: Exception) {
    def.reject(e)
}

⚠️ 注意:每个 Deferred 只能被 resolve 或 reject 一次,重复调用会抛出异常。

4.2. 从 Deferred 中提取 Promise

我们可以从 Deferred 中提取出一个 Promise<V, E>

val promise = def.promise

这个 Promise 就是 Deferred 操作的结果,只有在 resolve 或 reject 后才会有值:

val def = deferred<Long, Exception>()
try {
    def.resolve(someOperation())
} catch (e: Exception) {
    def.reject(e)
}
return def.promise

Deferred.promise 可以多次调用,但返回的是同一个 Promise 实例。

4.3. 使用 task 执行异步任务

大多数情况下,我们希望直接执行一个耗时任务并返回 Promise。Kovenant 提供了 task<V> 方法来实现这一点:

val result = task {
    updateDatabase()
}

Kotlin 会根据返回值类型自动推断泛型类型,因此无需手动指定。

4.4. 使用 Lazy Promise 委托

我们还可以使用 Promise 替代 Kotlin 的 lazy() 委托。与 lazy() 不同的是,这种属性的类型是 Promise<V, Exception>

val webpage: Promise<String, Exception> by lazyPromise { getWebPage("http://www.example.com") }

这类 Promise 会在后台线程中执行,并在适当时机返回结果。


5. 响应 Promise

当我们拿到一个 Promise 后,就需要对它进行响应。通常我们希望以响应式或事件驱动的方式进行处理。

5.1. Promise 回调函数

我们可以通过 successfailalways 注册回调:

task {
    fetchData("http://www.example.com")
} success { response ->
    println(response)
} fail { error ->
    println(error)
} always {
    println("Finished fetching data")
}

这些回调可以按顺序链式调用,也可以多次注册。所有成功回调会按注册顺序依次执行:

task {
    fetchData("http://www.example.com")
} success { response ->
    logResponse(response)
} success { response ->
    renderData(response)
} success { response ->
    updateStatusBar(response)
}

✅ 注意:成功或失败回调的执行顺序是确定的。

5.2. 链式调用 Promise

我们可以将多个 Promise 链式调用,形成异步流程:

task {
    fetchData("http://www.example.com")
} then { response -> 
    response.data
} then { responseBody ->
    sendData("http://archive.example.com/savePage", responseBody)
}

如果链中任意一步失败,整个链会立即失败,跳转到 fail 回调:

task {
    fetchData("http://bad.url") // 失败
} then { response -> 
    response.data // 被跳过
} then { body -> 
    sendData("http://good.url", body) // 被跳过
} fail { error ->
    println(error) // 被调用
}

✅ 这种机制类似于 try/catch,但支持多个错误处理器。

5.3. 同步获取 Promise 结果

有时我们需要同步获取 Promise 的结果:

val promise = task { getWebPage() }

try {
    println(promise.get()) // 阻塞直到 Promise 完成
} catch (e: Exception) {
    println("Failed to get the web page")
}

⚠️ 注意:如果 Promise 永远不完成,get() 会一直阻塞。可以使用 isDoneisSuccessisFailure 来检查状态:

val promise = doSomething()
println("Promise is done? ${promise.isDone()}")
println("Promise is successful? ${promise.isSuccess()}")
println("Promise failed? ${promise.isFailure()}")

5.4. 带超时的阻塞

目前 Kovenant 不支持带超时的阻塞等待,但我们可以自己实现一个带超时的 task:

fun <T> timedTask(millis: Long, body: () -> T): Promise<T?, List<Exception>> {
    val timeoutTask = task {
        Thread.sleep(millis)
        null
    }
    val activeTask = task(body = body)
    return any(activeTask, timeoutTask)
}

调用示例:

timedTask(5000) {
    getWebpage("http://slowsite.com")
}

6. 取消 Promise

Promise 通常代表异步任务。有时我们不再需要结果,可以取消它以释放资源。

使用 taskthen 创建的 Promise 默认是可取消的,但需要强制转换为 CancelablePromise

val promise = task { downloadLargeFile() }
(promise as CancelablePromise).cancel(UserGotBoredException())

使用 deferred 创建的 Promise 默认不可取消,除非指定取消回调:

val deferred = deferred<Long, String> { e ->
    println("Deferred was cancelled by $e")
}
deferred.promise.cancel(UserGotBoredException())

⚠️ cancel 是一个尽力而为的操作。它会尝试中断线程,抛出 InterruptedException,并触发 fail 回调。


7. 组合多个 Promise

有时我们需要等待多个异步任务完成,或只关心第一个完成的结果。Kovenant 提供了多种组合方式。

7.1. 等待所有 Promise 成功

使用 all 可以等待所有 Promise 完成:

all(
    task { getWebsite("http://www.example.com/page/1") },
    task { getWebsite("http://www.example.com/page/2") },
    task { getWebsite("http://www.example.com/page/3") }
) success { websites: List<String> ->
    println(websites)
} fail { error: Exception ->
    println("Failed to get website: $error")
}

✅ 所有 Promise 必须是相同类型,组合后的结果是 List<V>。如果任意一个失败,整个 all 会失败。

7.2. 等待第一个 Promise 成功

使用 any 只关心第一个完成的结果:

any(
    task { getWebsite("http://www.example.com/page/1") },
    task { getWebsite("http://www.example.com/page/2") },
    task { getWebsite("http://www.example.com/page/3") }
) success { result ->
    println("First web page loaded: $result")
} fail { errors ->
    println("All web pages failed to load: $errors")
}

✅ 任意一个成功即触发 success;全部失败才触发 fail。成功后,Kovenant 会尝试取消其余未完成的 Promise。

7.3. 组合不同类型 Promise

如果 Promise 的类型不同,可以使用 combine

combine(
    task { getMessages(userId) },
    task { getUnreadCount(userId) },
    task { getFriends(userId) }
) success { 
    messages: List<Message>, 
    unreadCount: Int, 
    friends: List<User> ->
    println("Messages in inbox: $messages")
    println("Number of unread messages: $unreadCount")
    println("List of users friends: $friends")
}

✅ 所有 Promise 必须具有相同的失败类型。

Kovenant 还提供了 and 方法用于组合两个 Promise:

val promise = 
  task { computePi() } and 
  task { getWebsite("http://www.example.com") }

promise.success { pi, website ->
    println("Pi is: $pi") 
    println("The website was: $website") 
}

8. 测试 Promise

Kovenant 是异步库,默认在后台线程运行任务。这在测试时会带来不确定性。

例如,以下测试可能失败:

@Test
fun testLoadUser() {
    val user = userService.loadUserDetails("user-123")
    Assert.assertEquals("Test User", user.syncName)
    Assert.assertEquals(5, user.asyncMessageCount) // 可能尚未完成
}

解决办法是启用 Kovenant 的测试模式,使所有操作同步执行:

@Before 
fun setupKovenant() {
    Kovenant.testMode { error ->
        Assert.fail(error.message)
    }
}

✅ 该设置是全局的,会影响所有测试。使用时需谨慎。


9. 总结

在本文中,我们介绍了 Kovenant 的基本使用方法,包括如何创建 Promise、注册回调、链式调用、取消和组合 Promise,以及在测试中如何处理异步逻辑。

Kovenant 是一个功能强大且灵活的库,适用于构建复杂的异步逻辑。如需了解更多示例,欢迎访问 GitHub 示例仓库


原始标题:Introduction to Kovenant Library for Kotlin

« 上一篇: Fuel HTTP 库与 Kotlin