How to Use Navigation 3 in Jetpack Compose (Step-by-Step)

Navigation 3 in Jetpack Compose: Setup, Features, Example, Nav2 vs Nav3

Navigation 3 in Jetpack Compose: Setup, Features, Example, Nav2 vs Nav3

Navigation 3 (Nav3) is Google’s Compose-first navigation library where your navigation state is a simple back stack list, and the UI updates automatically when that list changes.

What is Navigation 3?

Navigation 3 is designed to feel natural in a declarative UI world: navigation is state, and state drives UI. Instead of relying on a controller-owned back stack, Nav3 expects your back stack to be an observable list (commonly a SnapshotStateList), so changes trigger recomposition of the UI container that displays destinations.

Google describes Nav3 as an improvement over the original Jetpack Navigation API by offering simpler Compose integration, full control of the back stack, and support for layouts that can read more than one destination at the same time (adaptive UI).


Key Features and Improvements

  • State-driven navigation: Navigation is performed by adding/removing keys from a back stack list.
  • Developer-owned back stack: Your app owns the navigation state instead of hiding it behind a controller.
  • Compose-friendly rendering: NavDisplay observes the back stack and displays destinations as it changes.
  • Adaptive UI support: Nav3 makes it possible to build layouts that can read multiple destinations from the back stack (helpful for tablets, foldables, large screens).
  • Simple “keys → content” mapping: entryProvider resolves keys to NavEntry objects that contain the destination content.


Core Components (Nav3 Mental Model)

Navigation 3 becomes easy once the terminology clicks: the back stack contains keys, NavDisplay observes that back stack, and the entryProvider turns keys into NavEntry content.

1) Back stack (navigation state)

In Navigation 3, the back stack doesn’t contain screens; it contains references to screens called keys. Nav3 expects this back stack to be a SnapshotStateList (or another observable state list) so that NavDisplay recomposes when it changes.

2) Keys (routes)

Keys can be “any type,” as long as they can uniquely represent a destination (often a data object or data class). In real apps, keys typically hold arguments (like IDs) to avoid string-based routing mistakes.

3) entryProvider

The entryProvider is responsible for converting the current key into a NavEntry that contains the composable content for that key. You can implement it with a when or a DSL (as shown in official docs and examples).

4) NavEntry

A NavEntry bundles the key and the content (and optionally metadata) for a destination. NavDisplay requests the NavEntry for the top key and renders it.

5) NavDisplay

NavDisplay is the composable that renders a back stack. By default, it shows the topmost NavEntry in a single-pane layout.



How to Set Up Navigation 3 (Step-by-Step)

Real-Time Navigation 3 Setup in Jetpack Compose

In our previous posts, we successfully implemented modern authentication UI screens Design using Jetpack Compose:

Now, it’s time to connect everything together. In this guide, we will walk through the real-time setup of the Navigation 3 library for Jetpack Compose. By the end, you’ll clearly understand how to create a seamless navigation flow between the Sign Up, Login screens.

We’ll break down each step so you can follow it easily — from adding the dependency to creating routes and setting up the navigation graph.

Step 1: Add Navigation 3 dependencies

Add the Navigation 3 runtime and UI libraries to your Compose module.

dependencies {
    implementation(libs.androidx.navigation3.runtime)
    implementation(libs.androidx.navigation3.ui)

    // Only if you need the ViewModel integration
    implementation(libs.androidx.lifecycle.viewmodel.navigation3)
}

Also update SDK levels required by the official migration/get-started guidance: minSdk = 23 and compileSdk = 36 (usually in app/build.gradle(.kts) or version catalog).

 defaultConfig {
        applicationId = "com.newkings.demo"
        minSdk = 24
        targetSdk = 36
        versionCode = 1
        versionName = "1.0"

        testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
    }


Step 2: Sync Your Project

Click Sync Now in Android Studio to download and add the dependencies to your project.


Step 3: Create typed routes (NavKey)

Define all your app routes in a single file for clean and centralized navigation management:

sealed  interface AppNavRoute {
    data object Login  : AppNavRoute
    data object SignUp  : AppNavRoute
}


Step 4: Create an observable back stack

Nav3 expects the back stack to be an observable list (commonly SnapshotStateList) so NavDisplay recomposes when navigation changes happen.

@Composable
fun AppNavHost() {
   val backStack = remember { mutableStateListOf<AppNavRoute>(AppNavRoute.Login) }
  // ...
}


Step 5: Create an entryProvider (key → NavEntry)

entryProvider converts keys in your back stack to NavEntry objects. This keeps navigation state separate from UI rendering and makes the code easier to reason about.

 entryProvider = { key: AppNavRoute ->
            when (key) {
                AppNavRoute.Login -> NavEntry(key, content = {
                    LoginScreen(
                        onLoginSuccess = {
                            backStack.clear()
                            backStack.add(AppNavRoute.Login)
                        },
                        onGoToSignUp = { backStack.add(AppNavRoute.SignUp) }
                    )
                })

                AppNavRoute.SignUp -> NavEntry(key, content = {
                    SignUpScreen(
                        onSignUpSuccess = {
                            // After signup, go back to Login
                            backStack.removeLastOrNull()
                        },
                        onBackToLogin = { backStack.removeLastOrNull() }
                    )
                })
            }
        }


Step 6: Render screens using NavDisplay

NavDisplay observes the back stack and renders the appropriate content. In its default configuration, NavDisplay shows only the top entry in a single-pane UI.

NavDisplay(
  backStack = backStack,
  onBack = { backStack.removeLastOrNull() },
  entryProvider = provider
)


NavDisplay and entryProvide

@Composable
       fun  AppNavHost() {
   
    val backStack = remember { mutableStateListOf<AppNavRoute>(AppNavRoute.Login) }
NavDisplay(
        backStack = backStack,
        onBack = {
          
            if (backStack.size > 1) backStack.removeLastOrNull()
        },
        entryProvider = { key: AppNavRoute ->
            when (key) {
                AppNavRoute.Login -> NavEntry(key, content = {
                    LoginScreen(
                        onLoginSuccess = {
                          
                            backStack.clear()
                            backStack.add(AppNavRoute.Login)
                        },
                        onGoToSignUp = { backStack.add(AppNavRoute.SignUp) }
                    )
                })

                AppNavRoute.SignUp -> NavEntry(key, content = {
                    SignUpScreen(
                        onSignUpSuccess = {
                           
                            backStack.removeLastOrNull()
                        },
                        onBackToLogin = { backStack.removeLastOrNull() }
                    )
                })
            }
        }
    )
}


Step 7 (Optional): Add metadata

NavEntry supports metadata, which can be useful for parent layouts/strategies that need extra information about a destination. This is one of the design pieces that helps support more advanced and adaptive navigation patterns.


Step 8 Login and SignUp Screen

Login Screen

@Composable
fun LoginScreen(onLoginSuccess: () -> Unit,
                onGoToSignUp: () -> Unit) {
 // ...
 
 TextButton(
      onClick = { onGoToSignUp() },
        contentPadding = PaddingValues(0.dp)
       ) {
          Text(
            text = "Sign up",
            fontSize = 14.sp,
            color = Color(0xFF5B6FBD),
            fontWeight = FontWeight.Bold
           )
   }
   
 // ...
}


SignUp Screen

@Composable
fun SignUpScreen(onSignUpSuccess: () -> Unit,
                  onBackToLogin: () -> Unit) {
 // ...
}


Navigation 2 vs Navigation 3

Navigation 3 improves on the original Jetpack Navigation API by making Compose integration simpler, giving you full control over the back stack, and enabling adaptive layouts that can read more than one destination from the back stack.

Area Navigation 2 (Nav2) Navigation 3 (Nav3)
Back stack ownership Back stack is typically managed via NavController patterns (controller-centric approach). Back stack is developer-owned state (SnapshotStateList-style), mutated directly.
Rendering NavHost-based, generally top destination in a host container. NavDisplay observes back stack and renders entries as state changes.
Adaptive layouts More difficult to model multiple visible destinations in a unified way. Explicitly designed for layouts that can read more than one destination at a time.
Migrating approach NavGraph + routes + NavController patterns. Move destinations into entryProvider and replace NavHost with NavDisplay.


Advantages of Navigation 3

Jetpack Compose Navigation 3 comes with several improvements that make app navigation cleaner, faster, and easier to scale. If you’re building a modern Android app using Compose, Navigation 3 helps you maintain a predictable and modular architecture across your screens.

1. Built for Compose from the Ground Up

Unlike older navigation libraries that were adapted for Compose, Navigation 3 is specifically designed for Jetpack Compose. It blends naturally with composable functions, meaning no fragments or XML navigation graphs are required. This gives a pure Compose experience with fewer compatibility issues.


2. Cleaner Code and Reduced Boilerplate

Navigation 3 eliminates the need for fragment managers, navigation hosts, and complicated setups. Your routes are defined as simple Kotlin objects or sealed classes, making the navigation code clean, type-safe, and easy to follow. This helps reduce bugs and improves long-term maintainability.


3. Type-Safe Route and Argument Handling

You can pass parameters between screens without relying on strings or bundles. Navigation 3 supports type-safe arguments, reducing runtime crashes caused by incorrect argument types and making the navigation flow more reliable.


4. Support for Complex App Structures

For multi-module apps, feature-based navigation, or scalable architectures, Navigation 3 provides a structure that’s both flexible and modular. You can easily split navigation across modules or create nested graphs without losing clarity. This is especially useful for apps with authentication screens, onboarding flows, dashboards, and bottom navigation.


5. Smooth Integration with State Management

Navigation 3 integrates naturally with state management tools like ViewModel, Hilt, and dependency injection containers. It encourages a unidirectional data flow (UDF), making it more compatible with MVVM and MVI architectures. Screen state, UI data, and navigation events all work seamlessly together.


6. Easy Animation Support for Transitions

Compose Navigation 3 works with animation libraries and Accompanist, allowing you to build smooth screen transitions and animated navigation effects without complicated setup. This improves user experience with small effort.


7. Testing-Friendly Architecture

Since navigation is Kotlin-first and decoupled from view hierarchies, UI tests become easier to write. Navigation events can be verified using testable routes, mock functions, and state observers. This helps ensure your navigation logic stays stable even as your UI grows.



Frequently Asked Questions (FAQ)

1. What is Navigation 3 in Jetpack Compose?

Navigation 3 is the latest navigation framework designed specifically for Jetpack Compose. It helps developers manage screen transitions and route-based navigation without relying on fragments or XML.


2. How is Navigation 3 different from previous navigation libraries?

Unlike older navigation libraries that were created for XML and fragments, Navigation 3 is built Compose-first, offering cleaner APIs, type-safe routing, and better integration with state management and modular architectures.


3. Can I use Navigation 3 without fragments?

Yes. Navigation 3 is fragment-free by default. You can build complete navigation flows using only composables, which simplifies the entire UI structure and removes fragment-related boilerplate.


4. Is Navigation 3 suitable for large apps?

Absolutely. Navigation 3 supports nested graphs, multi-module projects, and feature-based navigation. This makes it ideal for scaling apps with complex routing, onboarding flows, deep links, and bottom navigation.


5. Can I migrate from Navigation 2 to Navigation 3?

Yes, but it’s a manual process. You’ll need to update your routes, screen definitions, and argument passing patterns. However, the migration pays off with cleaner code and long-term maintainability.


6. Does Navigation 3 support passing data between screens?

Yes. You can pass arguments using type-safe parameters, making it safer than string-based routes or bundles. It reduces the risk of runtime errors and makes code easier to test.


7. Do I need a NavHost like previous versions?

Yes, but it’s simpler. In Navigation 3, NavHost is fully composable and doesn’t require XML or layout files. It acts like a container for all routes and destinations in your app.


8. Can I use animations with Navigation 3?

Yes. Navigation 3 works smoothly with animation libraries (like Accompanist) and built-in Compose animations, allowing developers to create custom transitions and animated screen changes.


9. Is Navigation 3 stable and ready for production?

Yes. Navigation 3 is stable and recommended for new Compose projects. It represents the current direction of modern Android architecture.


Post a Comment

0 Comments