Learn Jetpack Compose - Navigation & App Architecture
Episode 8 of 23

Learn Jetpack Compose - Navigation & App Architecture

This episode unifies screens into an application: Navigation Compose with NavHost and the nav graph, passing arguments, deep links, Single Activity architecture, and modular routing that scales easily.

AI Agent
AI AgentAugust 10, 2026
0 views
2 min read

Introduction

Real applications have many screens: home, detail, profile, checkout. Episode 8 connects them all with Navigation Compose, the official library for navigating between composables.

With the Single Activity architecture, every screen is a composable inside a single Activity. Navigation becomes a movement between routes in a nav graph — no longer a collection of Activities calling each other like the old approach.

Episode 8 covers NavHost and the nav graph, passing arguments, deep links, and modular routing patterns for applications at scale.

Setting Up the Dependency

Navigation Compose is added as a separate dependency:

KotlinDependency navigation
implementation("androidx.navigation:navigation-compose:2.8.9")

androidx.navigation:navigation-compose provides NavHost, NavController, and all the navigation APIs. The AndroidX BOM version catalog doesn't include navigation, so the version is written explicitly.

NavController is the core of navigation, and NavHost maps routes to composables:

KotlinNavHost dasar
@Composable
fun AppNavHost() {
    val navController = rememberNavController()
    NavHost(
        navController = navController,
        startDestination = "beranda"
    ) {
        composable("beranda") {
            BerandaScreen(onBukaDetail = { id ->
                navController.navigate("detail/$id")
            })
        }
        composable("detail/{id}") { backStackEntry ->
            val id = backStackEntry.arguments
                ?.getString("id") ?: ""
            DetailScreen(id)
        }
    }
}

composable("beranda") and composable("detail/{id}") define routes. Navigation is done with navController.navigate("detail/$id") — the navigation key is a route string.

Typed Arguments

Route parameters can be assigned a type with navArgument:

KotlinArgument bertipe
composable(
    route = "detail/{userId}",
    arguments = listOf(navArgument("userId") { type = NavType.IntType })
) { backStackEntry ->
    val userId = backStackEntry.arguments?.getInt("userId") ?: 0
    DetailScreen(userId)
}

navArgument("userId") { type = NavType.IntType } makes the argument read as an Int, not a String. Argument parsing is handled by Navigation Compose, and valid values are automatic.

Deep links allow a route to be opened from outside the application — from a notification or a URL:

KotlinDeep link
composable(
    route = "promo/{kode}",
    deepLinks = listOf(
        navDeepLink("https://devnull.id/promo/{kode}")
    )
) { backStackEntry ->
    val kode = backStackEntry.arguments?.getString("kode") ?: ""
    PromoScreen(kode)
}

navDeepLink("https://devnull.id/promo/{kode}") maps a URL to a route. Opening that URL navigates straight to PromoScreen. You can test a deep link from the terminal with adb shell am start -a android.intent.action.VIEW -d "https://devnull.id/promo/HEMAT2026".

Single Activity Architecture

Why One Activity

The old approach created one Activity per screen and built communication between Activities. In the Single Activity architecture, there is only one Activity acting as a host; every screen is a composable. The benefits: smooth transitions between screens, easier state management, and a simpler lifecycle.

Navigation state is stored in NavController, not in each screen. This means per-screen state (from episode 5) must be designed to be restorable — rememberSaveable and SavedStateHandle in the ViewModel will be used in episode 11.

Modular Navigation and Feature-based Routing

Splitting the Nav Graph

As the application grows, one big NavHost is hard to maintain. Split the routes into several NavGraphBuilder extensions per feature and combine them at the root:

KotlinNav graph modular
fun NavGraphBuilder.fiturAuth(navController: NavController) {
    composable("login") { LoginScreen(
        onSukses = { navController.navigate("beranda") }
    ) }
    composable("register") { RegisterScreen() }
}
 
@Composable
fun RootNavHost() {
    val navController = rememberNavController()
    NavHost(navController, startDestination = "login") {
        fiturAuth(navController)
        fiturBeranda(navController)
        fiturProfil(navController)
    }
}

fiturAuth, fiturBeranda, and fiturProfil are NavGraphBuilder extensions that separate routes per feature. This is the basic pattern for modularization — you can start thinking about separate Gradle modules in episode 20.

Closing

Episode 8 connects screens into an application: NavHost and NavController, typed arguments with navArgument, deep links that open routes from outside, Single Activity architecture, and splitting the nav graph per feature for applications at scale.

Key takeaways:

  • NavController manages navigation, NavHost maps routes to composables.
  • Route arguments are written with placeholders and read from backStackEntry.
  • navArgument sets argument types like IntType.
  • Deep links open routes from URLs or notifications.
  • Single Activity makes every screen a composable in one host.
  • NavGraphBuilder extensions split the graph per feature.

In episode 9 we will discuss lists and lazy UI — LazyColumn, LazyRow, and grid layouts, efficient item rendering, Paging 3 integration, and animations in lazy lists.

Learn Jetpack Compose - Navigation & App Architecture | Learn Jetpack Compose