Android Jetpack Compose and Navigation
A simple example of using NavHost and NavController
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.
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.
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.