Blog Infos
Author
Published
Topics
,
Published

This is the second post of a two part series on how we can convert and consume Activity Results using coroutines.

In the first part, we were able to create a component that allowed us to convert the Activity Result’s contract RequestMultiplePermissions to a Coroutine, while still able to handle Activity Recreation and process-death scenarios.

When reviewing the code we had for our PermissionManager class, the only specific parts related to permissions were:

  1. The instantiation of RequestMultiplePermissions.
  2. The mapping of the result to our defined sealed class.
  3. Saving the permissions’ list to the saved state, and using it as the flag for deciding if there is any pending operation.

So for designing our component that can be used to consume all Activity Results, we just need to generalize or remove the above points.
The first two points are straight forward, as we are aiming for a generic component here, we’ll just remove them, so we’ll pass the instance of our ActivityResultContract to our function, and we’ll make it return the same type as defined by the contract. which means having a signature for our main function as this:

suspend fun <I, O, C : ActivityResultContract<I, O>> requestResult( contract: C, input: I ): O?

For the third point, we have many ways for handling it:

  1. Using a simple boolean to tell us if there is any pending operation. This would work for most cases, but it has one downside, if a process-death occurs, and in the ViewModel’s side we don’t handle it, our component will still think that there is a pending operation, which means if we request a different type of results, it’ll get stuck.
  2. By saving the contract’s class name, and comparing against it, this is a simple way for doing it, and would work for the majority of cases, although it may still break on a very specific case: if the ViewModel doesn’t handle process-death, and we request another result using the same type of Contract, but using a different input, we’ll probably return the wrong result.
  3. By saving both the contract’s class name and the input, and while this the ultimate solution, the comparison of the input against a saved value won’t be straight forward, since it’s a generic type, and we don’t know if it has a valid equals implementation or not.

I opted for the option two, since the failure scenario is a very far corner case, and when using the SavedStateHandle correctly to save state, it won’t occur at all, let me know what you think about this choice in the comments.

With all of this, we can have our component now:

class ActivityResultManager(private val activityProvider: ActivityProvider) {
private const val SAVED_STATE_REGISTRY_KEY = "permissions_saved_state"
private const val PENDING_RESULT_KEY = "pending"
private const val LAST_INCREMENT_KEY = "key_increment"
private val keyIncrement = AtomicInteger(0)
private var pendingResult: String? = null
suspend fun <I, O, C : ActivityResultContract<I, O>> requestResult(
contract: C,
input: I
): O? {
var (isLaunched, key) = activityProvider.currentActivity?.calculateKey(contract)
?: return null
pendingResult = contract.javaClass.simpleName
return activityProvider.activityFlow
.mapLatest { currentActivity ->
if (!isLaunched) {
currentActivity.prepareSavedData()
}
var launcher: ActivityResultLauncher<I>? = null
try {
suspendCancellableCoroutine<O> { continuation ->
launcher = currentActivity.activityResultRegistry.register(
key,
contract
) { result ->
pendingResult = null
currentActivity.clearSavedStateData()
continuation.resume(result)
}
if (!isLaunched) {
launcher!!.launch(input)
isLaunched = true
}
}
} finally {
launcher?.unregister()
}
}
.first()
}
private fun <C : ActivityResultContract<*, *>> ComponentActivity.calculateKey(contract: C): Pair<Boolean, String> {
fun generateKey(increment: Int) = "result_$increment"
val savedBundle = savedStateRegistry.consumeRestoredStateForKey(SAVED_STATE_REGISTRY_KEY)
return if (contract.javaClass.simpleName == savedBundle?.getString(PENDING_RESULT_KEY)) {
Pair(true, generateKey(savedBundle!!.getInt(LAST_INCREMENT_KEY)))
} else {
Pair(false, generateKey(keyIncrement.getAndIncrement()))
}
}
private fun ComponentActivity.prepareSavedData() {
savedStateRegistry.registerSavedStateProvider(
SAVED_STATE_REGISTRY_KEY
) {
bundleOf(
PENDING_RESULT_KEY to pendingResult,
LAST_INCREMENT_KEY to keyIncrement.get() - 1
)
}
}
private fun ComponentActivity.clearSavedStateData() {
savedStateRegistry.unregisterSavedStateProvider(
SAVED_STATE_REGISTRY_KEY
)
// Delete the data by consuming it
savedStateRegistry.consumeRestoredStateForKey(
SAVED_STATE_REGISTRY_KEY
)
}
}

Job Offers

Job Offers

There are currently no vacancies.

OUR VIDEO RECOMMENDATION

Jobs

After starting the article, I decided to publish this component as a library for anyone interested, it’s available in the same repo.
The Activity Result can be requested either using the low level API:

val uri = ActivityResultManager.getInstance().requestResult( contract = GetContent(), input = "image/*" )

or using the extensions for the built-in ActivityResultContracts:

val uri = ActivityResultManager.getInstance().getContent("image/*")

The example app has samples for using it for permissions and other activity results, and it has a sample for using it for a custom ActivityResultContract.

If you have any remarks or questions regarding the code or the API, please drop them in the comments section, I hope the library can be useful to some people.

Originally published at https://dev.to on December 7, 2021.

YOU MAY BE INTERESTED IN

YOU MAY BE INTERESTED IN

blog
It’s one of the common UX across apps to provide swipe to dismiss so…
READ MORE
blog
Hi, today I come to you with a quick tip on how to update…
READ MORE
blog
Automation is a key point of Software Testing once it make possible to reproduce…
READ MORE
blog
Drag and Drop reordering in Recyclerview can be achieved with ItemTouchHelper (checkout implementation reference).…
READ MORE

Leave a Reply

Your email address will not be published. Required fields are marked *

Fill out this field
Fill out this field
Please enter a valid email address.

Menu