Navigating With Parcelable Arguments in Jetpack Compose | by Usman Akhmedov | Nov, 2022

Use navigation composition in your Android apps

image by author

Android team announced that a Parcelable Transferring arguments between screens is a counterexample. But many are not yet ready to give up … and it is not convenient to save all the data before displaying it on another screen.

Today you will learn an easy way to pass arguments using Jetpack compose navigation library.

To begin with, let’s look at what Google offers us to navigate the arguments. Solution with substantial boilerplate code, but we’ll have to compare it to my solution.

First of all, you need to add navigation library for the project:

def nav_version = "2.5.3"


After this we should create our navigation graph, where NavController There is a central API for controlling your navigation stack.

val navController = rememberNavController()

NavHost(navController = navController, startDestination = "profile")
composable("profile") Profile(/*...*/)
composable("friendslist") FriendsList(/*...*/)

NavHost links to NavController With a navigation graph that specifies your composables that should be able to navigate between. when you run it NavHostwill start from startDestination ,profile, To go to the next screen, you need to call the following:


It looks simple, but there are two main problems here:

  1. we have String as a path that can lead to errors when writing code
  2. we can’t send parcelable Between screens without boilerplate as shown Here,

So, now let me show you my solution to the problem.

First, we need to create an abstract class that describes our screen. Here’s what it looks like:

abstract class Screen(
open val route: String,
open val arguments: Bundle? = null
constructor(route: String, extra: Extra) : this(route, Bundle().apply putParcelable(extra.key, extra.parcelable) )

data class Extra(val key: String, val parcelable: Parcelable)

Next, we need to create our own implementation ComposeNavigator,

public class ComposeNavigator : Navigator()

Where Destination We have our own class, which differs from the parent class in that it accepts lambda content with arguments:

public class Destination(
navigator: ComposeNavigator,
internal val content: @Composable (backStackEntry: NavBackStackEntry, arguments: Bundle?) -> Unit
) : NavDestination(navigator)

the next step would be to make a NavController which will control our screen:

public class NavExtrasHostController(context: Context, public val startDestination: Screen) :

private val _currentScreensBackStack: MutableStateFlow> =
MutableStateFlow(mutableMapOf(startDestination.route to startDestination))

public val currentScreensBackStack: StateFlow> =

override fun popBackStack(): Boolean
_currentScreensBackStack.update screensMap ->
screensMap.apply remove(currentDestination?.route)

return super.popBackStack()

public fun navigate(screen: Screen, navOptions: NavOptions? = null)
_currentScreensBackStack.update it.apply put(screen.route, screen)
navigate(route = screen.route, navOptions = navOptions)

here you can see currentScreensBackStack There is a map that stores the screen in the root stack, so we can move along the graph while keeping the backstack. When the user presses back, we remove the previous screen from our back stack.

and you can see, a new func navigates where you can add the next screen currentScreensBackStack,

Now, we’ll create a func that allows us to create our NavExtrasHostControllerBut before that we should create a Saver class which does save and restore NavExtrasHostController config changes and process of death:

private fun NavExtrasControllerSaver(
context: Context,
startDestination: Screen
): Saver = Saver(
save = it.saveState() ,
restore = createNavExtrasController(context, startDestination).apply restoreState(it)

And createNavExtrasController() Celebration:

private fun createNavExtrasController(context: Context, startDestination: Screen) =
NavExtrasHostController(context, startDestination = startDestination).apply

After these steps, we can create remember function, which will create out NavExtrasHostController add our ComposeNavigator and use NavExtrasControllerSaver To save and restore it:

public fun rememberNavExtrasController(
startDestination: Screen,
vararg navigators: Navigator
): NavExtrasHostController
val context = LocalContext.current
return rememberSaveable(
inputs = navigators,
saver = NavExtrasControllerSaver(context, startDestination)
createNavExtrasController(context, startDestination)
for (navigator in navigators)

the next step is building our own NavHost,

public fun NavHost(
navController: NavExtrasHostController,
graph: NavGraph,
modifier: Modifier = Modifier
) {
val lifecycleOwner = LocalLifecycleOwner.current
val viewModelStoreOwner = checkNotNull(LocalViewModelStoreOwner.current)
"NavHost requires a ViewModelStoreOwner to be provided via LocalViewModelStoreOwner"

val onBackPressedDispatcherOwner = LocalOnBackPressedDispatcherOwner.current
val onBackPressedDispatcher = onBackPressedDispatcherOwner?.onBackPressedDispatcher

// Setup the navController with proper owners
if (onBackPressedDispatcher != null)

// Ensure that the NavController only receives back events while
// the NavHost is in composition

// Then set the graph
navController.graph = graph

val saveableStateHolder = rememberSaveableStateHolder()

// Find the ComposeNavigator, returning early if it isn't found
// (such as is the case when using TestNavHostController)
val composeNavigator = navController.navigatorProvider.get>(
) as? ComposeNavigator ?: return
val visibleEntries by remember(navController.visibleEntries)
it.filter entry ->
entry.destination.navigatorName == ComposeNavigator.NAME

val screensBackStack by navController.currentScreensBackStack.collectAsState()

val backStackEntry = visibleEntries.lastOrNull()

var initialCrossfade by remember mutableStateOf(true)
if (backStackEntry != null) {
// while in the scope of the composable, we provide the navBackStackEntry as the
// ViewModelStoreOwner and LifecycleOwner
Crossfade(, modifier)
val lastEntry = visibleEntries.last entry ->
it ==

// We are disposing on a Unit as we only want to dispose when the CrossFade completes
if (initialCrossfade)
// There's no animation for the initial crossfade,
// so we can instantly mark the transition as complete
visibleEntries.forEach entry ->

initialCrossfade = false

visibleEntries.forEach entry ->

val previousScreenExtras = screensBackStack[lastEntry.destination.route]?.arguments

val newLastEntryArguments = (lastEntry.arguments ?: Bundle()).apply
if (previousScreenExtras != null)

(lastEntry.destination as Destination).content(lastEntry, newLastEntryArguments)


val dialogNavigator = navController.navigatorProvider.get>(
) as? DialogNavigator ?: return

// Show any dialog destinations

An overloaded function with route:

public fun NavExtrasHost(
navController: NavExtrasHostController,
modifier: Modifier = Modifier,
route: String? = null,
builder: NavGraphBuilder.() -> Unit
remember(route, navController.startDestination.route, builder)
navController.createGraph(navController.startDestination.route, route, builder)

After these steps, here is the last step we need to make composable Function that passes arguments to content:

public fun NavGraphBuilder.composable(
route: String,
arguments: List = emptyList(),
deepLinks: List = emptyList(),
content: @Composable (backStackEntry: NavBackStackEntry, arguments: Bundle?) -> Unit
Destination(provider[ComposeNavigator::class], content).apply
this.route = route
arguments.forEach (argumentName, argument) ->
addArgument(argumentName, argument)

deepLinks.forEach deepLink ->


So we updated composable Worked and added our logic to the navigation.

Before we start using our implementation of the navigation library, we should describe our screen. Here’s the code:

data class ProfilePage(val id: String) : Screen(PROFILE_ROUTE, Screen.Extra(PROFILE_EXTRAS_KEY, Profile(id))) 

data class Profile(val id: String) : Parcelable

companion object
const val PROFILE_ROUTE = "profile"

const val PROFILE_EXTRAS_KEY = "profile_extras_key"

fun Page(profile: Profile)

const val FRIENDS_LIST_ROUTE: String = "friendsList"

object FriendsList : Screen(FRIENDS_LIST_ROUTE) {

fun Page(navigateNextScreen: (String) -> Unit)
Text(it.toString(), modifier = Modifier
.clickable navigateNextScreen(it.toString()) )


then we should execute NavExtrasHostand that’s all:

val navController = rememberNavExtrasController(startDestination = FriendsList)
NavExtrasHost(navController = navController)
composable(ProfilePage.PROFILE_ROUTE) _, arguments ->
val profile: Profile = arguments?.getParcelable(PROFILE_EXTRAS_KEY) ?: return@composable

composable(FriendsList.route) _, _ ->
FriendsList.Page(navigateNextScreen = navController.navigate(ProfilePage(it)) )

So, now you can navigate with parcelable Logic.

That’s all. Thank you for reading.

Leave a Reply