Handle Navigation Args in Directly ViewModel by Hilt, Jetpack Compose
Navigation can be tricky for Android developers, although there are lots of options to make navigations, depending on external libraries can be problematic when new technologies arrives.
One of the best compose navigation post that I found is https://itnext.io/compose-navigation-a-great-choice-for-large-android-apps-94c02984acd5 . I strongly suggest you that take a look for this link, due to this mini app navigation flow is created based on this blog’s recommendation.
App Structure
In this mini app, there are two composable screens. InitialScreen and FinalScreen. InitialScreen do not have a viewModel, it has just a button. When the button is clicked, it triggers a function and navigate to FinalScreen.
Please import Hilt and compose navigation libraries in order to build the project.
implementation("com.google.dagger:hilt-android:2.48.1")
kapt("com.google.dagger:hilt-android-compiler:2.46")
kapt("androidx.hilt:hilt-compiler:1.1.0")
implementation("androidx.navigation:navigation-compose:2.7.6")
implementation("androidx.hilt:hilt-navigation-compose:1.1.0")
Navigation Flow
@Parcelize
data class Person(
val surname: String,
val age: Int,
): Parcelable
internal fun NavGraphBuilder.initialScreen(
onNavigateButtonClicked: (String?, Person) -> Unit,
)
Button(onClick = {
onNavigateButtonClicked(null, Person("Nakamoto", 15))
}) {
Text("Navigate to Final Screen")
}
You might be aware of that handling of a Parcelable argument can be a issue with Compose Navigation. As it can be seen in the code, onNavigateButtonClicked function expects a String nullable argument and a Person data class which a parcelable.
This code will trigger the below function. An args is created and the navigateWithArgs is executed. NavigateWithArgs is an extension function of NavController.
internal fun NavController.navigateToFinalScreen(name: String?, person: Person) {
val args = bundleOf(
NAME_ARG to name,
PERSON_ARG to person
)
navigateWithArgs(FINAL_ROUTE, args = args)
}
NavigateWithArgs
fun NavController.navigateWithArgs(
route: String,
args: Bundle,
navOptions: NavOptions? = null,
navigatorExtras: Navigator.Extras? = null,
) {
val routeLink = NavDeepLinkRequest.Builder.fromUri(createRoute(route).toUri()).build()
val deeplinkMatch = graph.matchDeepLink(routeLink)
deeplinkMatch?.let {
navigate(it.destination.id, args, navOptions, navigatorExtras)
} ?: navigate(route, navOptions, navigatorExtras)
}
Basically, with this function a implicit deeplink is created and checks if the route is matched. Also it provides us to add arguments in the bundle and receive it in the final screen. More information can be found in here -> https://developer.android.com/guide/navigation/use-graph/navigate#uri
Handling Data from VM Directly
By creating the InitialScreenArgModule, we can specify and get the arguments in viewmodel directly. In order to that, savedStatedHandle’s are used.
Handling Nullable Arguments
In order to make an example of argument which can be nullable, providePersonName return type is String?. It is provided in the ViewModelScoped and attached to that specific viewmodel. Also, by adding personName annotation, we can achieve this specific argument when injecting to viewmodel.
@Module
@InstallIn(ViewModelComponent::class)
object InitialScreenArgModule {
@Provides
@PersonName
@ViewModelScoped
fun providePersonName(
savedStatedHandle: SavedStateHandle,
): String? {
return savedStatedHandle[NAME_ARG]
}
annotation class PersonName
Handling Parcelable Arguments
Similarly to PersonName, a new annotation is created (PersonData). By checkingNotNull, Person object can be injected in ViewModel.
@Module
@InstallIn(ViewModelComponent::class)
object InitialScreenArgModule {
@Provides
@PersonData
@ViewModelScoped
fun providePersonData(
savedStatedHandle: SavedStateHandle,
): Person {
return checkNotNull(savedStatedHandle[PERSON_ARG])
}
annotation class PersonData
Full Code of InitialScreenArgModule
@Module
@InstallIn(ViewModelComponent::class)
object InitialScreenArgModule {
@Provides
@PersonName
@ViewModelScoped
fun providePersonName(
savedStatedHandle: SavedStateHandle,
): String? {
return savedStatedHandle[NAME_ARG]
}
@Provides
@PersonData
@ViewModelScoped
fun providePersonData(
savedStatedHandle: SavedStateHandle,
): Person {
return checkNotNull(savedStatedHandle[PERSON_ARG])
}
annotation class PersonName
annotation class PersonData
}
FinalScreenViewModel
Remember that when navigating we used a function onNavigateButtonClicked and send name= null and person to Person(“Nakamoto”, 15).
onNavigateButtonClicked(null, Person("Nakamoto", 15))
Because of this, when injecting personName will be null and it won’t be a problem since InitialScreenArgModule expects a nullable String. Because of it is null, “Satoshi” will be set.
In the code below a simple join operation has been made for two strings.
@HiltViewModel
class FinalScreenViewModel @Inject constructor(
@InitialScreenArgModule.PersonName private val personName: String?,
@InitialScreenArgModule.PersonData private val personData: Person,
): ViewModel() {
private val name = personName ?: "Satoshi"
private val surname = personData.surname
private val _fullName = MutableStateFlow("$name $surname")
val fullName = _fullName.asStateFlow()
}
As you aware of, we did not use the composable screen to achieve this values and we got from viewmodel directly. Now we can send this data by stateFlow and listen it from UI.
internal fun NavGraphBuilder.finalScreen() {
composable(route = FINAL_ROUTE) {
val finalScreenViewModel: FinalScreenViewModel = hiltViewModel()
val fullName by finalScreenViewModel.fullName.collectAsState()
Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = fullName,
)
}
}
}
As a personal view, I think this is a better approach of defining a standart route with optional or non-optional parameters with Jetpack Compose. Replacing old values and new values are not very convinient to make argument transactions. Also viewmodel and compose screen codes are getting lighter since these argument data transactions can be made with Hilt annotations.
You can achieve the repository from here -> https://github.com/bekiryavuzkoc/navigationwithhilt
Thanks for reading!