composable(): what it is, NavHost and routing in Jetpack Compose

Author: IT Sectr Published: 2026-06-30 Reading time: 9 min

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 a function for registering a screen in NavHost of the Navigation Compose library.
  • Route — each screen is identified by a string route passed as the first argument.
  • Parameters — composable() supports arguments via NavArgument, including required and optional ones.
  • Nesting — nested navigation is supported through nested NavHost with separate route graphs.
  • Performance — composable() uses lazy initialization: the screen is created only on first navigation.

What is composable() in NavHost

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.

kotlin
@Composable
fun AppNavigation() {
    val navController = rememberNavController()
    
    NavHost(
        navController = navController,
        startDestination = "home"
    ) {
        composable("home") {
            HomeScreen(
                onNavigateToProfile = {
                    navController.navigate("profile")
                }
            )
        }
        composable("profile") {
            ProfileScreen(
                onBack = { navController.popBackStack() }
            )
        }
    }
}

How composable() works: keys and parameters

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.

kotlin
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)
    }

Passing arguments via composable()

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 typeNavTypeRoute example
IntNavType.IntType"item/{id}"
StringNavType.StringType"user/{name}"
BooleanNavType.BoolType"filter?enabled={value}"
FloatNavType.FloatType"map/{lat}/{lon}"
LongNavType.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.

kotlin
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)
}

Nested navigation with composable()

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.

kotlin
// 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() }
    }
}

composable() vs Intent navigation

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:

  • Speed — composable() switches screens in milliseconds without recreating the Activity; Intent requires restarting the Activity.
  • Animations — in Navigation Compose, transition animations are defined declaratively via AnimatedNavHost, without needing overridePendingTransition.
  • Shared state — composable() works in a shared ViewModel scope, simplifying data transfer between screens without Intent extras.
Characteristiccomposable()Intent / Fragment
ArchitectureSingle Activity, Compose treeMulti Activity, Fragment stacks
Data transferpath/query parameters, shared ViewModelIntent extras, Bundle, SharedPreferences
Deep linksBuilt-in navDeepLink supportintent-filter in manifest
Back stackAutomatic popBackStack managementFragmentManager.popBackStack()
Switch time5–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.

Common mistakes with composable()

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.

kotlin
fun NavController.navigateToTab(route: String) {
    navigate(route) {
        popUpTo(navController.graph.findStartDestination().id) {
            saveState = true
        }
        launchSingleTop = true
        restoreState = true
    }
}

Frequently Asked Questions

What is the difference between composable() and a regular @Composable function?

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.

How to pass a complex object between composable() screens?

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.

Why does the composable() screen recreate on screen rotation?

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.

Can composable() be used without NavHost?

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.

How to distinguish a first navigation from a back navigation in composable()?

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

  • composable() is a screen registration function in NavHost, the main way to organize navigation in Jetpack Compose.
  • Routes — each screen is identified by a route string with optional path and query parameters.
  • Arguments — passed via NavArgument with support for primitive, Parcelable, and Serializable types.
  • Nesting — composable() supports nested NavHost for organizing modular navigation with independent stacks.
  • Performance — lazy screen initialization saves memory, screen switching speed is 5–15 ms.
  • Mistakes — main issues: NavController recreation, infinite recomposition with navigate() in composable body, incorrect BottomNavigation handling.
  • Migration — switching from FragmentManager to composable() reduces navigation code volume by 40–60% and eliminates a class of bugs related to Fragment lifecycle.

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.

Discuss the project

Read also