NavHost is a composable container that serves as the entry point for the navigation graph in Jetpack Compose. It links a NavController with a set of routes and renders the current screen based on the back stack state. According to Android Developers (2025), NavHost is a required component for any Compose app with navigation. Inside NavHost, composable routes are registered with optional arguments, deep links, and animations. Each route is a regular composable function that receives a NavBackStackEntry with transition data. NavHost automatically handles back press, state saving, and restoration on reconfiguration.
Key Takeaways
NavHost is a composable function that provides a container for displaying the current navigation screen. NavHost takes a NavController, startDestination, and a route graph built using Kotlin DSL. When the current route changes, NavHost switches the displayed composable with the specified animation.
NavHost works as a screen switcher: it tracks the current NavBackStackEntry from NavController and renders the corresponding composable block. Each screen is an independent composable function that receives a NavBackStackEntry with route arguments. All screens exist in a single composition tree, but NavHost shows only one at a time, hiding the others through animation.
Unlike FragmentManager, NavHost does not create a Fragment for each screen. The entire lifecycle is managed through CompositionLifecycle — composable functions do not have onStart/onResume, so LaunchedEffect and DisposableEffect are used for side effects. NavHost automatically subscribes to NavController and recomposes the UI when the route changes.
According to Google, NavHost has been a stable API since Navigation 2.4.0. Starting from 2.8.0, NavHost supports Type-Safe Navigation through Kotlin Serialization, replacing string routes with data classes. NavHost also supports nested graphs, enabling module-based navigation organization.
NavHost is created with two required parameters: navController (an instance of NavHostController) and startDestination (the route string of the first screen). The third parameter is a builder block where all routes are registered via composable(), navigation(), and dialog().
@Composable
fun AppNavHost(navController: NavHostController) {
NavHost(
navController = navController,
startDestination = "home"
) {
composable("home") { HomeScreen(navController) }
composable("settings") { SettingsScreen(navController) }
}
}
startDestination is the route that opens when NavHost is first launched. If the back stack is empty, NavHost automatically adds startDestination to the stack. On reconfiguration (screen rotation), NavHost restores the last route from savedState, not startDestination.
For BottomNavigation, startDestination is one of the bottom panel routes. The remaining panel routes are added as separate composable entries. NavHost should be placed inside Scaffold.content — where the main app content is displayed. NavHost takes up all available height minus the TopAppBar and BottomNavigation.
The composable(route, arguments, deepLinks, enterTransition, exitTransition, content) function registers a route in the NavHost graph. The route parameter is a string describing the path with optional placeholders in the form {paramName}. The placeholder is replaced with an actual value during navigation.
The content block of composable receives a NavBackStackEntry from which arguments are extracted. The screen composable function is rendered only when the current NavController route matches the route. On mismatch, the composable is removed from composition, but its state can be preserved through rememberSaveable or ViewModel with SavedStateHandle.
composable(
route = "article/{articleId}",
arguments = listOf(navArgument("articleId") {
type = NavType.IntType
defaultValue = 0
}),
deepLinks = listOf(navDeepLink { uriPattern = "https://app.example/article/{articleId}" })
) { backStackEntry ->
val articleId = backStackEntry.arguments?.getInt("articleId") ?: 0
ArticleScreen(articleId = articleId)
}
The number of composable entries inside NavHost can range from a few to hundreds. For large applications, routes are split across modules and connected via nested graphs. Each composable can have its own animation settings, deep links, and arguments.
Route arguments are defined through the arguments: List<NamedNavArgument> parameter in composable(). Each argument is defined via navArgument(name) { type; defaultValue }. NavType determines the argument type: StringType, IntType, LongType, FloatType, BoolType, ParcelableType, and ReferenceType.
| Route Parameter | Example route | NavType |
|---|---|---|
| Path | "user/{id}" | NavType.IntType |
| Query | "search?q={query}" | NavType.StringType |
| Optional | "details/{id}?tab={tab}" | StringType + defaultValue="" |
| Parcelable | "checkout/{order}" | NavType.ParcelableType |
Arguments are extracted from NavBackStackEntry via arguments?.getInt("id"). For required arguments, defaultValue can be omitted — NavType will use null. For optional arguments, defaultValue must be set, otherwise navigation will throw an exception if the parameter is missing.
Since Navigation 2.8.0, Type-Safe Navigation is recommended: define a sealed class or data class for routes with Kotlin Serialization. Instead of a string route, use composable<RouteType> { backStackEntry -> }. This eliminates typos in routes and automatically generates NavType for arguments. To migrate, add the navigation-compose-typesafe dependency and the Kotlin Serialization plugin.
nested graphs — a mechanism for grouping routes inside NavHost using the navigation(route, startDestination) function. A nested graph has its own route prefix and startDestination, and all its routes are accessible through the prefix. Nested graphs are used for modular architecture, where each feature module registers its own sub-graph.
Benefits of nested graphs: route isolation within a module, a unified back stack for a group of screens, and the ability to navigate by prefix without exposing the internal structure. For example, the “auth” graph contains “auth/login” and “auth/register”. Navigation is possible either by the full route or by prefix with a redirect to startDestination.
NavHost(navController = navController, startDestination = "main") {
composable("main") { MainScreen(navController) }
navigation(
route = "auth",
startDestination = "auth/login"
) {
composable("auth/login") { LoginScreen(navController) }
composable("auth/register") { RegisterScreen(navController) }
}
}
Nested graphs support argument passing at the graph level: parameters declared in the graph’s route are passed to all internal routes. To clear a nested graph, use popBackStack(route) — it will remove all internal entries. Nested graphs have no depth limit, but no more than 3 levels are recommended for readability.
NavHost supports transition animations between composable routes through the enterTransition, exitTransition, popEnterTransition and popExitTransition parameters. Animations are set once for NavHost and apply to all routes, or individually for each composable. By default, animations are disabled.
Typical configuration: enterTransition = slideInHorizontally(initialOffsetX = { it }) — the screen slides in from the right; exitTransition = slideOutHorizontally(targetOffsetX = { -it }) — the screen slides out to the left. For pop animation, directions are mirrored: the screen slides in from the left and slides out to the right. For BottomNavigation, fadeIn/fadeOut is used without slide.
NavHost(
navController = navController,
startDestination = "home",
enterTransition = { slideInHorizontally(initialOffsetX = { it }) + fadeIn() },
exitTransition = { slideOutHorizontally(targetOffsetX = { -it }) + fadeOut() },
popEnterTransition = { slideInHorizontally(initialOffsetX = { -it }) + fadeIn() },
popExitTransition = { slideOutHorizontally(targetOffsetX = { it }) + fadeOut() }
) { /* composable routes */ }
Custom animations are created using the Compose Animation API: AnimatedContentTransitionScope provides access to container dimensions, animation progress, and direction. For shared element transitions (one element smoothly moving to another screen), the Accompanist Navigation Animation library or a custom implementation via sharedElement Modifier is required. According to Android Developers (2025), the default slide animation (enter from the right, exit to the left) is used in 80% of Android apps with navigation.
Frequently Asked Questions
Technically yes, but it is not recommended. Each NavHost creates an independent back stack, breaking unified navigation. The exception is separate areas, such as a NavHost for main content and a NavHost for a BottomSheet with its own navigation.
NavHost is a navigation container that switches screens. Scaffold is the layout of the entire page (TopAppBar, BottomNavigation, FloatingActionButton). Typically, NavHost is placed inside Scaffold.content. Scaffold does not manage navigation, it only provides slots for UI components.
A ViewModel is created within a NavBackStackEntry through viewModel(). To share a ViewModel between screens, use parentNavController: bind the shared ViewModel to the parent entry. An alternative is DI (Hilt/Koin) with a NavGraph scope.
This is normal behavior — NavHost removes the composable from composition when leaving a route. To preserve state, use rememberSaveable for UI state and ViewModel with SavedStateHandle for business logic.
Add a final route composable("404") and navigate to it when an unknown deep link is received. NavHost does not have a catch-all route — check the route in the Deep Link intent handler before navigate(). If the route is not found, navigate to 404.
Summary
We will develop a mobile application turnkey
IT Sectr creates iOS and Android applications for startups and businesses since 2017. We will advise you and propose the best solution.
Read also