Kovenant cheat sheet

Promises are awesome. They remove a lot of the complexity of dealing with asynchronous code, and ever since I started using them in Javascript I’ve never gone back to callbacks. When it comes to Android development I’ve done a bit of experimenting with RxKotlin, but it was a bit overcomplicated for my needs. Recently I’ve been using the Kovenant library for Kotlin which adds pretty good Promise support with a nice and simple API.

Some interesting points about the way Kovenant works:

  • All operations are done in background threads, unless you use successUi, failUi, or promiseOnUi.
  • The success and fail blocks are not directly part of the promise chain, so any errors thrown inside won’t be passed down to the next fail block. Block types which handle throw correctly: promiseOnUi, task, then, bind
  • Nested promises are not handled by default. If a then block returns another promise, use bind instead or else the promise itself will be passed to the next block.
  • If you know Rx: then is like .map, and bind is like .flatMap.

Here is a list of Kovenant snippets which I have found extremely useful.

Android specific

1
2
3
4
dependencies {
implementation 'nl.komponents.kovenant:kovenant:3.3.0'
implementation 'nl.komponents.kovenant:kovenant-android:3.3.0'
}

Add both the Kovenant core library and the Android extensions to your build.gradle.

1
2
3
4
5
6
7
8
9
10
11
12
13
class App : Application() {

override fun onCreate() {
super.onCreate()
startKovenant()
}

override fun onTerminate() {
super.onTerminate()
stopKovenant()
}

}

Start and stop Kovenant along with your application. This can be done easily in a custom Application class.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
promiseOnUi {

// Update UI to show in progress

} bind {

// Start the operation
performOperation()

} successUi {

// Operation complete, show user success

} failUi {

// Operation failed, show the user the error
val errorText = it.localizedDescription

}

When performing actions based on the user’s intent (button press etc), it’s good to show them the progress. Kovenant makes it really easy.

Nested promises

1
2
3
task {} bind {
performAction()
}

Use bind instead of then if the block is returning a promise. bind unwraps the promise in the following block.

1
2
3
promiseOnUi {
performAction()
} bind { it }

If you have a nested promise and can’t use bind like above, you can unwrap it by calling bind afterwards.

Error handling

1
2
3
4
5
6
7
task {
throw Exception("Failed!")
} recover {
"OK"
} success {
print(it)
}
1
2
3
4
5
task {
performMainAction()
} recoverBind {
performFallbackAction()
}

Kovenant doesn’t come with a block for recovering from errors, but it’s easy to extend it to include one. Get Kovenant+Recover.kt

1
2
3
4
5
6
7
8
9
10
11
myOperation() recover {

// Convert known errors
if (it.description == "error_user_not_found")
throw Exception("We could not find that user.")

// Pass unknown errors through as-is OR specify a generic error
throw it
throw Exception("There was a problem.")

}

The recover block from above can be used to transform error messages as well, since it handles throw correctly.

Wrapping non-promise based functions

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
fun wrappedFunction() : Promise<String, Exception> {

// Create a pending promise
val pending = deferred<String, Exception>()

// Call it
myCallbackFunction(success = {
pending.resolve(it)
}, fail = {
pending.reject(it)
})

// Return promise
return pending.promise

}

You can easily wrap non-Promise functions by using Deferred.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// Stored pending promise
var pending : Deferred<String, Exception>? = null

// Start function
fun wrappedOperation() : Promise<String, Exception> {

// Create a pending promise
pending = deferred()

// Begin operation
beginOperation()

// Return promise
return pending!!.promise

}

// We have been informed the operation completed
fun onOperationSuccess(value : String) {
pending?.resolve(value)
pending = null
}

// We have been informed the operation failed
fun onOperationFailed() {
pending?.reject(Exception("Operation failed!"))
pending = null
}

Wrapping operations that use delegate callbacks are easy too, just store the Deferred promise until you can resolve or reject it later.

Synchronization and queueing

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
task {} bind {

// Search for devices
Bluetooth.scan()

} bind {

// Pick a device
val device = it.first ?: throw Exception("No devices found.")

// Pair the device
Bluetooth.pair(device)

} success {

// Complete

} fail {

// Failed
print("Failed to scan and pair: ${it.localizedDescription}")

}

You can easily chain multiple promises. When doing it this way, you can ignore all errors and only have one fail block right at the end for catching any error in the entire chain.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
val queue = PromiseQueue()

queue.add {

task {
"One!"
} success {
print(it)
}

}

queue.add {

performLongTask() then {
"Two!"
} success {
print(it)
}

}

queue.add {
print("Three!")
}

// Outputs: One! Two! Three!

It’s possible to create a “queue” of promises which ensures that operations always are executed in order, and that the next promise only starts when the previous one is entirely completed. Get PromiseQueue.kt

PromiseQueue.add handles throw correctly as well, so errors thrown inside the block will be passed on to the next fail or recover handler.

1
2
3
4
5
6
7
8
9
internal val queue = PromiseQueue()

fun read() = queue.add {
return readWithPromise()
}

fun write(value : String) = queue.add {
return writeWithPromise(value)
}

Using the PromiseQueue class above, it’s possible to create “synchronized” functions, which ensure they don’t run at the same time.

This was very useful for me when creating a Bluetooth library, since GATT characteristic read/write operations cannot be done simultaneously. This allows users of the library to call write() as many times as they want, without having to worry about previous writes still in progress.

Result value

1
2
3
4
5
6
7
8
9
10
11
fun doIt() = task {} bind {

// Start operation
longOperation()

} then {

// Discard the result
Unit

}

If you don’t want to return a value from a promise, use Unit as the value type. You can also return Unit from your then block.

Unit is Kotlin’s equivalent of void in Java.

1
2
3
task {
2
} then { it.toByte() }

You can use a then block to convert the output of a promise.

  • Copyrights © 2015-2021 jjv360

请我喝杯咖啡吧~

支付宝
微信