Android : Compose UI State
Where to hold the UI State (Compose) in Android and Why?

Senior Android Engineer from Bangladesh. Love to contribute in Open-Source. Indie Music Producer.
Introduction
In Android, the recommended practice is to hold a UI-related state (Compose UI State) in a ViewModel rather than in an Activity or Fragment. This is because Jetpack Compose follows a reactive programming model where the UI is composed based on the current state. By keeping the UI state in a ViewModel ensures that the UI remains independent of the Android lifecycle, making it more predictable and easier to manage.
Breakdown
Here's a breakdown of why we should hold the UI state in a ViewModel:
Lifecycle Independence:
ViewModelsarelifecycle-aware componentsthat survive configuration changes (like screen rotations) and are associated with a specific scope, often the hostingActivityorFragment. This ensures that your UI state persists across configuration changes, providing a smoother user experience.Separation of Concerns:
ViewModelhelps in separating the business logic and UI presentation. Your UI code can focus on describing how the UI should look based on the current state, while theViewModelmanages the data and logic required to drive that UI state.Testing: It's easier to unit test a
ViewModelas compared to testing UI components. You can write tests to verify the behavior of your business logic and state management without dealing with UI-related complexities.Reusability: Keeping the UI state in a
ViewModelmakes it more reusable across differentComposablefunctions or even different screens, improving code modularity.Compose Design Philosophy:
Jetpack Composeencourages a declarative UI approach, where we describe what the UI should look like based on the current state. Storing the state in aViewModelaligns well with this design philosophy.
Note
However, it's important to note that not all state needs to be stored in a ViewModel. UI state that is purely ephemeral and doesn't need to survive configuration changes (like animations, and temporary visual changes) can still be stored within a Composable using remember or other Composable-specific state management mechanisms.
Example
Let's say we have a simple counter app where you want to display a counter value and allow the user to increment it. We'll use a ViewModel to manage the counter state.
- First, let's create a
ViewModelthat holds the counter value for us:
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch
class CounterViewModel : ViewModel() {
// State flow to hold the counter value
private val _counterState = MutableStateFlow(0)
val counterState = _counterState
// Function to increment the counter
fun incrementCounter() {
viewModelScope.launch {
_counterState.value++
}
}
}
- In our
Composablefunction, we can use thecounterStatefrom theViewModelto display the counter value and use theincrementCounterfunction to handle user interactions:
import androidx.compose.foundation.layout.Column
import androidx.compose.material.Button
import androidx.compose.runtime.*
import androidx.compose.ui.platform.LocalContext
import androidx.lifecycle.viewmodel.compose.viewModel
@Composable
fun CounterScreen() {
// Access the ViewModel
val counterViewModel: CounterViewModel = viewModel()
// Observe the counter state
val counterState by counterViewModel.counterState.collectAsState()
// Access the context (for showing toasts, etc.)
val context = LocalContext.current
Column {
// Display the counter value
Text(text = "Counter: $counterState")
// Button to increment the counter
Button(onClick = {
counterViewModel.incrementCounter()
// Show a toast to indicate the increment
Toast.makeText(context, "Counter incremented", Toast.LENGTH_SHORT).show()
}) {
Text("Increment")
}
}
}
CounterViewModel holds the counter state using a MutableStateFlow. The CounterScreen Composable function uses the viewModel composable to access the ViewModel. It then observes the counterState using collectAsState and updates the UI whenever the state changes. The button click calls the incrementCounter function from the ViewModel to update the counter value.
By following this approach, we ensure that the UI state (counterState) is kept separate from the UI presentation logic, making the code easier to understand, test, and maintain.
Conclusion
While ViewModel is the recommended place to hold the UI state in Jetpack Compose, you should consider the nature of the state and its lifecycle requirements when making a decision. Always strive to keep your UI logic separate from your UI presentation to create maintainable and robust applications.
That's it for today. Happy Coding...
If get to know something new by reading my articles, don't forget to endorse me on LinkedIn
Read about SOLID Principles
Read about Android Development




