Jetpack Compose: inicie la solicitud ActivityResultContract desde la función Composable

7 minutos de lectura

avatar de usuario de foxtrotuniform6969
foxtrotuniform6969

A partir de 1.2.0-beta01 de androidx.activity:activity-ktxuno ya no puede launch la solicitud creada usando Activity.registerForActivityResult()como se destaca en el enlace anterior en “Cambios de comportamiento” y se ve en el Problema de Google aquí.

¿Cómo debería una aplicación lanzar esta solicitud a través de un @Composable función ahora? Anteriormente, una aplicación podía pasar la instancia del MainActivity abajo de la cadena mediante el uso de un Ambient y luego inicie la solicitud fácilmente.

El nuevo comportamiento se puede solucionar, por ejemplo, pasando una clase que se registra para el resultado de la actividad a lo largo de la cadena después de ser instanciada fuera de la Actividad. onCreate y luego inicie la solicitud en un Composable. Sin embargo, el registro de una devolución de llamada que se ejecutará después de la finalización no se puede hacer de esta manera.

Uno podría solucionar esto creando ActivityResultContract, que, en el lanzamiento, recibe una devolución de llamada. Sin embargo, esto significaría que prácticamente ninguna de las funciones integradas ActivityResultContracts podría usarse con Jetpack Compose.

TL;RD

¿Cómo lanzaría una aplicación un ActivityResultsContract solicitud de un @Composable ¿función?

avatar de usuario de ameencarpenter
ameencarpenter

A partir de androidx.activity:activity-compose:1.3.0-alpha06el registerForActivityResult() La API ha sido renombrada a rememberLauncherForActivityResult() para indicar mejor lo devuelto ActivityResultLauncher es un objeto administrado que se recuerda en su nombre.

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())
}

  • esta debería ser ahora la respuesta aceptada ya que RememberLauncherForActivityResult es el enfoque correcto ahora.

    – James Negro

    6 de mayo de 2021 a las 2:58

  • @JamesBlack Lo he hecho así. ¡Mucho, mucho más fácil!

    – foxtrotuniform6969

    3 de junio de 2021 a las 15:31

  • cómo usaremos shouldShowRequestPermissionRationale, debe tener una Actividad

    – joghm

    22 de enero a las 12:24

El resultado de la actividad tiene dos superficies API:

  • El núcleo ActivityResultRegistry. Esto es lo que realmente hace el trabajo subyacente.
  • Una interfaz de conveniencia en ActivityResultCaller eso ComponentActivity y Fragment Implementar que vincula la solicitud de resultado de actividad al ciclo de vida de la actividad o fragmento

Un Composable tiene una vida diferente a la Actividad o Fragmento (por ejemplo, si elimina el Composable de su jerarquía, debería limpiarse después de sí mismo) y, por lo tanto, usar el ActivityResultCaller API como registerForActivityResult() nunca es lo correcto.

En su lugar, debe utilizar el ActivityResultRegistry API directamente, llamando register() y unregister() directamente. Esto se combina mejor con el rememberUpdatedState() y DisposableEffect para crear una versión de registerForActivityResult que funciona con un 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
}

Entonces es posible usar esto en su propio Composable a través de un código como:

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())
}

  • ¿Tiene algún plan para exponer el registro como ambiente, como ActivityResultRegistryAmbient? está echando ContextAmbient a la actividad una mala praxis?

    – Nikola Despotoski

    6 de noviembre de 2020 a las 23:24

  • Puedes protagonizar el problema de solicitud de función por hacer que esto sea parte de Compose. OMI, un ActivityResultRegistryAmbient no es muy útil ya que nunca querrías usarlo fuera del alcance administrado de algo como esto registerForActivityResult(). Tenga en cuenta que no requiere ninguna actividad, solo la genérica ActivityResultRegistryOwnerpero a efectos prácticos, setContent requiere que estés dentro de un ComponentActivity de todos modos, por lo que este elenco siempre tiene éxito.

    – ianhanniballake

    6 de noviembre de 2020 a las 23:47

  • @ianhanniballake No sé por qué, pero esta solución está demostrando ser extremadamente poco confiable e impredecible. Parece currentOnResult.value(it) solo parece ser una llamada a veces, y no tengo ni idea de por qué. Es extremadamente frustrante.

    – foxtrotuniform6969

    3 de diciembre de 2020 a las 15:32

  • @Jeyhey: asegúrese de estar usando la Actividad 1.2.0-beta02 (y con ella, el Fragmento 1.3.0-beta02 para obtener las correcciones relacionadas en FragmentActivity / AppCompatActivity). Parece que te gustaría protagonizar la solicitud de función mencionado en los comentarios anteriores para hacer de esto ‘una función de utilidad del sistema’.

    – ianhanniballake

    5 de diciembre de 2020 a las 20:42

  • @nayandhabarde: parece que debería solicitar una función para ese SDK de pago: cualquier SDK puede proporcionar una ActivityResultContract que funciona igual de bien en una Actividad, Fragmento, Composable o en cualquier otro lugar.

    – ianhanniballake

    14 de abril de 2022 a las 1:56

A partir de Activity Compose 1.3.0-alpha03 y más allá, hay una nueva función de utilidad registerForActivityResult() que simplifica este proceso.

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

(De la muestra dada aquí )

Agregar en caso de que alguien esté iniciando una nueva intención externa. En mi caso, quería iniciar un aviso de inicio de sesión de Google al hacer clic en el botón en Jetpack Compose.

declara tu intención de lanzamiento

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

lanzar su nueva actividad o cualquier intención.

 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))
    }

#googleiniciar sesión

Para aquellos que no obtienen un resultado con la esencia proporcionada por @ianhanniballake en mi caso, el returnedLauncher en realidad captura un valor ya enajenado del realLauncher.

Entonces, si bien la eliminación de la capa de direccionamiento indirecto debería solucionar el problema, definitivamente no es la forma óptima de hacerlo.

Aquí está la versión actualizada, hasta que se encuentre una mejor solución:

@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
}

avatar de usuario de reznic
reznic

La llamada al método que solicita el permiso al usuario (p. ej., PermissionState.launchPermissionRequest()) debe invocarse desde un ámbito no componible.

val scope = rememberCoroutineScope()
if (!permissionState.status.isGranted) {
    scope.launch {
         permissionState.launchPermissionRequest()
    }
}

¿Ha sido útil esta solución?