Navigating from ViewModels in Jetpack Compose with Hilt
Navigating between screens in Jetpack Compose applications often involves managing the navigation flow from your ViewModels. This is especially true when using Hilt for dependency injection, as it allows for cleaner and more testable code. This article will delve into the best practices for navigating from ViewModels in a Compose application using Hilt.
Understanding the Problem
The challenge lies in decoupling the UI from navigation logic. Directly calling navigation functions within composables can lead to tightly coupled code and difficulty testing. Instead, we want to use ViewModels to manage the navigation state and trigger transitions from there.
Scenario and Original Code
Let's consider a simple example:
@Composable
fun HomeScreen(viewModel: HomeViewModel) {
Column {
Text("Welcome to the App!")
Button(onClick = {
// **Directly calling navigation from composable**
navController.navigate("profile")
}) {
Text("Go to Profile")
}
}
}
@HiltViewModel
class HomeViewModel @Inject constructor(
private val navController: NavHostController
) : ViewModel() {
// ViewModel logic here
}
In this example, the navigation action is triggered within the HomeScreen
composable, directly referencing the navController
. This approach can lead to code that's difficult to test and maintain.
The Solution: Using Navigation Events
The recommended approach is to use a navigation event mechanism within the ViewModel. This allows the UI to react to navigation events triggered by the ViewModel.
@Composable
fun HomeScreen(viewModel: HomeViewModel) {
Column {
Text("Welcome to the App!")
Button(onClick = {
viewModel.onNavigateToProfile()
}) {
Text("Go to Profile")
}
}
}
@HiltViewModel
class HomeViewModel @Inject constructor(
private val navController: NavHostController
) : ViewModel() {
private val _navigationEvent = MutableStateFlow<NavigationEvent?>(null)
val navigationEvent: StateFlow<NavigationEvent?> = _navigationEvent.asStateFlow()
fun onNavigateToProfile() {
_navigationEvent.value = NavigationEvent.NavigateToProfile
}
// Other ViewModel logic
}
sealed class NavigationEvent {
object NavigateToProfile : NavigationEvent()
}
In this revised code:
- We define a sealed class
NavigationEvent
to represent different navigation actions. - The
HomeViewModel
exposes anavigationEvent
state flow to the composable. - The
onNavigateToProfile
function updates thenavigationEvent
state flow with theNavigateToProfile
event. - The
HomeScreen
composable observes thenavigationEvent
and reacts accordingly.
How it Works
- Event Trigger: When the user clicks the "Go to Profile" button, the
onNavigateToProfile
function in theHomeViewModel
is called. - State Update: This function updates the
navigationEvent
state flow with theNavigateToProfile
event. - Composable Reaction: The
HomeScreen
composable observes thenavigationEvent
state flow and reacts when the event is emitted. - Navigation Execution: You can use a
LaunchedEffect
within the composable to listen for navigation events and navigate accordingly.
Example with LaunchedEffect
@Composable
fun HomeScreen(viewModel: HomeViewModel) {
// ... (rest of the composable code)
LaunchedEffect(key1 = viewModel.navigationEvent) {
viewModel.navigationEvent.collect { event ->
when (event) {
is NavigationEvent.NavigateToProfile -> navController.navigate("profile")
null -> {}
}
}
}
}
This approach ensures that navigation actions are triggered by the ViewModel, keeping the composable clean and focused on UI rendering.
Benefits of this approach:
- Testability: You can easily test the ViewModel's navigation logic without interacting with the UI.
- Maintainability: Decoupling navigation logic from composables leads to cleaner code and easier modifications.
- State Management: The ViewModel manages navigation events, ensuring consistent and predictable navigation behavior.
Important Considerations:
- Navigation Destination: Ensure your navigation destination (e.g., "profile") matches the route defined in your
NavHost
. - Clearing Navigation Events: After navigating, remember to clear the
navigationEvent
to avoid unintended re-navigation.
By implementing this approach, you can effectively handle navigation from your ViewModels in Jetpack Compose applications using Hilt. This results in cleaner, more maintainable, and testable code.
Resources: