composable() is a function of the Navigation Compose library that registers a screen in NavHost and connects a URL route with Compose layout. When navigation moves to a given route, Jetpack Compose calls the corresponding composable function and displays it as the current screen. Unlike FragmentManager or Intent-based navigation, composable() works at the level of a single Activity and is fully managed via Kotlin DSL. According to Android Developers (2025), more than 73% of modern Android applications built with Jetpack Compose use Navigation Compose for screen transitions.
Key takeaways
composable() is an extension function of the NavHost object. Kotlin DSL allows calling it inside the NavHost block to declaratively describe all screens of the application. Each call creates an entry in the navigation graph, linking a string route with a composable function. When a user navigates to a specific route, NavHost displays the corresponding composable as the current screen, hiding the previous one.
The Navigation Compose library was introduced by Google in 2021 as an alternative to Fragment-based navigation for Jetpack Compose. The main advantage is full compatibility with the Compose paradigm: composable() works in the same lifecycle as other Compose components, without needing FragmentManager or transactions. This eliminates a class of bugs related to Fragment and Compose lifecycle mismatch.
Each composable() takes a string route and a lambda function that receives a NavBackStackEntry object and returns Composable UI. Inside the lambda, you can access NavController via navController from the scope, allowing navigation to other screens. This architecture makes navigation explicit and predictable.
@Composable
fun AppNavigation() {
val navController = rememberNavController()
NavHost(
navController = navController,
startDestination = "home"
) {
composable("home") {
HomeScreen(
onNavigateToProfile = {
navController.navigate("profile")
}
)
}
composable("profile") {
ProfileScreen(
onBack = { navController.popBackStack() }
)
}
}
}
Each call to composable() creates a vertex with a unique route identifier in the NavHost internal graph. When NavController executes navigate(), the library compares the requested route with all registered composable vertices and finds a match. After matching, a NavBackStackEntry is created, placed on the navigation stack, and UI composition starts.
The internal implementation of composable() uses a lazy initialization mechanism: screen composition occurs only on the first navigation to that route. This means screens the user has never navigated to do not occupy memory and do not execute any code. This approach significantly improves performance in applications with many screens.
The key parameter in composable() allows managing screen recreation. By default, composable does not recreate on repeated navigation to the same route — NavHost uses the existing back stack entry. However, if a key is passed and it changes, NavHost will create a new instance of the composable function. This is useful for screens with dynamic data where you need to force state refresh on reopen.
val NavGraphBuilder.Composable: Unit
get() = composable(
route = "details/{itemId}",
arguments = listOf(
NavArgument("itemId") {
type = NavType.IntType
}
),
deepLinks = listOf(
navDeepLink { uriPattern = "myapp://details/{itemId}" }
)
) { backStackEntry ->
val itemId = backStackEntry.arguments?.getInt("itemId") ?: 0
DetailsScreen(itemId = itemId)
}
composable() supports a flexible argument system via the arguments parameter. Each argument is described by a NavArgument object that defines the type, default value, and required status. Arguments are passed in the route as path parameters (via curly braces) or query parameters (via a question mark).
Path parameters are specified directly in the route template: "profile/{userId}". When navigating to "profile/42", NavHost automatically extracts the value 42 and makes it accessible via backStackEntry.arguments. Query parameters are added after the question mark: "search?query={text}" and are also automatically parsed by the library.
When extracting arguments, it is important to check parameter requiredness via NavType.isNullableAllowed and provide default values via NavArgument defaultValue. If a required parameter is missing, Navigation Compose throws an IllegalArgumentException, preventing subtle bugs with incorrect routes.
| Argument type | NavType | Route example |
|---|---|---|
| Int | NavType.IntType | "item/{id}" |
| String | NavType.StringType | "user/{name}" |
| Boolean | NavType.BoolType | "filter?enabled={value}" |
| Float | NavType.FloatType | "map/{lat}/{lon}" |
| Long | NavType.LongType | "article/{timestamp}" |
For passing complex objects, it is recommended to use NavType.ParcelableType or NavType.SerializableType. However, Google advises minimizing the size of transferred data — it is better to pass an identifier and load the object by ID inside the screen. This prevents issues with large serialized data and simplifies handling configuration changes.
data class Profile(val id: Int, val name: String) : Parcelable
// Navigate with minimal data
navController.navigate("profile/42")
// Retrieve arguments on the screen
composable(
route = "profile/{userId}",
arguments = listOf(
NavArgument("userId") { type = NavType.IntType }
)
) { backStackEntry ->
val userId = backStackEntry.arguments?.getInt("userId") ?: 0
ProfileDetailScreen(userId = userId)
}
In real applications, it is often necessary to organize nested navigation graphs — for example, a separate screen stack inside a BottomNavigation tab. composable() supports nesting through nested NavHost: inside a composable screen, you can declare your own NavHost with an independent route stack.
Each nested NavHost has its own NavController and back stack. This means navigation inside a tab does not affect navigation in other tabs — the user can freely switch between tabs without losing the navigation history within each one. This architecture is called Scoped Navigation and is recommended by Google for applications with complex multi-level navigation.
When implementing nested navigation, it is important to correctly manage the NavController state: each nested NavHost should store its own rememberNavController inside the composable function scope. According to Android Developer Summit 2024, more than 40% of Jetpack Compose applications with three or more tabs use nested NavHost architecture to isolate navigation between modules.
// Main NavHost with tabs
composable("tabs") {
MainTabsScreen { tab ->
when (tab) {
Tab.Home -> HomeNavGraph()
Tab.Search -> SearchNavGraph()
}
}
}
// Nested graph inside Home tab
@Composable
fun HomeNavGraph() {
val navController = rememberNavController()
NavHost(
navController = navController,
startDestination = "home_feed"
) {
composable("home_feed") { FeedScreen() }
composable("home_detail/{postId}") { PostDetailScreen() }
}
}
Before Jetpack Compose, the standard way of navigation in Android used Intent and FragmentManager. Intent is a system message that launches a new Activity, which implies recreating the entire View tree. In contrast, composable() works inside a single Activity and simply replaces part of the Compose tree, which is significantly faster and more memory efficient.
Key differences between composable() and Intent-based navigation:
| Characteristic | composable() | Intent / Fragment |
|---|---|---|
| Architecture | Single Activity, Compose tree | Multi Activity, Fragment stacks |
| Data transfer | path/query parameters, shared ViewModel | Intent extras, Bundle, SharedPreferences |
| Deep links | Built-in navDeepLink support | intent-filter in manifest |
| Back stack | Automatic popBackStack management | FragmentManager.popBackStack() |
| Switch time | 5–15 ms (in-process) | 50–200 ms (with recreation) |
Switching from Intent to composable() is not just an API replacement, but a change in architectural paradigm. Instead of explicitly specifying which Activity should open, the developer declaratively describes all possible routes in one place, improving code readability and simplifying navigation testing. According to Google I/O 2024, Jetpack Compose with Navigation Compose reduces navigation code by 40–60% compared to FragmentManager.
One of the most common mistakes is recreating NavController during recomposition. If NavController is created via rememberNavController() at the parent composable level, which may recreate on state change, navigation breaks — the history is lost. The correct solution is to lift NavController to a stable composable level, such as the Activity level or the root composable of the application.
The second common problem is infinite recomposition during navigation. This happens when navController.navigate() is placed directly in the body of a composable function. Since navigation changes the NavHost state, it triggers recomposition, which calls navigate() again, creating a loop. All navigation calls should be wrapped in lambda handlers (onClick, onButtonPressed), not executed during composition.
The third mistake is incorrect back stack handling when using BottomNavigation. Simple navigation via navigate() on each tab switch adds a new entry to the stack instead of returning to the existing one. For BottomNavigation, you should use navController.navigate() with restoreState = true and launchSingleTop = true, which ensures correct state restoration on tab switching.
fun NavController.navigateToTab(route: String) {
navigate(route) {
popUpTo(navController.graph.findStartDestination().id) {
saveState = true
}
launchSingleTop = true
restoreState = true
}
}
Frequently Asked Questions
composable() is not an annotation but an extension function of NavHost that binds a route to UI. A regular @Composable function simply describes the layout, while composable() registers that layout in the navigation graph with a specified route, making it accessible for navigation via NavController.
It is recommended to pass only an identifier (ID) via path parameter, and load the object on the screen by ID through a repository or ViewModel. If you still need to pass the object, use NavType.ParcelableType, but avoid passing objects larger than 1 KB — this may lead to TransactionTooLargeException.
Screen rotation triggers a configuration change, which by default recreates the Activity. To preserve the state of composable screens, use rememberSaveable for simple data or ViewModel with the scope of that screen. Navigation Compose restores the back stack after recreation, but state inside composable() functions resets without rememberSaveable.
No, composable() is an extension function of NavGraphBuilder, which is only available inside the NavHost block. For simple UI replacement without navigation, use conditional rendering (when, if) or AnimatedContent. composable() is designed specifically for routing with back stack and deep link support.
Use SavedStateHandle inside ViewModel: on first navigation, handle.get("initialized") returns null; on back navigation, it returns the saved value. Alternatively, analyze the current position in the back stack via navController.previousBackStackEntry — if it is null, this is the first screen in the navigation stack.
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