Tomáš Repčík - 3. 1. 2023

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 and INTRO_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.

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

Thanks for reading, and stay tuned for more!

Resources

1. developer.android.com

Subscribe for more
LinkedIn GitHub Medium Threads X Bluesky