NavHost Configuration in Jetpack Compose: A Comprehensive Guide

Jetpack Compose is Google’s modern toolkit for building native Android UI. It simplifies UI development by using a declarative approach, making code more readable and maintainable. Navigation is a core component of any app, and Jetpack Compose provides a powerful navigation library to manage the app’s flow. Configuring a NavHost is essential for setting up navigation within your Compose application.

What is NavHost in Jetpack Compose?

The NavHost is a Compose component that displays the current destination of a navigation graph. It links the navigation graph with the composable destinations, allowing users to navigate between different screens in your app. The NavHost utilizes a NavController to manage navigation actions.

Why Configure NavHost?

  • Centralized Navigation: Manages all destinations in one place.
  • Transitions and Animations: Supports seamless transitions between screens.
  • State Management: Integrates with ViewModel for state persistence across navigation events.
  • Deep Linking: Enables navigation to specific screens via deep links.

How to Configure NavHost in Jetpack Compose

Follow these steps to set up a NavHost in your Jetpack Compose application:

Step 1: Add Dependencies

Make sure you have the required dependencies in your build.gradle file:

dependencies {
    implementation("androidx.core:core-ktx:1.9.0")
    implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.6.1")
    implementation("androidx.activity:activity-compose:1.8.2")
    implementation(platform("androidx.compose:compose-bom:2023.03.00"))
    implementation("androidx.compose.ui:ui")
    implementation("androidx.compose.ui:ui-graphics")
    implementation("androidx.compose.ui:ui-tooling-preview")
    implementation("androidx.compose.material3:material3")
    testImplementation("junit:junit:4.13.2")
    androidTestImplementation("androidx.test.ext:junit:1.1.5")
    androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
    androidTestImplementation(platform("androidx.compose:compose-bom:2023.03.00"))
    androidTestImplementation("androidx.compose.ui:ui-test-junit4")
    debugImplementation("androidx.compose.ui:ui-tooling")
    debugImplementation("androidx.compose.ui:ui-test-manifest")

    // Navigation Compose
    implementation("androidx.navigation:navigation-compose:2.7.7")
}

Step 2: Define Navigation Destinations

Create sealed classes or objects representing your app’s navigation destinations:

sealed class Screen(val route: String) {
    object Home : Screen("home")
    object Details : Screen("details/{itemId}") {
        fun createRoute(itemId: Int): String = "details/$itemId"
    }
}

Step 3: Create a NavController

Initialize a NavController using rememberNavController() inside your composable function:

import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.navigation.NavController
import androidx.navigation.compose.rememberNavController

@Composable
fun MyApp() {
    val navController: NavController = rememberNavController()
    // ...
}

Step 4: Set up NavHost

Configure the NavHost using the NavController and a start destination. Define the composable content for each route within the NavHost:

import androidx.compose.material3.Text
import androidx.compose.runtime.LaunchedEffect
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.NavType
import androidx.navigation.navArgument
import androidx.compose.ui.tooling.preview.Preview

@Composable
fun MyApp() {
    val navController: NavController = rememberNavController()

    NavHost(navController = navController, startDestination = Screen.Home.route) {
        composable(Screen.Home.route) {
            HomeScreen(navController = navController)
        }
        composable(
            route = Screen.Details.route,
            arguments = listOf(navArgument("itemId") { type = NavType.IntType })
        ) { entry ->
            val itemId = entry.arguments?.getInt("itemId")
            itemId?.let {
                DetailsScreen(itemId = it)
            }
        }
    }
}

@Composable
fun HomeScreen(navController: NavController) {
    androidx.compose.material3.Button(onClick = { navController.navigate(Screen.Details.createRoute(123)) }) {
        Text("Go to Details")
    }
}

@Composable
fun DetailsScreen(itemId: Int) {
    Text("Details Screen for item ID: $itemId")
}

@Preview(showBackground = true)
@Composable
fun DefaultPreview() {
    MyApp()
}

In this example:

  • We define two screens: Home and Details.
  • The Details screen includes a parameter itemId.
  • The NavHost maps each route to a composable function.
  • We use navArgument to define the arguments passed to the Details screen.

Step 5: Navigate Between Screens

Use navController.navigate() to navigate between different destinations:


// Navigate to the Details screen with itemId = 123
navController.navigate(Screen.Details.createRoute(123))

Passing Data Between Screens

You can pass data between screens by including arguments in the route definition:


// Define the route with a parameter
object Details : Screen("details/{itemId}") {
    fun createRoute(itemId: Int): String = "details/$itemId"
}

// Navigate with the parameter
navController.navigate(Screen.Details.createRoute(123))

// Retrieve the parameter in the Details screen
composable(
    route = Screen.Details.route,
    arguments = listOf(navArgument("itemId") { type = NavType.IntType })
) { entry ->
    val itemId = entry.arguments?.getInt("itemId")
    // ...
}

Advanced Configuration

Animated Transitions

You can add animated transitions between screens using AnimatedContent. To use AnimatedContent you need to use androidx.compose.animation:animation-core.

 implementation("androidx.compose.animation:animation-core:1.6.1")
import androidx.compose.animation.*
import androidx.compose.animation.core.*
import androidx.compose.material.Button
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.navigation.NavController
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController

sealed class Screen(val route: String) {
    object Home : Screen("home")
    object Details : Screen("details")
}

@Composable
fun AnimatedNavHost() {
    val navController = rememberNavController()

    NavHost(navController = navController, startDestination = Screen.Home.route) {
        composable(
            Screen.Home.route,
            enterTransition = {
                slideInHorizontally(
                    initialOffsetX = { 300 },
                    animationSpec = tween(durationMillis = 300, easing = FastOutSlowInEasing)
                ) + fadeIn(animationSpec = tween(durationMillis = 300))
            },
            exitTransition = {
                slideOutHorizontally(
                    targetOffsetX = { -300 },
                    animationSpec = tween(durationMillis = 300, easing = FastOutSlowInEasing)
                ) + fadeOut(animationSpec = tween(durationMillis = 300))
            }
        ) {
            HomeScreen(navController = navController)
        }

        composable(
            Screen.Details.route,
            enterTransition = {
                slideInHorizontally(
                    initialOffsetX = { 300 },
                    animationSpec = tween(durationMillis = 300, easing = FastOutSlowInEasing)
                ) + fadeIn(animationSpec = tween(durationMillis = 300))
            },
            exitTransition = {
                slideOutHorizontally(
                    targetOffsetX = { -300 },
                    animationSpec = tween(durationMillis = 300, easing = FastOutSlowInEasing)
                ) + fadeOut(animationSpec = tween(durationMillis = 300))
            }
        ) {
            DetailsScreen()
        }
    }
}

@Composable
fun HomeScreen(navController: NavController) {
    Button(onClick = {
        navController.navigate(Screen.Details.route)
    }) {
        Text(text = "Go to Details")
    }
}

@Composable
fun DetailsScreen() {
    Text(text = "Details Screen")
}

Navigation with Bottom Navigation

Implementing a bottom navigation bar can significantly improve user experience. Here’s how to configure it:


import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Home
import androidx.compose.material.icons.filled.Settings
import androidx.compose.runtime.getValue
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.navigation.NavController
import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.navigation.compose.rememberNavController
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.compose.ui.tooling.preview.Preview

sealed class BottomNavItem(val route: String, val icon: ImageVector, val label: String) {
    object Home : BottomNavItem("home", Icons.Filled.Home, "Home")
    object Settings : BottomNavItem("settings", Icons.Filled.Settings, "Settings")
}

@Composable
fun BottomNavigationApp() {
    val navController = rememberNavController()
    Scaffold(
        bottomBar = { BottomNavigationBar(navController = navController) }
    ) {
        NavigationGraph(navController = navController)
    }
}

@Composable
fun BottomNavigationBar(navController: NavController) {
    val items = listOf(
        BottomNavItem.Home,
        BottomNavItem.Settings
    )
    BottomNavigation {
        val navBackStackEntry by navController.currentBackStackEntryAsState()
        val currentRoute = navBackStackEntry?.destination?.route
        items.forEach { item ->
            BottomNavigationItem(
                icon = { Icon(item.icon, contentDescription = item.label) },
                label = { Text(text = item.label) },
                selected = currentRoute == item.route,
                onClick = {
                    navController.navigate(item.route) {
                        navController.graph?.startDestinationRoute?.let { route ->
                            popUpTo(route) {
                                saveState = true
                            }
                        }
                        launchSingleTop = true
                        restoreState = true
                    }
                }
            )
        }
    }
}

@Composable
fun NavigationGraph(navController: NavController) {
    NavHost(navController = navController, startDestination = BottomNavItem.Home.route) {
        composable(BottomNavItem.Home.route) {
            HomeScreen(navController = navController)
        }
        composable(BottomNavItem.Settings.route) {
            SettingsScreen()
        }
    }
}

@Composable
fun HomeScreen(navController: NavController) {
    Text(text = "Home Screen")
}

@Composable
fun SettingsScreen() {
    Text(text = "Settings Screen")
}

@Preview(showBackground = true)
@Composable
fun BottomNavigationAppPreview() {
    BottomNavigationApp()
}

Conclusion

Configuring a NavHost in Jetpack Compose is crucial for building robust and navigable Android applications. By defining destinations, setting up the NavController, and using navigation actions, you can create seamless transitions and manage your app’s flow efficiently. Additionally, advanced features such as animated transitions and bottom navigation can enhance the user experience and make your app more engaging.