Passing multi typed data between screens with Jetpack Compose navigation component.

In this third part of the Jetpack Compose navigation series, we would learn how to pass multi typed data using the compose navigation components while using its SafeArgs feature.

Ziv Kesten
ProAndroidDev

--

Here is the list of the blogs in this series:

In the last parts of the series, we wrote a small fun application using Jetpack Compose, where we implemented navigation with a bottom navigation bar and passed some string arguments between the screens using SafeArgs.

The code for this app can be cloned from here.
In this, the third part of the series we would learn how to pass data of different types.
to follow along with this article, you can checkout the safeArgs branch from that same repository.

Why are non-string typed arguments different with SafeArgs?

From the documentation:

By default, all arguments are parsed as strings.

But don’t get discouraged, there is a way to pass other types of data as arguments between destinations.

In part 2 of the series, we showed how we can declare a composable in the navigation graph with an argument appended to its route.

We also remember that composable can take a list of arguments as parameters by looking at the source code of the Compose Navigation library:

public fun NavGraphBuilder.composable(
route: String,
arguments: List<NamedNavArgument> = listOf(),
deepLinks: List<NavDeepLink> = listOf(),
content: @Composable (NavBackStackEntry) -> Unit
) {...}

But what does this have to do with the argument types?

In the last part of the series we showed how to add an argument to a route and extract it, but the extracted argument defaulted to a string type.

To declare a different type for an argument we add a navArgument object (or a list of them) to the composable like this:

composable(route + "ARGUMENT_KEY", <LIST_OF_NAV_ARGUMENTS>) { ... }

Each navArgument can be implemented like this:

navArgument(ARGUMENT_KEY) { type = <ARGUMENT_TYPE> }

Let's break it down a little bit

This is the definition of the navArgument from the Compose Navigation library source code.

fun navArgument(
name: String,
builder: NavArgumentBuilder.() -> Unit
): NamedNavArgument

The navArgument is a function that takes the argument’s key and a NavArgumentBuilder lambda to define its type, It returns a NamedNavArgument that we can pass to the composable.

In our case we can implement it like this:

navArgument(ARGUMENT_KEY) {
type = NavType.IntType
}

Notice how we defined the argument type here as IntType.
We follow up by extracting the argument as an int type:

val scaryAnimationId = backStackEntry.arguments?.getInt(ARGUMENT_KEY)

Just like we did before, only now we use getInt instead of getString.
And our code for declaring a composable destination with an integer type would end up like this:

Can we pass non-primitive types?

In the view system navigation component, we were able to pass parcelable objects as arguments to the destinations, you might be tempted to do that in Compose Navigation as well and write up code that looks like this:

navArgument(ARGUMENT_KEY) {
type = NavType.ParcelableArrayType(MyObject::class.java)
}

However, this would result in a crash:

UnsupportedOperationException: Parcelables don't support default values

This is caused by the fact that the Compose Navigation does not currently support default values for parcelable objects, you can learn more about this in this slack thread.

What about optional types?

Sometimes we might have a use case where we would want to pass values that are optional, which means we don’t know about their nullability status.

Those values differ from normal types in two ways.
From the documentation:

1. They must be included using query parameter syntax ("?argName={argName}")

2. They must have a defaultValue set, or have nullability = true (which implicitly sets the default value to null)

In other words, if we use optional types we have to decide if they can actually be null, or they must provide some default value.
The answer to this depends on your own implementation, sometimes, the receiver of the argument (a function, a class, etc.) would have its own default implementation, and so passing a null value to it would be fine.
In other cases, the receiver might not be able to handle an optional value, and in this case, we would want to provide it with some default value.

How would we code this?

Until now we declared 3 composables in our Navigation Graph, one with no arguments, one with a default string argument, and a third with an Integer type argument.

Now we want to add another composable destination to the graph, one with an option value, first, we would create the argument:

val argument = "?$ARGUMENT_KEY=${ScaryAnimation.ScaryBag.animId}"

We are using query parameter syntax here.

Query parameters are a defined set of parameters attached to the end of a url

we start with a question mark, followed by the argument key (could be any string, just like the keys we used so far), then an equals sign (=) and the argument itself.

In human language we can say:

“(?) for the key: (ARGUMENT_KEY) we should put (=) ${MY_VARIABLE}

So we have our argument, we can now append it to the route just like we did with the non-optional arguments:

val argument = "?$ARGUMENT_KEY=${ScaryAnimation.ScaryBag.animId}"
navigateWithArguments
(
argument = argument,
screen = screen,
navController = navController)

Extracting time

To extract an optional argument we need to add it as a navArgument to the composable destination jut like before:

BottomNavigationScreens.ScaryBag.route 
+ "?$ARGUMENT_KEY={$ARGUMENT_KEY}")

Notice that here we are placing the argument key after the `=` sign

Then we can add this new route with the argument to the composable destination, followed by a navArgument to define its nullability status.

composable(
BottomNavigationScreens.ScaryBag.route
.plus("?$ARGUMENT_KEY={$ARGUMENT_KEY}"),
arguments = listOf(navArgument(ARGUMENT_KEY) { //In the case that we need a default value we add:
defaultValue = ScaryAnimation.ScaryBag.animId.toString()
//In the case that we can use a null value we add:
nullability = true
})
) { ... }

Remember, our composable destination takes the route string and a list of arguments, each of them should be of type navArgument and with either a defaultValue or the nullability property set to true.

And to extract the value, we use the NavBackStackEntry as we did before:

val scaryAnimationIdAsString = backStackEntry.arguments?.getString(ARGUMENT_KEY)

Now we have a NavHost populated with four destination composables

  • A destination with no arguments
  • A destination with a default string argument
  • A destination with an integer argument
  • A destination with an optional string argument

And that's it! using SafeArgs can be a great way to pass data using איק Navigation library in Jetpack Compose, it might be a bit tricky to grasp at first, but hopefully, these articles might make it a bit simpler to understand.

Remember you can look at the source code for SpookyNavigation in my GitHub, and the code for part 2 and part 3 of the series is in the safeArgs branch of this repository.

Clap and share! If you think I deserve it :)

--

--

I am a mobile developer, an android enthusiast and a drone lover (Secretly, don’t tell the wife)