Android Jetpack Compose and Nesting Navigation
Putting composables into an appropriate structure
Photo by Marc Sendra Martorell on Unsplash
In the previous article, the basics of navigation with NavHost
and NavController
have been described. However, with more complex apps, the app’s navigation graph could get messy and lead to redundant mistakes in code. The app structure can make it much more testable, scalable, and easier to fix. Go here for the basic setup.
The most straightforward approach would be to put all the screens into one NavHost
. But we can do better with the nesting of the graphs.
As it goes in programming, we want to separate our logic. The multiple paths can be grouped into graphs like Introduction, Main Screen, or others. So, let’s take a look at it.
The targeted composition of the navigation
The demonstration will be done on a simple introduction workflow and main home screen, which can go to other screens. Firstly, we want to introduce the app, motivate the user and give them some recommendations for usage. Afterward, the user can use the app in the main navigation.
The split of the navigation can be done like this:
Screen-by-screen representation
Creating the Graphs
The graph is created with the NavGraphBuilder
, which can declare composables
and separate them into separate files. Moreover, it can be named in any way you want. The builder wraps the navigation component with its destinations.
Route is unique path to the screen/
composable
. Every screen/composable
has its route. However, the destination can appear in the documentation or as parameter name. In other words, every screen is destination to which we need to take some unique route. The same goes for the sole graphs, where every graph has its own route. That is why we define, for example,INTRO_ROUTE
to graph andINTRO_WELCOME_SCREEN
to specific screen/composable
.
fun NavGraphBuilder.introGraph(navController: NavController) {
navigation(startDestination = IntroNav.INTRO_WELCOME_SCREEN, route = IntroNav.INTRO_ROUTE) {
composable(IntroNav.INTRO_WELCOME_SCREEN){
WelcomeScreen(navController)
}
composable(IntroNav.INTRO_MOTIVATION_SCREEN){
MotivationScreen(navController)
}
composable(IntroNav.INTRO_RECOMMENDATION_SCREEN){
RecommendationScreen(navController)
}
}
}
object IntroNav {
const val INTRO_ROUTE = "intro"
const val INTRO_WELCOME_SCREEN = "welcome"
const val INTRO_MOTIVATION_SCREEN = "motivation"
const val INTRO_RECOMMENDATION_SCREEN = "recommendation"
}
fun NavGraphBuilder.mainGraph(navController: NavController) {
navigation(startDestination = MainNav.MAIN_HOME_SCREEN, route = MainNav.MAIN_ROUTE) {
composable(MainNav.MAIN_HOME_SCREEN){
HomeScreen(navController)
}
composable(MainNav.MAIN_SETTINGS_SCREEN){
SettingsScreen()
}
composable(MainNav.MAIN_ABOUT_SCREEN){
AboutScreen()
}
}
}
object MainNav {
const val MAIN_ROUTE = "main"
const val MAIN_HOME_SCREEN = "home_screen"
const val MAIN_SETTINGS_SCREEN = "settings"
const val MAIN_ABOUT_SCREEN = "about_screen"
}
With graphs created, we can declare them in the root activity of the app. The graphs are wrapped in a single NavHost
. The navController
is initialized at the root and passed to every graph so it is shared among them. Every screen should have access to the navController
too.
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
val navController = rememberNavController()
ExampleAppTheme {
NavHost(navController, startDestination = IntroNav.INTRO_ROUTE) {
introGraph(navController)
mainGraph(navController)
}
}
}
}
}
MainActivity, which includes navigation graphs.
From now on, all the screens will contain only two buttons to move forward and go backward for simplicity. The only thing that will change is the onClick
action of the buttons. For example:
@Composable
fun MotivationScreen(navController: NavController) {
Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Text("Motivation")
Button(onClick = {
navController.navigate(IntroNav.INTRO_RECOMMENDATION_SCREEN)
}) {
Text("Go to recommendations")
}
Button(onClick = {
navController.popBackStack()
}) {
Text("Go Back")
}
}
}
@Preview
@Composable
fun MotivationPreview() {
val navController = rememberNavController()
MotivationScreen(navController = navController)
}
Screen example where we will change only onClick methods.
Navigation Among the Screens
Now, the screens and graphs are implemented. The last missing piece is navigation from one screen to another. The example will follow the user flow.
Moving through the intro to the main home screen
The start destination is marked as the intro to the app, so the intro graph is picked as starting destination. If a user wants to move out of the current welcome screen, standard .navigate()
and .popStack()
are enough to use.
navController.navigate(IntroNav.INTRO_MOTIVATION_SCREEN)
// or to go back
navController.popBackStack()
Navigating through the intro, the stack fills with all the screens. If we moved to another graph and clicked the back button, we would still get to the previous screen. Navigating to the home screen, the stack needs to be cleaned from the intro screen. Here comes the popUpTo()
function and NavOptionsBuilder
. The builder, a parameter of the navigate function, can pop the stack till the beginning of the graph. To remove one graph and navigate to another:
// moving to the main graph route
navController.navigate(MainNav.MAIN_ROUTE) {
// removing everything reagrding intro (incliding the intro route itself)
popUpTo(IntroNav.INTRO_ROUTE)
}
Example of moving around the app.
It does not mean you cannot nest two graphs into each other. Feel free to create another graph and navigate to it without popUpTo()
. The back button will work in the same manner as you anticipate.
Advice
- Spend some time and think about the user flow in the app
- Try to make it easier for you. The navigation will be easier for your end user too.
- Spend some time with your actual perspective/actual user, and you will be surprised sometimes
Thanks for reading, and stay tuned for more!