← Back to list

Navigation & Route

Compose : Navigation & Route with BottomNavigati and NavigationDrawer

Abhishek Rai · 2026-02-16 11:06 · 0 claps · 8.5 min read
#android-jetpack-compose #navigation #dagger-hilt #detect #ci-cd-pipeline
Open on Medium ↗
Wiki topics: ☁️ · DevOps & Cloud

Navigation & Route

Compose : Navigation & Route with BottomNavigati and NavigationDrawer

Source: https://blog.kotlin-academy.com/

Source: https://blog.kotlin-academy.com/

  1. What is Nav-Host
  2. What is Nav-Graphs
  3. What is Nav-Controller
  4. What is Nested Nav-Graphs
  5. What is composable in Navigation
  6. What is Route
  7. What is NavigationItems
  8. How to pass Arguments / data in between Navigation
  9. How to Navigate to Others Compose Screen
  10. How to Manage Back Stack / backStackEntry in Navigation

NavHost

Definition: This is a unique composable that you can include in your layout. It shows various destinations from your Navigation Graph. The NavHost links the NavController with a navigation graph that specifies the composable destinations that you should be able to navigate between. As you navigate between composables, the content of the NavHost is automatically recomposed. Each composable destination in your navigation graph is associated with a route.

NavHost is a composable designed to hold our layouts. It holds layouts in stack. As we navigate through composable, the content within the NavHost changes. Each screen in the navigation has its route:

A route is a string that defines the path to your composable. You can think of it as a key that corresponds to a specific destination.

Each destination has a unique route.

  • The NavHost is the container that displays the current destination based on the NavController.
  • It works like a “stage” where Composables are shown depending on navigation state.

/* 
    navController - the navController for this host
    startDestination - the route for the start destination
    modifier - The modifier to be applied to the layout.
    route - the route for the graph
    builder - the builder used to construct the graph
*/ 
@Composable
public fun NavHost(
    navController: NavHostController,
    startDestination: String,
    modifier: Modifier = Modifier,
    route: String? = null,
    builder: NavGraphBuilder.() -> Unit
) {
    NavHost(
        navController,
        remember(route, startDestination, builder) {
            navController.createGraph(startDestination, route, builder)
        },
        modifier
    )
}

//Example
val navController = rememberNavController()

NavHost(
    navController = navController,
    startDestination = "home",
    modifier = Modifier.padding(innerPadding)
) {
    composable("home") { HomeScreen(navController) }
    composable("details") { DetailsScreen(navController) }
}

NavController

Definition: NavController manages the stack & back stack of composables, representing the screens in our app and their respective states.

Each [NavController](https://developer.android.com/reference/androidx/navigation/NavController) must be associated with a single [NavHost](https://developer.android.com/reference/kotlin/androidx/navigation/compose/package-summary#NavHost(androidx.navigation.NavHostController,kotlin.String,androidx.compose.ui.Modifier,kotlin.String,kotlin.Function1)) composable. The NavHost links the NavController with a navigation graph that specifies the composable destinations that you should be able to navigate between.

  • The NavController is the central API for navigation and back stack management in Jetpack Compose.
  • It’s responsible for: . Keeping track of the current screen (destination). . Managing the back stack (history of screens). . Executing navigation actions (e.g., navigate(), popBackStack()).
val navController = rememberNavController()

navController.navigate("details")

NavGraph

Definition:

  • A graph of all possible destinations (routes) in your app and how they connect.
  • This is a resource that collects all navigation-related data in one place. This includes all of the locations in your app, referred to as destinations, as well as the possible paths a user could take through your app. It’s like a big book that has all the places you can go in an app and how you can move between them. Think of it as a map and a guide combined.
  • It defines: . startDestination (entry screen). . destinations (composables). . relationships (navigation actions).
NavHost(navController, startDestination = "home") {
    composable("home") { HomeScreen(navController) }
    composable("profile") { ProfileScreen(navController) }
}

Nested NavGraphs

Definition:

  • A NavGraph inside another NavGraph, used to organize and modularize navigation.
  • Useful for flows like authentication, onboarding, or feature modules
NavHost(navController, startDestination = "auth") {
    navigation(startDestination = "login", route = "auth") {
        composable("login") { LoginScreen(navController) }
        composable("signup") { SignupScreen(navController) }
    }
    composable("home") { HomeScreen(navController) }
}

Composable in Navigation

Definition:

  • Each destination in the navigation graph is represented by a composable {} block.
  • A composable is linked to a route and displays a UI screen.
composable("login") { LoginScreen(navController) }

Route

Definition:

  • A unique string identifier for a destination.
  • Think of it like a screen ID or URL.
  • Can be plain ("home") or parameterized ("details/{id}").
object Routes {
    const val HOME = "home"
    const val DETAILS = "details/{itemId}"
}

NavigationItems

Definition:

  • A data structure (often sealed class or object) used to organize routes + metadata (label, icon).
  • Commonly used for BottomNavigation or DrawerNavigation.
sealed class NavigationItem(val route: String, val label: String, val icon: Int) {
    object Home : NavigationItem("home", "Home", R.drawable.ic_home)
    object Profile : NavigationItem("profile", "Profile", R.drawable.ic_profile)
}

Navigating to Other Screens

Definition:

  • The process of moving from one destination to another using the NavController.
navController.navigate("profile")

navController.navigate("home") {
    launchSingleTop = true   // Avoid duplicates
    restoreState = true      // Restore previous state if exists
}

Back Stack & BackStackEntry

Definition:

  • The back stack is a history of screens (like a browser’s history).
  • A BackStackEntry is one record in the back stack that holds: . Route name. . Arguments. . Saved state.
// Get current screen
val currentBackStackEntry = navController.currentBackStackEntryAsState()
val currentRoute = currentBackStackEntry.value?.destination?.route

//Managing Back Stack:
navController.popBackStack()  // Go back one screen

navController.navigate("home") {
    popUpTo("login") { inclusive = true }  // Clear everything up to login
}

Passing Arguments / Data in Compose Navigation

Route Parameters (Simple, URL-style)

Arguments are part of the route string, just like query params in a URL.

navController.navigate("details/101")

NavHost(navController, startDestination = "home") {
    composable("home") { HomeScreen(navController) }
    composable("details/{itemId}") { backStackEntry ->
        val itemId = backStackEntry.arguments?.getString("itemId")
        DetailsScreen(itemId)
    }
}

Typed Arguments with navArgument

Compose Navigation supports strong typing by declaring arguments explicitly.

navController.navigate("details/42")

NavHost(navController, startDestination = "home") {
    composable(
        route = "details/{itemId}",
        arguments = listOf(
            navArgument("itemId") { type = NavType.IntType }
        )
    ) { backStackEntry ->
        val id = backStackEntry.arguments?.getInt("itemId")
        DetailsScreen(id)
    }
}

Query Parameters (Optional Arguments)

Arguments are passed like query params in a route.

composable(
    route = "profile?userId={userId}&isPremium={isPremium}",
    arguments = listOf(
        navArgument("userId") { type = NavType.StringType; defaultValue = "guest" },
        navArgument("isPremium") { type = NavType.BoolType; defaultValue = false }
    )
) { entry ->
    val userId = entry.arguments?.getString("userId")
    val isPremium = entry.arguments?.getBoolean("isPremium")
    ProfileScreen(userId, isPremium)
}

Supported Types:

  • NavType.StringType
  • NavType.IntType
  • NavType.BoolType
  • NavType.FloatType
  • NavType.LongType
  • NavType.EnumType(MyEnum::class.java)
  • Object Class / JSON

Complete Example

📂 Project Structure

Awesome 🚀 Let’s put everything together into one consistent project example that you can directly use to:

  • Build a simple Compose Navigation app
  • Run Unit tests (for NavGraph logic)
  • Run UI tests (for user flows)
  • Run Screenshot tests (using Paparazzi)

Now I’ll give you a minimal but 100% runnable project with:

  • ✅ Clean-ish architecture (simple structure)
  • ✅ MVI
  • ✅ Hilt
  • ✅ Compose
  • ✅ Unit test
  • ✅ UI test
  • ✅ Detekt
  • ✅ Lint
  • ✅ GitHub CI
  • ✅ All required config
app/src/main/java/com/example/app/
 ├── MyApp.kt
 ├── MainActivity.kt
 ├── di/AppModule.kt
 ├── navigation/
 │     ├── NavGraph.kt
 │     └── Routes.kt
 ├── presentation/
 │     ├── home/
 │     │    └── HomeScreen.kt
 │     ├── profile/
 │     │    ├── ProfileScreen.kt
 │     │    ├── ProfileViewModel.kt
 │     │    └── ProfileState.kt
 │     └── settings/
 │          └── SettingsScreen.kt
 ├── domain/
 │     ├── User.kt
 │     ├── UserRepository.kt
 │     └── GetUserUseCase.kt
 └── data/
       ├── UserApi.kt
       ├── UserRepositoryImpl.kt
       └── UserDto.kt

app/
 ├── src/main/java/com/example/app/
 │   ├── MyApp.kt
 │   ├── MainActivity.kt
 │   ├── di/AppModule.kt
 │   ├── data/
 │   │     ├── UserApi.kt
 │   │     ├── UserRepositoryImpl.kt
 │   │     └── UserDto.kt
 │   ├── domain/
 │   │     ├── User.kt
 │   │     ├── UserRepository.kt
 │   │     └── GetUserUseCase.kt
 ├── navigation/
 │     ├── NavGraph.kt
 │     └── Routes.kt
 ├── presentation/
 │     ├── home/
 │     │    └── HomeScreen.kt
 │     ├── profile/
 │     │    ├── ProfileScreen.kt
 │     │    ├── ProfileViewModel.kt
 │     │    └── ProfileState.kt
 │     └── settings/
 │          └── SettingsScreen.kt
 │         ├── UserIntent.kt
 │         ├── UserState.kt
 │         ├── UserViewModel.kt
 │         └── UserScreen.kt
 │
 ├── src/test/java/com/example/app/
 │   ├── MainDispatcherRule.kt
 │   └── UserViewModelTest.kt
 │
 ├── src/androidTest/java/com/example/app/
 │   └── UserScreenTest.kt
 │
.github/workflows/android-ci.yml
config/detekt/detekt.yml

app/ ├── src/main/java/com/example/app/ │ ├── MyApp.kt │ ├── MainActivity.kt │ ├── di/AppModule.kt │ ├── data/ │ │ ├── UserApi.kt │ │ ├── UserRepositoryImpl.kt │ │ └── UserDto.kt │ ├── domain/ │ │ ├── User.kt │ │ ├── UserRepository.kt │ │ └── GetUserUseCase.kt │ └── presentation/ │ ├── UserIntent.kt │ ├── UserState.kt │ ├── UserViewModel.kt │ └── UserScreen.kt │ ├── src/test/java/com/example/app/ │ ├── MainDispatcherRule.kt │ └── UserViewModelTest.kt │ ├── src/androidTest/java/com/example/app/ │ └── UserScreenTest.kt │ .github/workflows/android-ci.yml config/detekt/detekt.yml

✅ 1️⃣ PROJECT build.gradle (root)

buildscript {
    dependencies {
        classpath("com.google.dagger:hilt-android-gradle-plugin:2.50")
    }
}

plugins {
    id("io.gitlab.arturbosch.detekt") version "1.23.5"
}

✅ 2️⃣ app/build.gradle

plugins {
    id("com.android.application")
    id("org.jetbrains.kotlin.android")
    id("kotlin-kapt")
    id("dagger.hilt.android.plugin")
}

android {
    namespace = "com.example.app"
    compileSdk = 34

    defaultConfig {
        applicationId = "com.example.app"
        minSdk = 24
        targetSdk = 34
        testInstrumentationRunner =
            "androidx.test.runner.AndroidJUnitRunner"
    }

    buildFeatures { compose = true }

    composeOptions {
        kotlinCompilerExtensionVersion = "1.5.8"
    }

    lint {
        abortOnError = true
    }
}

dependencies {

    // Compose
    implementation("androidx.activity:activity-compose:1.8.2")
    implementation("androidx.compose.ui:ui:1.6.1")
    implementation("androidx.compose.material3:material3:1.2.1")
    implementation("androidx.lifecycle:lifecycle-runtime-compose:2.7.0")

    // Hilt
    implementation("com.google.dagger:hilt-android:2.50")
    kapt("com.google.dagger:hilt-compiler:2.50")
    implementation("androidx.hilt:hilt-navigation-compose:1.2.0")

    // Coroutines
    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3")

    // Unit test
    testImplementation("junit:junit:4.13.2")
    testImplementation("io.mockk:mockk:1.13.10")
    testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.3")
    testImplementation("app.cash.turbine:turbine:1.0.0")

    // UI test
    androidTestImplementation("androidx.compose.ui:ui-test-junit4:1.6.1")
    debugImplementation("androidx.compose.ui:ui-test-manifest:1.6.1")
}

✅ 3️⃣ DOMAIN

User.kt

data class User(val id: String, val name: String)

UserRepository.kt

interface UserRepository {
    suspend fun getUser(): User
}

GetUserUseCase.kt

class GetUserUseCase(
    private val repository: UserRepository
) {
    suspend operator fun invoke() = repository.getUser()
}

✅ 4️⃣ DATA

UserDto.kt

data class UserDto(val id: String, val name: String)

UserApi.kt

interface UserApi {
    suspend fun getUser(): UserDto
}

class FakeUserApi : UserApi {
    override suspend fun getUser() =
        UserDto("1", "John")
}

UserRepositoryImpl.kt

class UserRepositoryImpl(
    private val api: UserApi
) : UserRepository {
    override suspend fun getUser() =
        api.getUser().let { User(it.id, it.name) }
}

✅ 5️⃣ HILT MODULE

AppModule.kt

@Module
@InstallIn(SingletonComponent::class)
object AppModule {

    @Provides
    fun provideApi(): UserApi = FakeUserApi()

    @Provides
    fun provideRepository(
        api: UserApi
    ): UserRepository = UserRepositoryImpl(api)

    @Provides
    fun provideUseCase(
        repo: UserRepository
    ) = GetUserUseCase(repo)
}

✅ 6️⃣ MVI

UserIntent.kt

sealed interface UserIntent {
    data object Load : UserIntent
}

UserState.kt

data class UserState(
    val loading: Boolean = false,
    val user: User? = null,
    val error: String? = null
)

UserViewModel.kt

@HiltViewModel
class UserViewModel @Inject constructor(
    private val getUser: GetUserUseCase
) : ViewModel() {

    private val _state = MutableStateFlow(UserState())
    val state: StateFlow<UserState> = _state

    fun process(intent: UserIntent) {
        if (intent is UserIntent.Load) {
            viewModelScope.launch {
                _state.value = UserState(loading = true)
                runCatching { getUser() }
                    .onSuccess {
                        _state.value = UserState(user = it)
                    }
                    .onFailure {
                        _state.value = UserState(error = it.message)
                    }
            }
        }
    }
}

✅ 7️⃣ UI

Navigation

Routes.kt

object Routes {
    const val HOME = "home"
    const val PROFILE = "profile"
    const val SETTINGS = "settings"
}

NavGraph.kt

@Composable
fun AppNavGraph(
    navController: NavHostController = rememberNavController()
) {
    NavHost(navController, startDestination = Routes.HOME) {

        composable(Routes.HOME) {
            HomeScreen(
                onNavigateProfile = { navController.navigate(Routes.PROFILE) },
                onNavigateSettings = { navController.navigate(Routes.SETTINGS) }
            )
        }

        composable(Routes.PROFILE) {
            ProfileScreen(onBack = { navController.popBackStack() })
        }

        composable(Routes.SETTINGS) {
            SettingsScreen(onBack = { navController.popBackStack() })
        }
    }
}

MainActivity

@AndroidEntryPoint
class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            MaterialTheme {
                AppNavGraph()
            }
        }
    }
}

HomeScreen.kt

@Composable
fun HomeScreen(
    onNavigateProfile: () -> Unit,
    onNavigateSettings: () -> Unit
) {
    Column(
        Modifier.fillMaxSize(),
        verticalArrangement = Arrangement.Center,
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        Button(onClick = onNavigateProfile) { Text("Go to Profile") }
        Spacer(Modifier.height(16.dp))
        Button(onClick = onNavigateSettings) { Text("Go to Settings") }
    }
}

ProfileScreen.kt

@Composable
fun ProfileScreen(
    onBack: () -> Unit,
    viewModel: ProfileViewModel = hiltViewModel()
) {
    val state by viewModel.state.collectAsState()

    LaunchedEffect(Unit) {
        viewModel.loadUser()
    }

    Column(
        Modifier.fillMaxSize(),
        verticalArrangement = Arrangement.Center,
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        when {
            state.loading -> CircularProgressIndicator()
            state.user != null -> Text("Profile: ${state.user.name}")
            state.error != null -> Text("Error")
        }

        Spacer(Modifier.height(16.dp))
        Button(onClick = onBack) { Text("Back") }
    }
}

SettingsScreen

@Composable
fun SettingsScreen(onBack: () -> Unit) {
    Column(
        Modifier.fillMaxSize(),
        verticalArrangement = Arrangement.Center,
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        Text("Settings Screen")
        Spacer(Modifier.height(16.dp))
        Button(onClick = onBack) { Text("Back") }
    }
}

UserScreen.kt

@Composable
fun UserScreen(
    viewModel: UserViewModel = hiltViewModel()
) {
    val state by viewModel.state.collectAsState()

    LaunchedEffect(Unit) {
        viewModel.process(UserIntent.Load)
    }

    when {
        state.loading -> CircularProgressIndicator()
        state.user != null -> Text(state.user.name)
        state.error != null -> Text("Error")
    }
}

✅ 8️⃣ APP ENTRY

MyApp.kt

@HiltAndroidApp
class MyApp : Application()

MainActivity.kt

@AndroidEntryPoint
class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            MaterialTheme { UserScreen() }
        }
    }
}

✅ 9️⃣ UNIT TEST

MainDispatcherRule.kt

@OptIn(ExperimentalCoroutinesApi::class)
class MainDispatcherRule(
    private val dispatcher: TestDispatcher =
        StandardTestDispatcher()
) : TestWatcher() {

    override fun starting(description: Description) {
        Dispatchers.setMain(dispatcher)
    }

    override fun finished(description: Description) {
        Dispatchers.resetMain()
    }
}

UserViewModelTest.kt

@OptIn(ExperimentalCoroutinesApi::class)
class UserViewModelTest {

    @get:Rule val rule = MainDispatcherRule()

    @Test
    fun load_success() = runTest {
        val repo = mockk<UserRepository>()
        coEvery { repo.getUser() } returns User("1","Test")

        val vm = UserViewModel(GetUserUseCase(repo))
        vm.process(UserIntent.Load)

        vm.state.test {
            assertTrue(awaitItem().loading)
            assertEquals("Test", awaitItem().user?.name)
        }
    }
}

✅ 🔟 UI TEST

UserScreenTest.kt

@get:Rule
val composeRule = createAndroidComposeRule<MainActivity>()

@Test
fun user_displayed() {
    composeRule.onNodeWithText("John")
        .assertIsDisplayed()
}

✅ 1️⃣1️⃣ DETEKT CONFIG

config/detekt/detekt.yml

style:
 MagicNumber:
 active: false

✅ 1️⃣2️⃣ CI PIPELINE

.github/workflows/android-ci.yml

name: Android CI

on:
  push:
  pull_request:

jobs:
  build:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-java@v4
        with:
          distribution: 'temurin'
          java-version: '17'

      - run: chmod +x gradlew

      - run: ./gradlew detekt
      - run: ./gradlew lint
      - run: ./gradlew testDebugUnitTest
      - run: ./gradlew assembleDebug

HOW TO RUN

./gradlew clean build
./gradlew detekt
./gradlew lint
./gradlew testDebugUnitTest
./gradlew connectedDebugAndroidTest

메타데이터
post_id
7ebd0dfdf1eb
slug
navigation-route-7ebd0dfdf1eb
url
https://medium.com/@mappsdeveloper1991/navigation-route-7ebd0dfdf1eb
canonical_url
https://medium.com/@mappsdeveloper1991/navigation-route-7ebd0dfdf1eb
author_url
https://medium.com/@mappsdeveloper1991
status
ok
fetched_at
2026-07-13 06:23:13