Jetpack Compose: Starten Sie die ActivityResultContract-Anfrage über die Composable-Funktion

Lesezeit: 7 Minuten

Benutzeravatar von foxtrotuniform6969
foxtrotuniform6969

Ab 1.2.0-beta01 von androidx.activity:activity-ktxman kann nicht mehr launch Die mit erstellte Anfrage Activity.registerForActivityResult()wie im obigen Link unter „Verhaltensänderungen“ hervorgehoben und im angezeigt Google-Problem hier.

Wie soll eine Anwendung diese Anfrage über a starten? @Composable Funktioniert das jetzt? Zuvor konnte eine App die Instanz von übergeben MainActivity entlang der Kette über die Verwendung eines Ambient und dann die Anfrage einfach starten.

Das neue Verhalten kann umgangen werden, indem beispielsweise eine Klasse, die sich für das Aktivitätsergebnis registriert, in der Kette weitergegeben wird, nachdem sie außerhalb der Aktivität instanziiert wurde onCreate Funktion und starten Sie dann die Anfrage in a Composable. Allerdings kann auf diese Weise kein Rückruf registriert werden, der nach Abschluss ausgeführt werden soll.

Man könnte dies umgehen, indem man benutzerdefinierte erstellt ActivityResultContract, die beim Start einen Rückruf annehmen. Dies würde jedoch bedeuten, dass praktisch keine der eingebauten Funktionen vorhanden ist ActivityResultContracts könnte mit Jetpack Compose verwendet werden.

TL;DR

Wie würde eine App eine starten? ActivityResultsContract Anfrage von a @Composable Funktion?

Benutzeravatar von ameencarpenter
ameencarpenter

Ab androidx.activity:activity-compose:1.3.0-alpha06Die registerForActivityResult() API wurde umbenannt in rememberLauncherForActivityResult() um die Rückgabe besser anzuzeigen ActivityResultLauncher ist ein verwaltetes Objekt, das in Ihrem Namen gespeichert wird.

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

  • Dies sollte nun die akzeptierte Antwort sein, da „rememberLauncherForActivityResult“ jetzt der richtige Ansatz ist.

    – James Black

    6. Mai 2021 um 2:58

  • @JamesBlack Ich habe es so gemacht. Viel viel einfacher!

    – foxtrotuniform6969

    3. Juni 2021 um 15:31 Uhr

  • Wie wir ShouldShowRequestPermissionRationale verwenden werden, muss es eine Aktivität haben

    – joghm

    22. Januar um 12:24

Das Aktivitätsergebnis verfügt über zwei API-Oberflächen:

  • Der Kern ActivityResultRegistry. Das ist es, was eigentlich die zugrunde liegende Arbeit leistet.
  • Eine praktische Schnittstelle in ActivityResultCaller Das ComponentActivity Und Fragment Implementieren Sie, dass die Aktivitätsergebnisanforderung mit dem Lebenszyklus der Aktivität oder des Fragments verknüpft wird

Ein Composable hat eine andere Lebensdauer als die Aktivität oder das Fragment (z. B. wenn Sie das Composable aus Ihrer Hierarchie entfernen, sollte es nach sich selbst bereinigt werden) und verwendet daher das ActivityResultCaller APIs wie registerForActivityResult() ist nie das Richtige.

Stattdessen sollten Sie die verwenden ActivityResultRegistry APIs direkt aufrufend register() Und unregister() direkt. Dies lässt sich am besten mit dem kombinieren rememberUpdatedState() Und DisposableEffect um eine Version davon zu erstellen registerForActivityResult das funktioniert mit einem 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
}

Dann ist es möglich, dies in Ihrem eigenen Composable über Code zu verwenden, wie zum Beispiel:

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

  • Haben Sie Pläne, die Registrierung als Ambient-Registrierung verfügbar zu machen? ActivityResultRegistryAmbient? Ist Casting ContextAmbient einen Kunstfehler begehen?

    – Nikola Despotoski

    6. November 2020 um 23:24

  • Sie können das markieren Problem mit der Funktionsanforderung dafür, dass Sie diesen Teil von Compose selbst gemacht haben. Ich stöhne ActivityResultRegistryAmbient ist nicht sehr hilfreich, da Sie es niemals außerhalb des verwalteten Bereichs von so etwas verwenden möchten registerForActivityResult(). Beachten Sie, dass Sie keine Aktivität benötigen, sondern nur die allgemeine ActivityResultRegistryOwneraber aus praktischen Gründen, setContent erfordert, dass Sie sich innerhalb eines befinden ComponentActivity Wie auch immer, diese Besetzung gelingt also immer.

    – ianhanniballake

    6. November 2020 um 23:47 Uhr

  • @ianhanniballake Ich weiß nicht warum, aber diese Lösung erweist sich als äußerst unzuverlässig und unvorhersehbar. Es sieht so aus currentOnResult.value(it) Scheint nur ein Anruf zu sein Manchmal, und ich habe keine Ahnung warum. Es ist äußerst frustrierend.

    – foxtrotuniform6969

    3. Dezember 2020 um 15:32 Uhr

  • @Jeyhey – Stellen Sie sicher, dass Sie Aktivität 1.2.0-beta02 (und damit Fragment 1.3.0-beta02) verwenden, um die zugehörigen Korrekturen zu erhalten FragmentActivity / AppCompatActivity). Es hört sich so an, als würdest du die Hauptrolle spielen wollen die Funktionsanfrage oben in den Kommentaren erwähnt, um dies zu einer „Systemdienstprogrammfunktion“ zu machen.

    – ianhanniballake

    5. Dezember 2020 um 20:42 Uhr

  • @nayandhabarde – es hört sich so an, als ob Sie eine Funktionsanfrage an dieses Zahlungs-SDK stellen sollten – beliebig SDK kann eine bereitstellen ActivityResultContract Das funktioniert in einer Aktivität, einem Fragment, einem Composable oder anderswo gleichermaßen gut.

    – ianhanniballake

    14. April 2022 um 1:56

Ab Activity Compose 1.3.0-alpha03 und darüber hinaus gibt es eine neue Nutzenfunktion registerForActivityResult() Das vereinfacht diesen Prozess.

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

(Aus der gegebenen Probe Hier )

Hinzufügen für den Fall, dass jemand eine neue externe Absicht startet. In meinem Fall wollte ich beim Klicken auf die Schaltfläche in Jetpack Compose eine Google-Anmeldeaufforderung starten.

Erklären Sie Ihre Absicht, zu starten

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

Starten Sie Ihre neue Aktivität oder eine andere Absicht.

 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

Für diejenigen, die mit dem Kerngedanken von @ianhanniballake in meinem Fall kein Ergebnis zurückbekommen returnedLauncher erfasst tatsächlich einen bereits entsorgten Wert des realLauncher.

Obwohl das Entfernen der Indirektionsebene das Problem beheben sollte, ist dies definitiv nicht die optimale Vorgehensweise.

Hier ist die aktualisierte Version, bis eine bessere Lösung gefunden wird:

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

reznics Benutzeravatar
reznic

Der Aufruf der Methode, die die Berechtigung für den Benutzer anfordert (z. B. PermissionState.launchPermissionRequest()), muss aus einem nicht zusammensetzbaren Bereich aufgerufen werden.

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

1453220cookie-checkJetpack Compose: Starten Sie die ActivityResultContract-Anfrage über die Composable-Funktion

This website is using cookies to improve the user-friendliness. You agree by using the website further.

Privacy policy