Streamlining intents with Kotlin coroutines

Launching activities on Android is complicated. What makes it more complicated is the fact that they are asynchronous by nature, with results being handled elsewhere, namely in the onActivityResult() method.

Here is an example (in Kotlin) of how one can launch an activity that picks an image file, and handle the result:

companion object {
const val RC_GET_IMAGE = 0
}
fun getImage() {
val intent = Intent(Intent.ACTION_GET_CONTENT).apply {
type = "image/*"
}

if (intent.resolveActivity(packageManager) == null) {
return
}
startActivityForResult(intent, RC_GET_IMAGE)
}
override fun onActivityResult(requestCode: Int,
resultCode: Int, data: Intent?) {
if (requestCode == RC_GET_IMAGE
&& resultCode == Activity.RESULT_OK) {
data?.let {
val bitmap = MediaStore.Images.Media.getBitmap(
this.getContentResolver(), it.data);
myImageView.setImageBitmap(bitmap)
}
} else {
super.onActivityResult(requestCode, resultCode, data)
}
}

There is a lot going on here:

  1. First of all, we need to define a special constant RC_GET_IMAGE, only for the purpose of determining, in onActivityResult, what request was originally launched.
  2. Second, when launching the ACTION_GET_CONTENT activity in getImage(), we need to check if the activity is actually available using resolveActivity()
  3. Finally, we need to override a separate method, onActivityResult(), check if the requestCode matches our special RC_GET_IMAGE constant and only the process the result available in data.

When launching multiple activities, this gets even harder to manage, because the code that handles the activity results gets further separated from the code that launches it.

How can we make this better? Given that we are performing an asynchronous operation, Kotlin coroutines are a great way of making this easier. The key for streamlining the process of launching activities is to package the asynchronous mechanism into a separate Activity base class that takes care of the heavy lifting.

Introducing AsyncActivity

The following class takes care of the mechanics of launching activities, tracking requestCodes and firing deferred completions:

First, we introduce a helper class, ActivityResult, that contains the data returned from the activity (notice how we don’t include requestCode — we no longer need it!)

Second, we define the the AsyncActivity class and add the resultByCode map that stores activities we launch. It contains CompletableDeferred<ActivityResult> instances — a key for doing continuations when the activity returns.

Third, we override onActivityResult and check if the requestCode was in our map. If it was, we trigger the asynchronous completion and remove the activity from our collection (it’s no longer needed).

Finally, we introduce a launchIntent() method that checks if the activity can be resolved, and returns a Deferred<ActivityResult>. If we cannot resolve the activity, we trigger the completion immediately, with a null payload.

Now we can use this to simplify the activity invocation!

Note: in order to use it, you you will need to extend this class. If your activity does not extend AppCompatActivity(), you’ll need to create a copy of AsyncActivity.

Additionally, you would need to add kotlinx-coroutines-core and kotlinx-coroutines-android to your app’s gradle file. At the time of the writing, they were at version 1.0:

implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.0.0'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.0.0'

Launching activity with coroutines

Now that your code is using AsyncActivity, launching an activity becomes much simpler:

val intent = Intent(Intent.ACTION_GET_CONTENT).apply {
type = "image/*"
}

GlobalScope.launch(Dispatchers.Main) {
val
result = launchIntent(intent).await()
result?.data?.let {
val
bitmap = MediaStore.Images.Media.getBitmap(getContentResolver(), it.data);
myImageView.setImageBitmap(bitmap)
}
}

Note the usage of ?let that allows, in one line, to deal with both the resolveActivity() failure and no data being returned in the intent.

It is worth noting that this approach does not deal with Android lifecycle events — if the process gets terminated while the other activity is launched, the continuation will obviously not get triggered. Handling configuration changes might help prevent your activity from getting killed in some cases to address this issue.

Write code for fun and living

Love podcasts or audiobooks? Learn on the go with our new app.