Reputation: 4125
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?
Upvotes: 65
Views: 52943
Reputation: 10212
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/*")))
}
)
}
Upvotes: 1
Reputation: 710
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()
}
}
Upvotes: 1
Reputation: 11018
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
Upvotes: 21
Reputation: 2579
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())
}
Upvotes: 124
Reputation: 17747
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 )
Upvotes: 12
Reputation: 7602
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
}
Upvotes: 5
Reputation: 199795
The Activity Result has two API surfaces:
ActivityResultRegistry
. This is what actually does the underlying work.ActivityResultCaller
that ComponentActivity
and Fragment
implement that ties the Activity Result request to the lifecycle of the Activity or FragmentA 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())
}
Upvotes: 15