---
title: "Charting a Course from Android Compose Nav2 to Nav3: Manually Inject Navigation Arguments"
source: "/writing/charting-a-course-from-android-compose-nav2-to-nav3-manually-inject-navigation-arguments/"
---

Article

# Charting a Course from Android Compose Nav2 to Nav3: Manually Inject Navigation Arguments

Lucas Kivi

July 27, 2026

A toucan looking at a globe.

_This is a part of a larger_ [_guide_](/writing/charting-a-course-from-android-compose-navigation-2-to-navigation-3) _for migrating from Android Compose Navigation 2 to Navigation 3. Checkout the parent guide for an overview of the migration._

One of the first things to address in a Nav2 → Nav3 migration is how navigation arguments are provided to `ViewModels`. Nav3 has a specific [recipe](https://github.com/android/nav3-recipes/tree/main/app/src/main/java/com/example/nav3recipes/passingarguments/viewmodels/hilt) for this, which was the main inspiration for the pattern I ultimately implemented.

In many past navigation implementations (Compose Nav2 included), the navigation component supplies the `SavedStateHandle` with navigation arguments, and `ViewModel`s are generally responsible for knowing how to read those arguments from an injected `SavedStateHandle`:

@HiltViewModel
class ProfileViewModel @Inject constructor(
    savedStateHandle: SavedStateHandle,
    private val userRepository: UserRepository
) : ViewModel() {

    private val route: ProfileRoute = savedStateHandle.toRoute<ProfileRoute>()

    val user = userRepository.getUser(route.userId)
}

This pattern works, but it couples the `ViewModel` directly to navigation-managed state. The `ViewModel` does not receive its inputs explicitly. Instead, it has to know how to pull them out of the navigation layer. In my experience, this is a major source of confusion for new devs -- why does this `SavedStateHandle` even have my nav args? Eventually we just accept that this is a magic part of the nav component.

In Nav3, this pattern is thrown overboard; its navigation component no longer populates the `SavedStateHandle`. **Route arguments are resolved at each screen's entry point**. I then recommend passing them to the `ViewModel` at creation time via factory injection.

### Back Stack Management Changes

In Nav3 back stack management becomes very hands-on. This is a major benefit of the new paradigm but it makes talking about navigation a bit more abstract. Ultimately, you just need to provide a set of `NavEntry`s to the `NavDisplay`, but I will avoid going deeper into that as it is beyond the scope of this article. I think 90% of use cases will look something like this:

val backStack = rememberNavBackStack(HomeRoute)

NavDisplay(
    backStack = backStack,
    entryProvider = entryProvider {
        entry<HomeRoute> { ... }
        entry<ProfileRoute> { ... }
        ...
    },
    ...,
)

The two relevant parameters are:

-   `backStack`: A collection of keys that represents the state that needs to be handled by the `NavDisplay`. This is what we manipulate for navigation!
-   `entryProvider`: The lambda used to construct each possible `NavEntry`. This is where we build the screens associated with each navigation route.

### Nuts and Bolts

For the following examples, we will assume you are employing [Hilt dependency injection](https://developer.android.com/training/dependency-injection/hilt-android) (Hilt, now there is something natural for new devs...). Importantly, we will use Hilt's assisted injection, for which there is plenty of documentation and precedent; [this link](https://developer.android.com/training/dependency-injection/hilt-jetpack#assisted-injection) can get you started. We will also use typed navigation with the following `ProfileRoute` type.

@Serializable
data class ProfileRoute(
    val userId: String,
)

### In Nav2

A common pattern looks like this:

-   Define the type-safe `composable` destination

composable<ProfileRoute> {
    val viewModel: ProfileViewModel = hiltViewModel()
    ProfileScreen(viewModel)
}

-   Navigate with navigation arguments, creating a back stack entry that stores and provides the argument

val route = ProfileRoute(...)
navController.navigate(route)

-   Read navigation arguments inside the ViewModel from `SavedStateHandle`

@HiltViewModel
class ProfileViewModel @Inject constructor(
    savedStateHandle: SavedStateHandle,
    private val userRepository: UserRepository
) : ViewModel() {

    private val route: ProfileRoute = savedStateHandle.toRoute<ProfileRoute>()

    val user = userRepository.getUser(route.userId)
}

This makes navigation arguments feel convenient, but it also hides an important dependency. The `ViewModel` depends on a navigation argument, but that dependency is not visible in its constructor -- it is impossible to see this dependency in the `ProfileViewModel`'s public contract. If the VM somehow did not receive a route the app would crash.

### In Nav3

Here, the preferred model is:

-   Define a `ViewModel` and an associated factory, so you can construct the `ViewModel` with both its injected dependencies and its navigation inputs

@HiltViewModel(assistedFactory = ProfileViewModel.Factory::class)
class ProfileViewModel @AssistedInject constructor(
    @Assisted private val route: ProfileRoute,
    private val userRepository: UserRepository
) : ViewModel() {

    val user = userRepository.getUser(userId)

    @AssistedFactory
    interface Factory {
        fun create(route: ProfileRoute): ProfileViewModel
    }
}

// This can be made generic if you use a `BaseViewModel`.
@Composable
fun profileViewModel(
    route: ProfileRoute,
): ProfileViewModel =
    hiltViewModel<ProfileViewModel, ProfileViewModel.Factory>(
        creationCallback = { factory -> factory.create(route) },
    )

-   Define the type-safe `entry` destination, handle resolving the navigation argument, then pass the data off to the `ViewModel` factory

entry<ProfileRoute> { route ->
    val viewModel = profileViewModel(
        route = route,
    )
    ProfileScreen(viewModel)
}

-   Navigate with navigation arguments, creating a back stack entry that stores and provides the argument

val route = ProfileRoute(...)
backStack.add(route)

This makes the `ViewModel`'s required inputs explicit and shifts responsibility to the correct layer:

-   Navigation owns route parsing
-   Dependency injection owns object construction
-   The `ViewModel` just receives what it needs

This _is_ a bit more verbose, but in some ways it _should_ be -- it makes explicit something that was previously abstracted away from us behind the navigation component.

### Conclusion

### Manual Injection is a Better Pattern

Moving away from `SavedStateHandle` for primary argument provision has a few main benefits:

-   Dependencies are explicit, typed, and required instead of hidden, casted, and implied
-   `ViewModel`s are less coupled to navigation internals
-   Testing is simpler because inputs can be provided directly

`SavedStateHandle` may still have a place for state restoration, but it no longer works out of the box for delivering navigation arguments into a `ViewModel`, and therefore it should stop being the default mechanism.

**Up next**: u [se metadata to control navigation animations](https://medium.com/@lucas_80602/charting-a-course-from-android-compose-nav2-to-nav3-use-metadata-to-control-navigation-entry-32371bbb11bd) .

_Lucas_ _arg__ues with_ `_ViewModel_`_s and_ _assist__s with_ _injection_ _at Livefront._

-   Lucas Kivi

    Software Engineer

    [More from Lucas](/team/lucas-kivi/)

    Lucas Kivi

### Tags

[Mobile Apps](/insights/all/?topic=mobile-apps)

### Share

-   Copied

## View these next.

[See all](/insights/all/?type=writing)

-   [

    Article

    How To Sabotage Your Project Using Inconsistency

    Collin Flynn

    ](/writing/how-to-sabotage-your-project-using-inconsistency/)
-   [

    Article

    Unit Testing race conditions by creating chaos (Swift)

    Sean Berry

    ](/writing/unit-testing-race-conditions-by-creating-chaos-swift/)
-   [

    Article

    UIApplicationDelegate call sequence reference

    Sean Berry

    ](/writing/uiapplicationdelegate-call-sequence-reference/)
