Pase argumento Parcelable con navegación compuesta

7 minutos de lectura

Quiero pasar un objeto parcelable (BluetoothDevice) a un componible usando la navegación compuesta.

Pasar tipos primitivos es fácil:

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

Pero no puedo pasar un objeto parcelable en la ruta a menos que pueda serializarlo en una cadena.

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

El código anterior obviamente no funciona porque solo llama implícitamente toString().

¿Hay alguna forma de serializar un Parcelable a un String entonces puedo pasarlo en la ruta o pasar el argumento de navegación como un objeto con una función diferente a navigate(route: String)?

  • puede JSON serializar su objeto a String y luego volver

    – EpicPandaForce

    16 de marzo de 2021 a las 17:59

He escrito una pequeña extensión para NavController.

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

Como puedes comprobar hay al menos 16 funciones”navegar” con diferentes parámetros, por lo que es solo un convertidor para usar

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

Entonces, al usar esta extensión, puede usar Compose Navigation sin estos terribles parámetros de enlace profundo para argumentos en las rutas.

  • Esta debería ser la respuesta aceptada.

    – M. Reza Nasirloo

    5 oct 2021 a las 16:00

  • Se ve bien, pero obtengo un puntero nulo cuando lo intento.

    – mamá

    11/10/2021 a las 20:44

  • La mejor respuesta. Eres mucho mejor que Googler.

    – eggham0518

    3 ene a las 14:06

  • ¿Puedes mejorarlo y cómo usarlo?

    – gaohomway

    20 de febrero a las 6:08

avatar de usuario
Aidanvii

Aquí hay otra solución que también funciona agregando Parcelable a la correcta NavBackStackEntry, NO la entrada anterior. La idea es primero llamar navController.navigateluego agregue el argumento al último NavBackStackEntry.arguments en el NavController.backQueue. Tenga en cuenta que esto utiliza otra API restringida del grupo de bibliotecas (anotada con RestrictTo(LIBRARY_GROUP)), por lo que podría romperse. Las soluciones publicadas por otros usan el restringido NavBackStackEntry.argumentssin embargo NavController.backQueue también está restringido.

Aquí hay algunas extensiones para el NavController para navegar y NavBackStackEntry para recuperar los argumentos dentro de la ruta componible:


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

Para navegar con un solo argumento, ahora puede hacerlo en el ámbito de un 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
}

O si desea pasar múltiples argumentos del mismo tipo:

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
}

El beneficio clave de este enfoque es que, de manera similar a la respuesta que usa Moshi para serializar el argumento, funcionará cuando popUpTo se utiliza en el navOptionspero también será más eficiente ya que no se involucra la serialización JSON.

Por supuesto, esto no funcionará con enlaces profundos, pero sobrevivirá a la recreación de procesos o actividades. Para los casos en los que necesite admitir enlaces profundos o incluso solo argumentos opcionales para las rutas de navegación, puede usar el entry.rememberArgument extensión. A diferencia de entry.rememberRequiredArgumentdevolverá nulo en lugar de lanzar un IllegalStateException.

  • addedEntry.arguments = it no se puede reasignar, ¿tal vez usar putParceable?

    – mamá

    11 oct 2021 a las 21:21

  • Esto se rompió después de que se lanzó una nueva versión de navegación compuesta (solía ser var no val). El problema con el uso de putParcelable es que requiere que el campo de argumentos no sea nulo (es anulable). Tuve que instalar un truco temporal para configurarlo a través de la reflexión si es nulo. Recomiendo encontrar otra solución. Encuentre otra biblioteca de navegación que sea menos dogmática acerca de obligar a los desarrolladores a modelar rutas de navegación como URL, o cumpla con el dogmatismo que nos impone el equipo detrás de esto y reestructure su código para pasar ID a rutas de navegación, luego busque los datos que necesita usando ID .

    – Aidanvii

    9 de noviembre de 2021 a las 9:24

La solución backStackEntry proporcionada por @nglauber no funcionará si aparece (popUpTo(...)) pilas traseras en navigate(...).

Así que aquí hay otra solución. Podemos pasar el objeto convirtiéndolo en una cadena JSON.

Código de ejemplo:

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 no se puede reasignar, ¿tal vez usar putParceable?

    – mamá

    11 oct 2021 a las 21:21

  • Esto se rompió después de que se lanzó una nueva versión de navegación compuesta (solía ser var no val). El problema con el uso de putParcelable es que requiere que el campo de argumentos no sea nulo (es anulable). Tuve que instalar un truco temporal para configurarlo a través de la reflexión si es nulo. Recomiendo encontrar otra solución. Encuentre otra biblioteca de navegación que sea menos dogmática acerca de obligar a los desarrolladores a modelar rutas de navegación como URL, o cumpla con el dogmatismo que nos impone el equipo detrás de esto y reestructure su código para pasar ID a rutas de navegación, luego busque los datos que necesita usando ID .

    – Aidanvii

    9 de noviembre de 2021 a las 9:24

avatar de usuario
dharma

Siguiente nglauber sugerencia, he creado dos extensiones que me están ayudando un poco

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

Y los uso de esta manera:

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 es nulo

    – Dr. jacky

    4 oct 2021 a las 15:20

¿Ha sido útil esta solución?

Esta web utiliza cookies propias y de terceros para su correcto funcionamiento y para fines analíticos y para mostrarte publicidad relacionada con sus preferencias en base a un perfil elaborado a partir de tus hábitos de navegación. Al hacer clic en el botón Aceptar, acepta el uso de estas tecnologías y el procesamiento de tus datos para estos propósitos. Configurar y más información
Privacidad