'What is the proper way to navigate from ViewModel in Jetpack Compose + Hilt + ViewModel?
I have stumbled upon this quite trivial, but tricky problem. I have spent a decent amount of time searching official docs, but unfortunately found no answer.
Official docs say that you should pass an instance of NavController
down to @Composable
-s, and call it as onClick = { navController.navigate("path") }
. But what happens if I have to trigger navigation event from ViewModel (ex. redirect on login, redirect to newly created post page)? Awaiting any coroutine (ex. HTTP request) in @Composable
would be not just bad, but probably force Android to kill app because of the blocked UI thread
Unofficial solutions (documented mostly if form of Medium articles) are based on the concept of having a singleton class and observing some MutableStateFlow
containing path.
That sounds stupid in theory, and doesn't help much in practice (not side-effect and recomposition friendly, triggers unnecessary re-navigation).
Solution 1:[1]
The rememberNavController
has a pretty simple source code that you can use to create it in a singleton service:
@Singleton
class NavigationService @Inject constructor(
@ApplicationContext context: Context,
) {
val navController = NavHostController(context).apply {
navigatorProvider.addNavigator(ComposeNavigator())
navigatorProvider.addNavigator(DialogNavigator())
}
}
Create a helper view model to share NavHostController
with NavHost
view:
@HiltViewModel
class NavViewModel @Inject constructor(
navigationService: NavigationService,
): ViewModel() {
val controller = navigationService.navController
}
NavHost(
navController = hiltViewModel<NavViewModel>().controller,
startDestination = // ...
) {
// ...
}
Then in any view model you can inject it and use for navigation:
@HiltViewModel
class ScreenViewModel @Inject constructor(
private val navigationService: NavigationService
): ViewModel() {
fun navigateToNextScreen() {
navigationService.navController.navigate(Destinations.NextScreen)
}
}
Solution 2:[2]
I have been struggling with the exact same question myself. From the limited documentation Google provided on this topic, specifically the architecture events section I'm wondering if what they're suggesting is to use a state as a trigger for navigation?
Quoting the document:
For example, when implementing a sign-in screen, tapping on a Sign in button should cause your app to display a progress spinner and a network call. If the login was successful, then your app navigates to a different screen; in case of an error the app shows a Snackbar. Here's how you would model the screen state and the event:
They have provided the following snippet of code for the above requirement:
sealed class UiState {
object SignedOut : UiState()
object InProgress : UiState()
object Error : UiState()
object SignIn : UiState()
}
class MyViewModel : ViewModel() {
private val _uiState = mutableStateOf<UiState>(SignedOut)
val uiState: State<UiState>
get() = _uiState
}
What they did not provide is the rest of the view model and compose code. I'm guessing it's supposed to look like:
@Composable
fun MyScreen(navController: NavController, viewModel: MyViewModel) {
when(viewModel.uiState){
is SignedOut -> // Display signed out UI components
is InProgress -> // Display loading spinner
is Error -> // Display error toast
// Using the SignIn state as a trigger to navigate
is SignIn -> navController.navigate(...)
}
}
Also the view model could have a function like this one (trigger by clicking a "sign in" button from compose screen
fun onSignIn() {
viewModelScope.launch {
// Make a suspending sign in network call
_uiState.value = InProgress
// Trigger navigation
_uiState.value = SignIn
}
}
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
Solution | Source |
---|---|
Solution 1 | Pylyp Dukhov |
Solution 2 | Nikola Špiri? |