Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Jetpack Compose: Launch ActivityResultContract request from Composable function

As of 1.2.0-beta01 of androidx.activity:activity-ktx, one can no longer launch the request created using Activity.registerForActivityResult(), as highlighted in the above link under "Behavior Changes" and seen in the Google issue here.

How should an application launch this request via a @Composable function now? Previously, an app could pass the instance of the MainActivity down the chain via using an Ambient and then launch the request easily.

The new behavior can be worked around by, for example, passing a class registering for the activity result down the chain after being instantiated outside of the Activity's onCreate function, and then launch the request in a Composable. However, registering the a callback to be executed after completion cannot be done this way.

One could get around this by creating custom ActivityResultContract, which, at launch, take a callback. However, this would mean that virtually none of the built-in ActivityResultContracts could be used with Jetpack Compose.

TL;DR

How would an app launch an ActivityResultsContract request from a @Composable function?

like image 802
foxtrotuniform6969 Avatar asked Nov 06 '20 20:11

foxtrotuniform6969


People also ask

How do I use Webview in Jetpack Compose?

Step by Step ImplementationWhile choosing the template, select Empty Compose Activity. If you do not find this template, try upgrading the Android Studio to the latest version. We demonstrated the application in Kotlin, so make sure you select Kotlin as the primary language while creating a New Project.

What is composable in Jetpack Compose?

Jetpack Compose is built around composable functions. These functions let you define your app's UI programmatically by describing how it should look and providing data dependencies, rather than focusing on the process of the UI's construction (initializing an element, attaching it to a parent, etc.).

What is launch effect in Jetpack Compose?

LaunchedEffect: run suspend functions in the scope of a composable. rememberCoroutineScope: obtain a composition-aware scope to launch a coroutine outside a composable. rememberUpdatedState: reference a value in an effect that shouldn't restart if the value changes. DisposableEffect: effects that require cleanup.


2 Answers

As of androidx.activity:activity-compose:1.3.0-alpha06, the registerForActivityResult() API has been renamed to rememberLauncherForActivityResult() to better indicate the returned ActivityResultLauncher is a managed object that is remembered on your behalf.

val result = remember { mutableStateOf<Bitmap?>(null) }
val launcher = rememberLauncherForActivityResult(ActivityResultContracts.TakePicturePreview()) {
    result.value = it
}

Button(onClick = { launcher.launch() }) {
    Text(text = "Take a picture")
}

result.value?.let { image ->
    Image(image.asImageBitmap(), null, modifier = Modifier.fillMaxWidth())
}
like image 88
ameencarpenter Avatar answered Sep 18 '22 21:09

ameencarpenter


The Activity Result has two API surfaces:

  • The core ActivityResultRegistry. This is what actually does the underlying work.
  • A convenience interface in ActivityResultCaller that ComponentActivity and Fragment implement that ties the Activity Result request to the lifecycle of the Activity or Fragment

A Composable has a different lifetime than the Activity or Fragment (e.g., if you remove the Composable from your hierarchy, it should clean up after itself) and thus using the ActivityResultCaller APIs such as registerForActivityResult() is never the right thing to do.

Instead, you should be using the ActivityResultRegistry APIs directly, calling register() and unregister() directly. This is best paired with the rememberUpdatedState() and DisposableEffect to create a version of registerForActivityResult that works with a Composable:

@Composable
fun <I, O> registerForActivityResult(
    contract: ActivityResultContract<I, O>,
    onResult: (O) -> Unit
) : ActivityResultLauncher<I> {
    // First, find the ActivityResultRegistry by casting the Context
    // (which is actually a ComponentActivity) to ActivityResultRegistryOwner
    val owner = ContextAmbient.current as ActivityResultRegistryOwner
    val activityResultRegistry = owner.activityResultRegistry

    // Keep track of the current onResult listener
    val currentOnResult = rememberUpdatedState(onResult)

    // It doesn't really matter what the key is, just that it is unique
    // and consistent across configuration changes
    val key = rememberSavedInstanceState { UUID.randomUUID().toString() }

    // Since we don't have a reference to the real ActivityResultLauncher
    // until we register(), we build a layer of indirection so we can
    // immediately return an ActivityResultLauncher
    // (this is the same approach that Fragment.registerForActivityResult uses)
    val realLauncher = mutableStateOf<ActivityResultLauncher<I>?>(null)
    val returnedLauncher = remember {
        object : ActivityResultLauncher<I>() {
            override fun launch(input: I, options: ActivityOptionsCompat?) {
                realLauncher.value?.launch(input, options)
            }

            override fun unregister() {
                realLauncher.value?.unregister()
            }

            override fun getContract() = contract
        }
    }

    // DisposableEffect ensures that we only register once
    // and that we unregister when the composable is disposed
    DisposableEffect(activityResultRegistry, key, contract) {
        realLauncher.value = activityResultRegistry.register(key, contract) {
            currentOnResult.value(it)
        }
        onDispose {
            realLauncher.value?.unregister()
        }
    }
    return returnedLauncher
}

Then it is possible to use this in your own Composable via code such as:

val result = remember { mutableStateOf<Bitmap?>(null) }
val launcher = registerForActivityResult(ActivityResultContracts.TakePicturePreview()) {
    // Here we just update the state, but you could imagine
    // pre-processing the result, or updating a MutableSharedFlow that
    // your composable collects
    result.value = it
}

// Now your onClick listener can call launch()
Button(onClick = { launcher.launch() } ) {
    Text(text = "Take a picture")
}

// And you can use the result once it becomes available
result.value?.let { image ->
    Image(image.asImageAsset(),
        modifier = Modifier.fillMaxWidth())
}
like image 32
ianhanniballake Avatar answered Sep 19 '22 21:09

ianhanniballake