Migrating from Navigation 2 to Navigation 3 in Android

Modern Android development evolves rapidly, and Google's Jetpack libraries continuously improve developer experience and application architecture. One such evolution is the transition from Navigation 2 (Nav2) to Navigation 3 (Nav3).

If you're building modern apps with Jetpack Compose, Navigation 3 introduces a simpler, type-safe, and scalable navigation system that improves code maintainability and reduces runtime errors.


Why Navigation 3?

Jetpack Navigation 2 works well but has several limitations:

Common issues with Nav2:

  • String-based routes

  • Runtime navigation errors

  • Difficult deep-link handling

  • Hard-to-maintain argument passing

  • Poor type safety

Navigation 3 introduces:

✔  Type-safe destinations
✔  Compile-time safety
✔  Cleaner navigation APIs
✔  Better Compose integration
✔  Reduced boilerplate


Nav2 vs Nav3: Key Differences

FeatureNavigation 2Navigation 3
Route definitionString basedType-safe
ArgumentsManual parsingStrongly typed
SafetyRuntime errors possibleCompile-time safety
Compose integrationGoodExcellent
MaintainabilityModerateHigh

Dependency Setup

First, ensure your project is using the latest navigation library.

dependencies {
    implementation("androidx.navigation:navigation-compose:2.8.0")
}

Navigation 3 features are enabled via typed routes.


Navigation 2 Example (Before Migration)

Typical Nav2 implementation uses string routes.

Define Routes

object Routes {
    const val HOME = "home"
    const val DETAILS = "details/{id}"
}

NavHost Setup

NavHost(
    navController = navController,
    startDestination = Routes.HOME
) {

    composable(Routes.HOME) {
        HomeScreen {
            navController.navigate("details/10")
        }
    }

    composable(
        route = Routes.DETAILS,
        arguments = listOf(navArgument("id") { type = NavType.IntType })
    ) { backStackEntry ->

        val id = backStackEntry.arguments?.getInt("id")

        DetailScreen(id)
    }
}

Problems:

  • Hardcoded strings

  • Argument errors occur at runtime

  • Difficult refactoring


Navigation 3 Approach (Recommended)

Navigation 3 introduces type-safe destinations using Kotlin serialization or sealed classes.


Step 1: Define Type-safe Routes

Use sealed classes or data classes.

sealed class Screen {

    data object Home : Screen()

    data class Detail(val id: Int) : Screen()
}

This gives:

  • Compile-time safety

  • Strongly typed navigation


Step 2: Navigation Setup

NavHost(
    navController = navController,
    startDestination = Screen.Home
) {

    composable<Screen.Home> {

        HomeScreen {
            navController.navigate(Screen.Detail(10))
        }
    }

    composable<Screen.Detail> { entry ->

        val detail = entry.toRoute<Screen.Detail>()

        DetailScreen(detail.id)
    }
}

Now navigation is:

✔ type-safe
✔ compile-time validated
✔ easier to maintain


Step 3: Passing Arguments Safely

Old approach:

navController.navigate("details/10")

New Nav3 approach:

navController.navigate(Screen.Detail(10))

No more:

  • string concatenation

  • route parsing

  • argument mismatch errors


Migration Strategy for Production Apps

Large applications cannot migrate everything at once.

Recommended migration strategy:

Step 1 — Introduce Typed Routes

Convert existing routes to sealed classes gradually.

Step 2 — Replace String Navigation

Replace:

navController.navigate("details/$id")

With:

navController.navigate(Screen.Detail(id))

Step 3 — Remove navArgument()

Typed navigation removes the need for manual argument definitions.

Step 4 — Refactor Navigation Graph

Update composables to use typed navigation.


Best Practices for Navigation 3

1. Use Sealed Classes for Screens

sealed interface AppScreen

Keeps navigation organized.


2. Use Feature-Based Navigation

Structure navigation by feature modules.

Example:

navigation
 ├── auth
 ├── home
 ├── profile

Each feature owns its navigation graph.


3. Avoid Passing Large Objects

Pass IDs instead of full models.

Good:

Screen.Detail(productId)

Bad:

Screen.Detail(product)

4. Keep Navigation in One Layer

Recommended architecture:

UI Layer
   ↓
Navigation Layer
   ↓
ViewModel
   ↓
Repository

Example Project Structure

A clean navigation architecture might look like this:

app
 ├── navigation
 │     AppNavHost.kt
 │     Screen.kt
 │
 ├── features
 │     ├── home
 │     ├── detail
 │     ├── profile
 │
 ├── ui
 ├── data
 ├── domain

Performance Benefits

Navigation 3 also improves performance because:

  • Fewer runtime checks

  • Cleaner back stack management

  • Reduced route parsing


Common Migration Pitfalls

Avoid these mistakes:

❌ Mixing typed routes with string routes
❌ Passing complex objects between screens
❌ Creating huge navigation graphs

Instead:

✔ Keep navigation modular
✔ Use feature-based graphs
✔ Pass only necessary arguments


When Should You Migrate?

Migration is recommended if:

  • Your app uses Jetpack Compose

  • You maintain a large codebase

  • You want compile-time navigation safety

If your project still uses Fragments, Nav2 may still be sufficient.


My Thoughts

Navigation 3 represents a major improvement in Android navigation architecture, especially for Compose-first applications.

By adopting:

  • Type-safe routes

  • Cleaner APIs

  • Compile-time safety

Android engineers can build more maintainable, scalable, and safer navigation systems.

If you are starting a new Jetpack Compose project today, Navigation 3 should be your default choice.