DEV Community

Cover image for Consuming Activity Results using coroutines: Part 1
Hicham Boushaba
Hicham Boushaba

Posted on • Edited on

Consuming Activity Results using coroutines: Part 1

Handling results from other Activies was always a tedious task: you need to override callbacks in the Activity/Fragment, and you need to manage the different request codes, and when you get a result, you need to parse it and this was never easy*.

* will the returned Uri be in intent.data or passed in the extras (looking at you RingtoneManager.EXTRA_RINGTONE_PICKED_URI)? what's the key of the returned data inside the extras and what's its type?...

To solve the above issues, the Android team introduced the androidx's Activity Result API, it offers a nicer API, and it's easily extendable by writing custom Contracts, but it still requires doing most of the work on the UI layer, which means lots of going back and forth between the View and the ViewModel for simple tasks.

This post is the first of two posts where we'll explore how we can optimize the API, by converting it to suspendable functions, and allowing it to be consumed by the ViewModel instead of the UI layer.
On this first part, we'll use permissions as an example, for this We'll start by showing an example on how the Activity Result API can be used to handle them, and then we'll try to write the new suspendable API together, and see how it can improve the readability of our code.
The code of the example can be found in this repo.

Part 2 is available here.

Activity Result API in play

Using the Activity Result API is quite simple, you just need to register an ActivityResultContract with your Activity/Fragment, while offering a callback that will be called when the result is delivered:


The registration will return an ActivityResultLauncher which you will use whenever you need to request the result

But while it's simple now, it becomes a bit complicated when we try to hook everything with the ViewModel, as it would require multiple two-way communication steps for something simple as the permission:


This is all while omitting some necessary parts, especially dealing with configuration changes and process-death.
For a full example, check the following sample fragment.

I feel like with this code, the View is doing so much, and we can't easily reuse this code if we need to request the same permission in multiple parts of the app.

Converting the Activity Result to suspendable functions: Permissions as an example

On this part, we'll try to move the logic of Activity Result API to another layer, to allow it to be consumed by the ViewModel.
Our goal is to have something similar to this by the end:

Let's get started, since the Activity Result API needs a reference to the Activity or the Fragment, the first thing we'll need, is a reference to the current Activity, we can use the function registerActivityLifecycleCallbacks to listen to Activity lifecycle changes, and keep a track of the latest one, a short example on how we can achieve this:


This class needs to be a singleton, and initiated in the Application's onCreate.
With this in hand, we can now "naively" write something like this easily:

Before testing our code, let's take a moment to check what it does:
  • We use a suspendCancellableCoroutine to convert the Activity result to a suspendable function.
  • We can't register our Contract directly using the function registerForActivityResult, as it requires to be called before onCreate, instead we use the activityResultRegistry directly, and here we just need to handle the unregistering ourself, so we use the finally block to make sure we are unregistering in either a cancellation or completion.

Now, let's test the code:


When testing this, we'll see that it works well, until you rotate your phone, or a process-death occurs, when we'll leak the activity, and also we won't be able to recover to continue handling the result.

Fix the issue for screen rotation

This happens because we are capturing the current Activity in a variable, this variable stays available for the whole function's lifetime, which is longer than the Activity's lifecycle. To solve this, we need a way to listen to the Activity changes, to be able to cancel the previous block and unregister the Contract, then re-register with the new Activity, so we need to change our ActivityProvider's implementation to offer a Flow instead of a single variable, which can bee easily done using MutableStateFlow, for full code check this.
With those changes, we can now use Flow's mapLatest operator to handle registering our Contract, and unregistering it when the block is cancelled, since it would occur everytime the Activity wach changed, which would give something like this:


Let's examine what the code does:
  • We capture the value of the key outside of the Flow, to make sure we keep using the same value for other Activities.
  • We have a variable isLaunched to let us know that the ActivityResultLauncher has been already launched, to avoid re-submitting the Intent or permission request.
  • the suspendable funtion passed to mapLatest will be cancelled each time the upstream Flow emits a new value, so it does exactly what we need, it suspends waiting for the Activity Result, but if the Activity was changed before, it cancels the current block, and starts a new one with the new Activity.

When testing the new code, it works pretty well on screen rotations, we can continue the Flow from where we left, great.
Except that it has an issue, if a process-death occurs, or the Activity was destroyed to release memory (or when "Don't keep activities" option is enabled), we can't recover even if our ViewModel uses SavedStateHandle to restore its state, since the viewModelScope will be cancelled, and our function will lose its state, which takes us to the last part of this article, how we can use the Activity's SavedStateRegistry to save our state, and make sure that even when we make a second call to requestPermissions it will continue from where it left.

Saving the state

ComponentActivity from androidx has a component called SavedStateRegistry that allows plugging in custom components to save additional data and retrieve it later, and this is exactly what we need here, since we can't use the Activity's callbacks, we'll use it to keep track of the last key we used to register our Contract, to be able to intercept the Activity Result even after process-death:


The code seems complicated, but it differs from the previous step in only the fact that it tries to restore the last used key from the SavedStateRegistry, and also it registers our SavedStateProvider to save the state when needed.
If we test the code now, using a ViewModel that uses SavedStateHandle for saving/restoring its state correctly, we'll see that we continue to receive the Activity Result correctly even after a process-death.

What we can do with this component now

Being able to use a suspendable function like this here, makes it way easier to extract our logic into a reusable components, for example if your app requests the permission from multiple areas, and uses the same logic for showing the rationale or handling a pemanent denial, you'll be able to create a component or a usecase that handles this for you, without having to repeat it on each Fragment/Activity.
For example a class like this:


This component is self contained, and allows to handle the whole permission request Flow, and can be reused on every screen easily.
A full example can be found here.

Conclusion

To sum up, the callback aspect of the new Result Activity API allows it to be converted to suspendable functions relatively easy, instead of needing an intermediate Fragment or Activity for handling the callbacks (as many libraries do), and this conversion made it easier to extract pieces of the logic to their own component. But the code is not tested against all scenarios, especially when the app has multiple activities, so we'll try to check this in the next part, and we'll try to generalize what we did with permissions so far for all the Activity Result Contracts.
Please let me know in the comments if you have any remarks about this solution, thanks for your time.

Top comments (0)