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
, orpromiseOnUi
. - The
success
andfail
blocks are not directly part of the promise chain, so any errors thrown inside won't be passed down to the nextfail
block. Block types which handlethrow
correctly:promiseOnUi
,task
,then
,bind
- Nested promises are not handled by default. If a
then
block returns another promise, usebind
instead or else the promise itself will be passed to the next block. - If you know Rx:
then
is like.map
, andbind
is like.flatMap
.
Here is a list of Kovenant snippets which I have found extremely useful.
Android specific
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
.
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.
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
task {} bind { performAction() }
Use bind
instead of then
if the block is returning a promise. bind
unwraps the promise in the following block.
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
task { throw Exception("Failed!") } recover { "OK" } success { print(it) } 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.
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
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
.
// 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
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.
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.
PromiseQueue.add
handles throw
correctly as well, so errors thrown inside the block will be passed on to the next fail
or recover
handler.
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
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.
task { 2 } then { it.toByte() }
You can use a then
block to convert the output of a promise.