52

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?

Vadim Kotov
  • 8,084
  • 8
  • 48
  • 62
foxtrotuniform6969
  • 3,527
  • 7
  • 28
  • 54

7 Answers7

94

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())
}
ameencarpenter
  • 2,059
  • 1
  • 11
  • 20
  • how we will use shouldShowRequestPermissionRationale ,it needs to have an Activity – joghm Jan 22 '23 at 12:24
  • Where does this code belong? I assume in an `@Composable` function, but does that function need to be a member of the activity subclass? – Code-Apprentice Jul 24 '23 at 01:03
15

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())
}
ianhanniballake
  • 191,609
  • 30
  • 470
  • 443
  • Do you have any plans to expose the registry as ambient, like `ActivityResultRegistryAmbient`? Is casting `ContextAmbient` to activity a malpractice? – Nikola Despotoski Nov 06 '20 at 23:24
  • You can star the [feature request issue](https://issuetracker.google.com/172690553) for making this part of Compose itself. IMO, an `ActivityResultRegistryAmbient` isn't very helpful since you wouldn't never want to use it outside of the managed scope of something like this `registerForActivityResult()`. Note that you aren't requiring any activity - just the generic `ActivityResultRegistryOwner`, but for practical purposes, `setContent` requires that you are are within a `ComponentActivity` anyways, so this cast always succeeds. – ianhanniballake Nov 06 '20 at 23:47
  • @ianhanniballake Straight from the horse's mouth! This works well. Thank you for the example! – foxtrotuniform6969 Nov 07 '20 at 19:10
  • @ianhanniballake So this doesn't appear to be working for `ActivityResultContracts.TakePicture()`. It appears as though the `realLauncher`'s result never arrives, and this the `currentOnResult` is never fired. Any idea why this might be? – foxtrotuniform6969 Nov 23 '20 at 17:29
  • @ianhanniballake Actually, this is the same for `OpenMultipleDocuments()` – foxtrotuniform6969 Nov 23 '20 at 18:28
  • All contracts are treated identically - the Activity Result API doesn't even know which contract you're using, so there's no reason why one contract would work and another wouldn't. Just keep in mind that many contracts can return null, so a `remember` that relies on a null changing to non-null won't actually change, despite `onResult` being triggered. – ianhanniballake Nov 23 '20 at 20:23
  • 2
    @ianhanniballake I don't know why, but this solution is proving to be extremely unreliable and unpredictable. It seems to `currentOnResult.value(it)` only seems to be call *sometimes*, and I have no idea why. Its extremely frustrating. – foxtrotuniform6969 Dec 03 '20 at 15:32
  • This does not work. I used it with 'ActivityResultContracts.GetContent()' and '.launch("image/*")' but the selected image is never handed over to onResult. Also, I don't understand why I should write this massive unreadable piece of code to just make sure that the selected image is returned to the caller. This seems overkill and should probably be handled by some system utility function. Thanks for the hint that 'AmbientContext.current' is always a 'ComponentActivity'. A solution that works for my current need is to call 'registerForActivityResult' on this activity. – Jeyhey Dec 05 '20 at 20:25
  • 1
    @Jeyhey - Make sure you're using Activity 1.2.0-beta02 (and with it, Fragment 1.3.0-beta02 to get the related fixes in `FragmentActivity` / `AppCompatActivity`). It sounds like you'd want to star [the feature request](https://issuetracker.google.com/issues/172690553) mentioned in the comments up above which to make this 'a system utility function'. – ianhanniballake Dec 05 '20 at 20:42
  • @ianhanniballake running in the same issue as @Jeyhey with `Activity 1.2.0-beta02` – Róbert Nagy Dec 09 '20 at 09:27
  • I have this third party library which does not expose the intent. I still want to get result in compose for that. This is an important payment SDK. Here is how it starts the payment activity sdk.dopayment() How should this be handled? – nayan dhabarde Apr 14 '22 at 01:38
  • 1
    @nayandhabarde - it sounds like you should put in a feature request to that payment SDK - *any* SDK can provide an `ActivityResultContract` that works equally well in an Activity, Fragment, Composable, or anywhere else. – ianhanniballake Apr 14 '22 at 01:56
12

As of Activity Compose 1.3.0-alpha03 and beyond, there is a new utility function registerForActivityResult() that simplifies this process.

@Composable
fun RegisterForActivityResult() {
    val result = remember { mutableStateOf<Bitmap?>(null) }
    val launcher = registerForActivityResult(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())
    }
}

(From the sample given here )

HRJ
  • 17,079
  • 11
  • 56
  • 80
12

Adding in case if someone is starting a new external intent. In My case, I wanted to launch a google sign-in prompt on click on the button in jetpack compose.

declare your intent launch

val startForResult =
    rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { result: ActivityResult ->
        if (result.resultCode == Activity.RESULT_OK) {
            val intent = result.data
            //do something here
        }
    }

launch your new activity or any intent.

 Button(
        onClick = {
            //important step
            startForResult.launch(googleSignInClient?.signInIntent)
        },
        modifier = Modifier
            .fillMaxWidth()
            .padding(start = 16.dp, end = 16.dp),
        shape = RoundedCornerShape(6.dp),
        colors = ButtonDefaults.buttonColors(
            backgroundColor = Color.Black,
            contentColor = Color.White
        )
    ) {
        Image(
            painter = painterResource(id = R.drawable.ic_logo_google),
            contentDescription = ""
        )
        Text(text = "Sign in with Google", modifier = Modifier.padding(6.dp))
    }

#googlesignin

vikas kumar
  • 10,447
  • 2
  • 46
  • 52
5

For those who are not getting back a result with the gist provided by @ianhanniballake in my case the returnedLauncher actually captures an already disposed value of the realLauncher.

So while removing the layer of indirection should fix the issue, it's definitely not the optimal way of doing this.

Here's the updated version, until a better solution is found:

@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 = AmbientContext.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() }

    // TODO a working layer of indirection would be great
    val realLauncher = remember<ActivityResultLauncher<I>> {
        activityResultRegistry.register(key, contract) {
            currentOnResult.value(it)
        }
    }

    onDispose {
        realLauncher.unregister()
    }
    
    return realLauncher
}
Róbert Nagy
  • 6,720
  • 26
  • 45
0

The call to the method that requests the permission to the user (e.g. PermissionState.launchPermissionRequest()) needs to be invoked from a non-composable scope.

val scope = rememberCoroutineScope()
if (!permissionState.status.isGranted) {
    scope.launch {
         permissionState.launchPermissionRequest()
    }
}
reznic
  • 672
  • 6
  • 9
0

here is how to launch and choose an Image in Compose:

@Composable
  fun ChangeProfilePictureScreen(viewModel: ChangeProfilePictureViewModel = viewModel()) {
  val pickMedia = rememberLauncherForActivityResult(ActivityResultContracts.PickVisualMedia()) { uri: Uri? ->
        if (uri != null) {
            Log.d("PhotoPicker", "Selected URI: $uri")
        } else {
            Log.d("PhotoPicker", "No media selected")
        }
    }

  Button(
    text = "Select image",
    onClick = {
        pickMedia.launch(PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.SingleMimeType(mimeType = "image/*")))
    }
  )

}
Dan Alboteanu
  • 9,404
  • 1
  • 52
  • 40