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?
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
SavedStateHandlein 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
SavedStateHandleat 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() {
// ...
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With