How to call Coroutines (Suspend Function and Flow) from Java?

Viewed 2883

I decided to write this question as there are no best practices yet available on this topic.

We are providing an Android SDK that implements asynchronous calls with Coroutines. I want our clients to consume suspending functions and Flow from Java or standard Kotlin.

I am aware that there is kotlinx-coroutines-jdk8 but this can only be used from Android Api level 24 and our SDK supports Android down to Api level 21. So this is not an option at the moment.

My idea was to bridge the worlds of Java (or standard Kotlin) and Coroutines by providing a simply callback API.

I would like to know if my following approach is a good solution. Are there any downsides or dangers? What alternative would I have to make clients call Coroutines functions without forcing them to use Coroutines themself?

Now let's get started. First I show you some interfaces and helper function I can use later to map suspend functions and Flow.

Cancellation

I need to ensure that Coroutine can be cancelled from Java. So I created a Cancelable interface.

interface Cancelable {
  fun cancel()
}

This interface gets implemented by CancelableJob that contains and hides the Job to be canceled.

class CancelableJob(private val job: Job) : Cancelable {
  override fun cancel() {
    job.cancel()
  }
}

Launch a new Coroutine

Every time a clients calls a function I will launch a new Coroutine. For this I create a top level function launchCancelableJob. This function gets a suspending block and returns a Cancelable. The Coroutine will be launched on Dispatchers.Main, so all results can be observed on UI Thread, together with a SupervisedJob.

fun launchCancelableJob(block: suspend () -> Unit): Cancelable {
  val job: Job = CoroutineScope(Dispatchers.Main + SupervisorJob()).launch {
    block.invoke()
  }

  return CancelableJob(job)
}

Bridge Suspend Function and Flow to World of Java

Now it's time to provide a bridge function that is not a suspend function itself, but launches a Coroutine and returns a Cancelable. From a given callback the result will be delivered to the caller.

// normal coroutine api
suspend fun generateQrCode(): QrCode

// bridge function - to be called from Java
fun generateQrCode(callback: (QrCode) -> Unit): Cancelable {
  return launchCancelableJob {
    val qrCode: QrCode = generateQrCode()
    callback(qrcode)
  }
}

The same I could do with Flow.

// normal coroutine api
fun generateQrCodes(): Flow<QrCode>

// bridge function - to be called from Java
fun generateQrCodes(callback: (QrCode) -> Unit): Cancelable {
  return launchCancelableJob {
    generateQrCodes().collect { qrCode: QrCode ->
      callback(qrcode)
    }
  }
}

Usage

The above function could be called from Java like:

Cancelable cancelable = generateQrCode(new Function1<QrCode, Unit>() {
  @Override
  public Unit invoke(QrCode qrCode) {
    // show the qrCode
    return Unit.INSTANCE;
  }
});

And if no longer needed it could be canceled like:

cancelable.cancel();

This is my approach. I'm really looking forward to your opinions or perhaps better solutions. Thank you for reading, I know this was a very long question.

0 Answers
Related