Android Jetpack Compose and Navigation

A simple example of using NavHost and NavController

Tomáš Repčík
4 min readDec 3, 2022
Photo by Brendan Church on Unsplash

With Jetpack Compose, Android is abandoning implicit ways of moving around with fragment transactions, navigations, and intents for activities. Nowadays, the app should declare all the possible paths users can take at the beginning.

NavHost and NavController, let usyou can push or pop your composables as you need. So let's get to it.

Dependencies

Firstly, you will need to add a dependency to your project. To get the current version, go here.

implementation "androidx.navigation:navigation-compose:2.5.3"

Going from screen to screen and back

Implementation

Firstly, NavHost and NavController are introduced to the project.

NavHost hosts the declaration of the routes which users can take. NavController provides API to move around the declared routes. Route is path, which is unique for each screen/other composable which could be shown in the app.

Main activity with NavHost

Instantiation

NavController is instantiated with rememberNavController(), so even upon the rebuild of composables, the NavController is the same.

Starting destination

The NavHost with NavController and starting destination are declared. Starting destination is the name of our first screen route.

Declaration of routes

All the options are declared where a user can move. Both screens need to declare routes. The route name is unique because the NavController will use them to identify the next composable to show.

Code of the two screens
Navigation between two screens

Any composable can be shown via the navcontroller.navigate(“route to composable”).

To mimic the back button of the user, the navcontroller.popBackStack() can be used. This will pop out the current composable from the stack of all composable.

Stack can be viewed as a history of the composables/screens, so first screen is the root and other composable is put on top of it. So by navigating to second screen, the second screen is added on top of the stack. By popping out the stack, the top composable is removed.

Clean up

To be more error-prone in the future, [aths should be declared more cleanly. So avoid adding strings everywhere and create the standalone file where you put all the routes for your app. Afterward, the declared constants should be used. Embarrassing typos will be avoided in the future. Moreover, it provides a more descriptive view of how the app is structured.

Going through multiple screens and back to the root

From now on adding subsequent screens to our navigation is easy. The NavHost needs to be extended by one more composable, and screens need to adopt more buttons to move around.

The third screen contains a button, which gets the user back to the first screen and cleans all the stacked components.

To pop multiple composables in the stack, navController.popBackStack(“Screen where you want to stop the popping”, inclusive: false) can be used.

The inclusive parameter tells us if the targeted composable should be removed too. Removing the first screen would result in a blank screen, so the false value is passed.

Passing value to the screen

Most apps want to show information or data dependent on the user's previous choice. Another screen needs to know what to show, so value needs to be passed.

This is done by putting the value inside the route, so the value is encoded as a string and then parsed at another screen.

Declaration

Firstly, the route needs to declare the presence of the value, which is done like this:

const val SECOND_SCREEN = "SecondScreen/{customValue}"
// where the {customValue} is placeholder for the passed value

Add the argument to composable too:

composable(
Routes.SECOND_SCREEN,
arguments = listOf(navArgument("customValue") {
type = NavType.StringType
})
) { backStackEntry ->
SecondScreen(
navigation = navController,
// callback to add new composable to backstack
// it contains the arguments field with our value
textToShow = backStackEntry.arguments?.getString(
"customValue", // key for our value
"Default value" // default value
)
)
}

Next, the value needs to be passed to the screen by formatting the value inside of the path like this and calling navController with it:

val valueToPass = "Information for next screen"
val routeToScreenWithValue = "SecondScreen/$valueToPass"
navigation.navigate(routeToScreenWithValue)

Example

The first screen now contains one text field, which contents are passed to the second screen.

Do not pass complex data to views in the navigation

Only one value should be passed to the view. Other data should be obtained with the help of the ViewModel or other logic unit. It will make your code much cleaner and more testable.

For example, if you want to show details about a product or user, an ID should be passed to the view only. The logic unit obtains the ID and digs up more detailed information from the internet or a database. The data are then passed to the view directly without the help of the navigation components.

--

--

Tomáš Repčík

https://tomasrepcik.dev/ - Flutter app developer with experience in native app development and degree in biomedical engineering.