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:

dependencies 
def nav_version = "2.5.3"

implementation("androidx.navigation:navigation-compose:$nav_version")

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:

navController.navigate("friendslist")

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,

@Navigator.Name("composable")
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:

@NavDestination.ClassType(Composable::class)
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) :
NavHostController(context)

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

public val currentScreensBackStack: StateFlow> =
_currentScreensBackStack.asStateFlow()

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
navigatorProvider.addNavigator(ComposeNavigator())
navigatorProvider.addNavigator(DialogNavigator())

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

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


the next step is building our own NavHost,

@Composable
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
navController.setLifecycleOwner(lifecycleOwner)
navController.setViewModelStore(viewModelStoreOwner.viewModelStore)
if (onBackPressedDispatcher != null)
navController.setOnBackPressedDispatcher(onBackPressedDispatcher)

// Ensure that the NavController only receives back events while
// the NavHost is in composition
DisposableEffect(navController)
navController.enableOnBackPressed(true)
onDispose
navController.enableOnBackPressed(false)

// 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>(
ComposeNavigator.NAME
) as? ComposeNavigator ?: return
val visibleEntries by remember(navController.visibleEntries)
navController.visibleEntries.map
it.filter entry ->
entry.destination.navigatorName == ComposeNavigator.NAME


.collectAsState(emptyList())
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(backStackEntry.id, modifier)
val lastEntry = visibleEntries.last entry ->
it == entry.id

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

initialCrossfade = false

onDispose
visibleEntries.forEach entry ->
composeNavigator.onTransitionComplete(entry)


lastEntry.LocalOwnersProvider(saveableStateHolder)
val previousScreenExtras = screensBackStack[lastEntry.destination.route]?.arguments

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

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


}

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

// Show any dialog destinations
DialogHost(dialogNavigator)
}

An overloaded function with route:

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

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
)
addDestination(
Destination(provider[ComposeNavigator::class], content).apply
this.route = route
arguments.forEach (argumentName, argument) ->
addArgument(argumentName, argument)

deepLinks.forEach deepLink ->
addDeepLink(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))) 

@Parcelize
data class Profile(val id: String) : Parcelable

companion object
const val PROFILE_ROUTE = "profile"

const val PROFILE_EXTRAS_KEY = "profile_extras_key"

@Composable
fun Page(profile: Profile)
Text(profile.id)


const val FRIENDS_LIST_ROUTE: String = "friendsList"

object FriendsList : Screen(FRIENDS_LIST_ROUTE) {

@Composable
fun Page(navigateNextScreen: (String) -> Unit)
LazyColumn
items(10)
Text(it.toString(), modifier = Modifier
.fillParentMaxWidth()
.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
ProfilePage.Page(profile)

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