Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to pass arguments with Navigation3 using SavedStateHandle?

I have tried to migrate from navigation 2 to navigation 3.

Here is a part of my NavDisplay:

@Composable
fun Navigation() {

    val backStack = rememberNavBackStack(Splash)

    NavDisplay(
        backStack = backStack,
        onBack = { backStack.removeLastOrNull() },
        entryDecorators = listOf(
            // Add the default decorators for managing scenes and saving state
            rememberSceneSetupNavEntryDecorator(),
            rememberSavedStateNavEntryDecorator(),
            // Then add the view model store decorator
            rememberViewModelStoreNavEntryDecorator()
        ),
        entryProvider = { route ->
            when (route) {
                is Splash ->
                    NavEntry(key = route) {
                        SplashScreen(
                            navigateToCalendarScreen = {
                                backStack.add(CalendarScreen)
                            }
                        )
                    }


                is Calendar ->
                    NavEntry(key = route) {
                        CalendarScreen(
                            navigateToScreen = { screen ->
                                backStack.add(screen)
                            },
                            navigateToDailyReport = { id ->
                                backStack.add(Report(id))
                            },
                        )
                    }


                is Report ->
                    NavEntry(key = route) {
                        ReportScreen(
                            navigateBack = {
                                backStack.removeLastOrNull()
                            },
                            navigateToWorkout = {
                                backStack.add(Workout(it))
                            },
                            navigateToBodyMeasurement = {
                                backStack.add(Measurement(it))
                            },
                        )
                    }
                 }
             }
        )
   }

When I navigate from Calendar Screen to Report Screen I want to pass an argument Id. Until now I used to receive the arguement in the ViewModel by using savedStateHandle.

@HiltViewModel
open class ReportViewModel @Inject constructor(
    private val savedStateHandle: SavedStateHandle,
): BlocViewModel<ReportEvent, ReportState>() {

    private val id get() = savedStateHandle.get<Long>("id")!!
// rest of the code
}

This practice does not work anymore because I get NullPointerException. It seems that the argument isn't passed or received in the savedStateHandle object.

Any thoughts? What is the proper way to pass arguments from one screen to another using navigation 3?

like image 720
Nontas Papadopoulos Avatar asked Oct 31 '25 02:10

Nontas Papadopoulos


1 Answers

There is issue #420932904 on the Google Issue Tracker that is asking exactly the same question. The answer from a Google Developer was as follows:

Generally, we're steering away from requiring a magic round trip through SavedStateHandle in Navigation3. [...]
Instead, you might consider using the idea of 'assisted injection' - e.g., just passing your key class to the constructor of your ViewModel. [...]

As of Dagger 2.49 and Hilt 1.2.0, hiltViewModel also supports assisted injection, as explained in this issue:

val viewModel = hiltViewModel<MyViewModel, MyViewModel.Factory>(
  creationCallback = { factory -> factory.create(assistedArg = myKey) }
)

Which allows you to combine the Hilt injected parameters with your key directly, without going through SavedStateHandle at all.


So the approach using SavedStateHandle is no longer recommended by Google. Instead, use Assisted Injection.

with Hilt

In Hilt, this works as follows:

@HiltViewModel(assistedFactory = ReportViewModel.Factory::class)
open class ReportViewModel @Inject constructor(
    private val savedStateHandle: SavedStateHandle,
    @Assisted private val id: Long,
): BlocViewModel<ReportEvent, ReportState>() {

    @AssistedFactory
    interface Factory {
        fun create(reportId: Long): ReportViewModel
    }

    // ...
}

Then, you can update your ReportScreen Composable to use that factory while creating the ViewModel:

@Composable
fun ReportScreen(
    reportId: Long,
    reportViewModel: ReportViewModel = hiltViewModel<ReportViewModel, ReportViewModel.Factory> { factory ->
        factory.create(reportId = reportId)
    },
    // ...
) {
    // ...
}

Finally, call your ReportScreen Composable as follows:

NavEntry(key = route) {
    ReportScreen(
        reportId = route.report.id  // or however Report is defined
        navigateBack = {
            backStack.removeLastOrNull()
        },
        navigateToWorkout = {
            backStack.add(Workout(it))
        },
        navigateToBodyMeasurement = {
            backStack.add(Measurement(it))
        },
    )
}

without Hilt

If you are using the standard viewModel() inject function, the same thing can be achieved by simply using

@Composable
fun ReportScreen(
    reportId: Long,
    reportViewModel: ReportViewModel = viewModel {
        ReportViewModel(id = reportId, savedStateHandle = createSavedStateHandle())
    },
    // ...
) {
    // ...
}

Then, you don't even need a custom factory in your ViewModel:

class ReportViewModel(
    private val savedStateHandle: SavedStateHandle
    private val id: Long,  // id is passed into here
): ViewModel() {
    // ...
}
like image 72
BenjyTec Avatar answered Nov 03 '25 00:11

BenjyTec



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!