Handle Navigation Args in Directly ViewModel by Hilt, Jetpack Compose

Bekir Yavuz Koc
4 min readJan 10, 2024

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!

--

--