The Two Best Ways to Create a ViewModel in Jetpack Compose

Jetpack Compose has revolutionized Android development by introducing a declarative UI framework that enables developers to build modern, efficient, and maintainable user interfaces. As an essential part of Android’s modern development toolkit, Compose simplifies UI creation and state management, allowing developers to focus more on functionality and less on boilerplate code. Among the many key components of Jetpack Compose is the integration of ViewModel, which plays a vital role in managing UI-related data in a lifecycle-aware manner. In this blog post, we’ll dive deep into the two best ways to create a ViewModel in Jetpack Compose, enabling you to harness its full potential in your projects.

Why Use a ViewModel in Jetpack Compose?

ViewModel, part of the Android Jetpack library, is designed to store and manage UI-related data in a lifecycle-conscious way. It ensures data survives configuration changes, such as screen rotations, making it a crucial component for creating robust Android applications. When combined with Jetpack Compose, ViewModel serves as the backbone for state management, keeping your UI reactive and efficient.

Jetpack Compose’s declarative paradigm thrives on unidirectional data flow and state management. ViewModel naturally complements this by acting as the single source of truth for UI state, ensuring clean separation of concerns and adherence to modern Android development best practices.

Method 1: Using viewModel() from Compose Runtime

One of the most straightforward ways to create a ViewModel in Jetpack Compose is by using the viewModel() function provided by Compose’s runtime. This function is lifecycle-aware and automatically integrates with the lifecycle of the component in which it is used.

How to Implement

Here’s a basic example of how to use the viewModel() function in Jetpack Compose:

@Composable
fun MyScreen() {
    val myViewModel: MyViewModel = viewModel()

    // Observe ViewModel state
    val uiState by myViewModel.uiState.collectAsState()

    // Build your UI based on state
    MyContent(uiState = uiState, onEvent = { myViewModel.handleEvent(it) })
}

class MyViewModel : ViewModel() {
    private val _uiState = MutableStateFlow(UiState())
    val uiState: StateFlow<UiState> = _uiState

    fun handleEvent(event: UiEvent) {
        // Update state based on the event
    }
}

data class UiState(val someValue: String = "")
data class UiEvent(val action: String)

Key Points

  • Lifecycle-aware: The viewModel() function ensures the ViewModel is scoped to the lifecycle of the composable’s host, such as an Activity or a NavBackStackEntry in Jetpack Navigation.

  • Ease of use: It eliminates the need for manual ViewModel creation, reducing boilerplate code.

  • State management: Compose’s collectAsState makes it simple to observe and react to state changes.

When to Use

Use viewModel() when your composable is hosted within a lifecycle-aware component such as an Activity, Fragment, or a Navigation destination. It’s ideal for most straightforward scenarios where you need to maintain and manage UI state effectively.

Method 2: Using hiltViewModel() for Dependency Injection

For projects leveraging Dependency Injection (DI) with Hilt, the hiltViewModel() function simplifies ViewModel creation and scoping. Hilt handles ViewModel injection seamlessly, allowing you to focus on building your application’s core logic.

How to Implement

Here’s how you can use hiltViewModel() in a Jetpack Compose project:

@Composable
fun MyHiltScreen() {
    val myViewModel: MyViewModel = hiltViewModel()

    val uiState by myViewModel.uiState.collectAsState()

    MyContent(uiState = uiState, onEvent = { myViewModel.handleEvent(it) })
}

@HiltViewModel
class MyViewModel @Inject constructor(
    private val repository: MyRepository
) : ViewModel() {
    private val _uiState = MutableStateFlow(UiState())
    val uiState: StateFlow<UiState> = _uiState

    fun handleEvent(event: UiEvent) {
        // Logic to handle events and update state
    }
}

Key Points

  • Seamless DI integration: hiltViewModel() works with Hilt’s DI framework, enabling constructor injection for ViewModels.

  • Scoped lifetimes: Like viewModel(), hiltViewModel() scopes ViewModels to the lifecycle of their host components.

  • Reduced boilerplate: Hilt eliminates the need for manual factory creation, streamlining ViewModel instantiation.

When to Use

Use hiltViewModel() when your project is set up with Hilt for dependency injection. It’s particularly beneficial for complex applications where managing dependencies manually would result in significant overhead.

Best Practices for ViewModel Usage in Jetpack Compose

To get the most out of ViewModel integration in Jetpack Compose, keep these best practices in mind:

  1. Keep UI logic in ViewModel: Avoid placing business or UI logic directly in composables. Delegate these responsibilities to the ViewModel for better separation of concerns.

  2. Use immutable state: Prefer immutable state models and expose them via StateFlow or LiveData to ensure a predictable unidirectional data flow.

  3. Optimize recompositions: Use tools like remember and derivedStateOf to avoid unnecessary recompositions, improving performance.

  4. Test ViewModel logic: Write unit tests for your ViewModel to ensure your state management and business logic are reliable.

Advanced Features

Jetpack Compose offers several advanced features that complement ViewModel integration. Here are a few noteworthy mentions:

  • Navigation: Use NavBackStackEntry as the lifecycle owner when creating ViewModels scoped to a navigation graph.

  • SavedStateHandle: Leverage SavedStateHandle in your ViewModel to persist and restore UI state across process deaths.

  • Custom ViewModel factories: For scenarios requiring additional customization, create your own ViewModel factories and use them with Compose’s viewModel().

Practical Example: Combining Navigation and ViewModel

Let’s consider a scenario where you need to create a multi-screen app with shared state management:

@Composable
fun MainNavGraph(navController: NavController) {
    NavHost(navController = navController, startDestination = "home") {
        composable("home") { HomeScreen(navController) }
        composable("details") { DetailsScreen(navController) }
    }
}

@Composable
fun HomeScreen(navController: NavController) {
    val homeViewModel: HomeViewModel = hiltViewModel()

    // Render UI using homeViewModel
}

@Composable
fun DetailsScreen(navController: NavController) {
    val detailsViewModel: DetailsViewModel = hiltViewModel()

    // Render UI using detailsViewModel
}

This example demonstrates how to use hiltViewModel() with Jetpack Navigation to scope ViewModels to specific destinations, ensuring proper lifecycle management.

Conclusion

Jetpack Compose and ViewModel together form a powerful combination for modern Android UI development. Whether you use viewModel() for simplicity or hiltViewModel() for DI, these methods enable you to build scalable, maintainable, and efficient applications. By adhering to best practices and exploring advanced features, you can harness the full potential of these tools to deliver exceptional user experiences.

Now that you’ve learned the two best ways to create a ViewModel in Jetpack Compose, it’s time to put this knowledge into practice. Start building and experimenting with these techniques in your next project, and see how they transform your development workflow!