Use navigation composition in your Android apps
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 NavHost
will 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:
- we have
String
as a path that can lead to errors when writing code - 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
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 NavExtrasHostController
But 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 NavExtrasHost
and 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.