Parcelable-Argument mit Compose-Navigation übergeben

Lesezeit: 7 Minuten

Ich möchte ein parzellierbares Objekt übergeben (BluetoothDevice) zu einem Composable mithilfe der Compose-Navigation.

Das Übergeben primitiver Typen ist einfach:

composable(
  "profile/{userId}",
  arguments = listOf(navArgument("userId") { type = NavType.StringType })
) {...}
navController.navigate("profile/user1234")

Aber ich kann kein parzellierbares Objekt in der Route übergeben, es sei denn, ich kann es in eine Zeichenfolge serialisieren.

composable(
  "deviceDetails/{device}",
  arguments = listOf(navArgument("device") { type = NavType.ParcelableType(BluetoothDevice::class.java) })
) {...}
val device: BluetoothDevice = ...
navController.navigate("deviceDetails/$device")

Der obige Code funktioniert offensichtlich nicht, weil er nur implizit aufruft toString().

Gibt es eine Möglichkeit, entweder a Parcelable zu einer String also kann ich es in der Route übergeben oder das Navigationsargument als Objekt mit einer anderen Funktion als übergeben navigate(route: String)?

  • Sie können Ihr Objekt JSON in String und dann zurück serialisieren

    – EpicPandaForce

    16. März 2021 um 17:59 Uhr

Ich habe eine kleine Erweiterung für den NavController geschrieben.

import android.os.Bundle
import androidx.core.net.toUri
import androidx.navigation.*

fun NavController.navigate(
    route: String,
    args: Bundle,
    navOptions: NavOptions? = null,
    navigatorExtras: Navigator.Extras? = null
) {
    val routeLink = NavDeepLinkRequest
        .Builder
        .fromUri(NavDestination.createRoute(route).toUri())
        .build()

    val deepLinkMatch = graph.matchDeepLink(routeLink)
    if (deepLinkMatch != null) {
        val destination = deepLinkMatch.destination
        val id = destination.id
        navigate(id, args, navOptions, navigatorExtras)
    } else {
        navigate(route, navOptions, navigatorExtras)
    }
}

Wie Sie überprüfen können, gibt es mindestens 16 Funktionen “navigieren” mit unterschiedlichen Parametern, es ist also nur ein Konverter zur Verwendung

public open fun navigate(@IdRes resId: Int, args: Bundle?) 

Mit dieser Erweiterung können Sie also Compose Navigation ohne diese schrecklichen Deep-Link-Parameter für Argumente bei Routen verwenden.

  • Dies sollte die akzeptierte Antwort sein.

    – M. Reza Nasirloo

    5. Oktober 2021 um 16:00 Uhr

  • Sieht gut aus, aber ich bekomme einen Nullzeiger, wenn ich es versuche.

    – Mutter

    11. Oktober 2021 um 20:44 Uhr

  • Beste Antwort. Du bist viel besser als Googler.

    – eggham0518

    3. Januar um 14:06 Uhr

  • Kannst du es verbessern und wie benutzt man es?

    – Gaohomway

    20. Februar um 6:08 Uhr

Benutzer-Avatar
Aidanvii

Hier ist eine andere Lösung, die auch funktioniert, indem das Parcelable zum richtigen hinzugefügt wird NavBackStackEntry, NICHT der vorherige Eintrag. Die Idee ist, zuerst anzurufen navController.navigateund fügen Sie dann das Argument zum letzten hinzu NavBackStackEntry.arguments in dem NavController.backQueue. Beachten Sie, dass dies eine andere Bibliotheksgruppen-eingeschränkte API verwendet (annotiert mit RestrictTo(LIBRARY_GROUP)), könnte also möglicherweise brechen. Lösungen, die von einigen anderen gepostet wurden, verwenden die eingeschränkte NavBackStackEntry.argumentsJedoch NavController.backQueue ist ebenfalls eingeschränkt.

Hier sind einige Erweiterungen für die NavController zum Navigieren u NavBackStackEntry zum Abrufen der Argumente innerhalb der Route Composable:


fun NavController.navigate(
    route: String,
    navOptions: NavOptions? = null,
    navigatorExtras: Navigator.Extras? = null,
    args: List<Pair<String, Parcelable>>? = null,
) {
    if (args == null || args.isEmpty()) {
        navigate(route, navOptions, navigatorExtras)
        return
    }
    navigate(route, navOptions, navigatorExtras)
    val addedEntry: NavBackStackEntry = backQueue.last()
    val argumentBundle: Bundle = addedEntry.arguments ?: Bundle().also {
        addedEntry.arguments = it
    }
    args.forEach { (key, arg) ->
        argumentBundle.putParcelable(key, arg)
    }
}

inline fun <reified T : Parcelable> NavController.navigate(
    route: String,
    navOptions: NavOptions? = null,
    navigatorExtras: Navigator.Extras? = null,
    arg: T? = null,
    
) {
    if (arg == null) {
        navigate(route, navOptions, navigatorExtras)
        return
    }
    navigate(
        route = route,
        navOptions = navOptions,
        navigatorExtras = navigatorExtras,
        args = listOf(T::class.qualifiedName!! to arg),
    )
}

fun NavBackStackEntry.requiredArguments(): Bundle = arguments ?: throw IllegalStateException("Arguments were expected, but none were provided!")

@Composable
inline fun <reified T : Parcelable> NavBackStackEntry.rememberRequiredArgument(
    key: String = T::class.qualifiedName!!,
): T = remember {
    requiredArguments().getParcelable<T>(key) ?: throw IllegalStateException("Expected argument with key: $key of type: ${T::class.qualifiedName!!}")
}

@Composable
inline fun <reified T : Parcelable> NavBackStackEntry.rememberArgument(
    key: String = T::class.qualifiedName!!,
): T? = remember {
    arguments?.getParcelable(key)
}

Um mit einem einzigen Argument zu navigieren, können Sie dies jetzt im Bereich von a tun NavGraphBuilder:

composable(route = "screen_1") {
    Button(
        onClick = {
            navController.navigate(
                route = "screen_2",
                arg = MyParcelableArgument(whatever = "whatever"),
            )
        }
    ) {
        Text("goto screen 2")
    }
}
composable(route = "screen_2") { entry ->
    val arg: MyParcelableArgument = entry.rememberRequiredArgument()
    // TODO: do something with arg
}

Oder wenn Sie mehrere Argumente desselben Typs übergeben möchten:

composable(route = "screen_1") {
    Button(
        onClick = {
            navController.navigate(
                route = "screen_2",
                args = listOf(
                    "arg_1" to MyParcelableArgument(whatever = "whatever"),
                    "arg_2" to MyParcelableArgument(whatever = "whatever"),
                ),
            )
        }
    ) {
        Text("goto screen 2")
    }
}
composable(route = "screen_2") { entry ->
    val arg1: MyParcelableArgument = entry.rememberRequiredArgument(key = "arg_1")
    val arg2: MyParcelableArgument = entry.rememberRequiredArgument(key = "arg_2")
    // TODO: do something with args
}

Der Hauptvorteil dieses Ansatzes besteht darin, dass er ähnlich wie bei der Antwort, bei der Moshi zum Serialisieren des Arguments verwendet wird, funktioniert, wenn popUpTo wird verwendet in der navOptionsist aber auch effizienter, da keine JSON-Serialisierung beteiligt ist.

Dies funktioniert natürlich nicht mit Deep Links, aber es überlebt die Prozess- oder Aktivitätserneuerung. Für Fälle, in denen Sie Tiefenlinks oder sogar nur optionale Argumente für Navigationsrouten unterstützen müssen, können Sie die verwenden entry.rememberArgument Verlängerung. nicht wie entry.rememberRequiredArgumentgibt es null zurück, anstatt ein zu werfen IllegalStateException.

  • addedEntry.arguments = it kann nicht neu zugewiesen werden, vielleicht putParceable verwenden??

    – Mutter

    11. Oktober 2021 um 21:21 Uhr

  • Dies brach zusammen, nachdem eine neue Version der Compose-Navigation veröffentlicht wurde (früher war es var, nicht val). Das Problem bei der Verwendung von putParcelable besteht darin, dass das Argumentfeld nicht null sein darf (es ist nullable). Ich musste einen temporären Hack einfügen, um es durch Reflektion zu setzen, wenn es null ist. Ich empfehle, eine andere Lösung zu finden. Finden Sie entweder eine andere Navigationsbibliothek, die Entwickler weniger dogmatisch dazu zwingt, Navigationsrouten als URLs zu modellieren, oder passen Sie sich dem Dogmatismus an, den uns das Team dahinter aufzwingt, und strukturieren Sie Ihren Code neu, um IDs an Navigationsrouten zu übergeben, und suchen Sie dann die Daten, die Sie benötigen, mithilfe von IDs .

    – Aidanvii

    9. November 2021 um 9:24 Uhr

Die von @nglauber angegebene backStackEntry-Lösung funktioniert nicht, wenn wir auftauchen (popUpTo(...)) Backstacks auf navigate(...).

Also hier ist eine andere Lösung. Wir können das Objekt übergeben, indem wir es in einen JSON-String konvertieren.

Beispielcode:

val ROUTE_USER_DETAILS = "user-details?user={user}"


// Pass data (I am using Moshi here)
val user = User(id = 1, name = "John Doe") // User is a data class.

val moshi = Moshi.Builder().build()
val jsonAdapter = moshi.adapter(User::class.java).lenient()
val userJson = jsonAdapter.toJson(user)

navController.navigate(
    ROUTE_USER_DETAILS.replace("{user}", userJson)
)


// Receive Data
NavHost {
    composable(ROUTE_USER_DETAILS) { backStackEntry ->
        val userJson =  backStackEntry.arguments?.getString("user")
        val moshi = Moshi.Builder().build()
        val jsonAdapter = moshi.adapter(User::class.java).lenient()
        val userObject = jsonAdapter.fromJson(userJson)

        UserDetailsView(userObject) // Here UserDetailsView is a composable.
    }
}


// Composable function/view
@Composable
fun UserDetailsView(
    user: User
){
    // ...
}

  • addedEntry.arguments = it kann nicht neu zugewiesen werden, vielleicht putParceable verwenden??

    – Mutter

    11. Oktober 2021 um 21:21 Uhr

  • Dies brach zusammen, nachdem eine neue Version der Compose-Navigation veröffentlicht wurde (früher war es var, nicht val). Das Problem bei der Verwendung von putParcelable besteht darin, dass das Argumentfeld nicht null sein darf (es ist nullable). Ich musste einen temporären Hack einfügen, um es durch Reflektion zu setzen, wenn es null ist. Ich empfehle, eine andere Lösung zu finden. Finden Sie entweder eine andere Navigationsbibliothek, die Entwickler weniger dogmatisch dazu zwingt, Navigationsrouten als URLs zu modellieren, oder passen Sie sich dem Dogmatismus an, den uns das Team dahinter aufzwingt, und strukturieren Sie Ihren Code neu, um IDs an Navigationsrouten zu übergeben, und suchen Sie dann die Daten, die Sie benötigen, mithilfe von IDs .

    – Aidanvii

    9. November 2021 um 9:24 Uhr

Benutzer-Avatar
Dharman

Folgend nglauber Vorschlag, ich habe zwei Erweiterungen erstellt, die mir ein bisschen helfen

@Suppress("UNCHECKED_CAST")
fun <T> NavHostController.getArgument(name: String): T {
    return previousBackStackEntry?.arguments?.getSerializable(name) as? T
        ?: throw IllegalArgumentException()
}

fun NavHostController.putArgument(name: String, arg: Serializable?) {
    currentBackStackEntry?.arguments?.putSerializable(name, arg)
}

Und ich benutze sie so:

Source:
navController.putArgument(NavigationScreens.Pdp.Args.game, game)
navController.navigate(NavigationScreens.Pdp.route)

Destination:
val game = navController.getArgument<Game>(NavigationScreens.Pdp.Args.game)
PdpScreen(game)

  • previousBackStackEntry?.arguments? => arguments ist Null

    – Dr.jacky

    4. Oktober 2021 um 15:20 Uhr

1010820cookie-checkParcelable-Argument mit Compose-Navigation übergeben

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

Privacy policy