Compose State vs ViewModel: Complete Android Developer Guide 2025

I. Executive Summary / TL;DR
Compose State vs ViewModel isn't always an either-or decision. Compose State (using remember
, mutableStateOf
) excels at managing UI-specific, transient state within a Composable or its immediate children—think button toggles, text field inputs, or animation states. ViewModel, on the other hand, is your go-to for holding UI-related data that needs to survive configuration changes, represents screen-level state, and often contains business logic.
The key insight? These approaches are not mutually exclusive. The most robust Android applications combine both: ViewModels manage the core data and business logic for screens, while individual Composables handle their own presentation-specific state using remember
. This hybrid approach delivers the best of both worlds—separation of concerns, configuration change resilience, and clean, maintainable code.
II. Introduction: The State Management Challenge in Jetpack Compose
Jetpack Compose has revolutionized Android UI development with its declarative paradigm, where UI is a function of state. This shift brings incredible benefits—more predictable UIs, less boilerplate, and better performance—but it also introduces critical questions about Jetpack Compose state management.
In traditional Android development, Views held their own state internally. With Compose's declarative nature, we explicitly manage state and pass it down to Composables. This brings us to a fundamental decision point: should we use local Composable state mechanisms like remember
and mutableStateOf
, or should we leverage the architecture-proven Android ViewModel Compose integration?
The answer isn't straightforward because both approaches serve different purposes and often work together. Understanding when to use remember vs ViewModel is crucial for building maintainable, performant, and user-friendly Android applications.
This article provides definitive guidance on making these architectural decisions, complete with practical examples and real-world scenarios to help you architect robust Compose UIs.
III. Understanding Composable State (remember
, mutableStateOf
, etc.)
What is Composable State?
Composable state in Jetpack Compose revolves around several key APIs:
State<T>
: The core interface representing a value that can be observedmutableStateOf()
: Creates a mutable state holderremember
: Preserves state across recompositions within the CompositionrememberSaveable
: Likeremember
, but survives process death
These tools are designed for managing UI element state—the kind of data that directly affects how your Composables render and behave.
Purpose and Use Cases
Composable state excels at handling:
- Simple UI interactions (button states, dropdown visibility)
- Transient data that doesn't need persistence
- Animation states and transitions
- Form input values during user interaction
- UI-specific flags and toggles
Lifecycle and Scope
The lifecycle of Composable state is tied directly to the Composition—the tree of Composables that Compose maintains. When a Composable leaves the Composition (due to navigation or conditional rendering), its remember
state is lost. This behavior is intentional and appropriate for truly transient UI state.
@Composable
fun ExpandableCard() {
var isExpanded by remember { mutableStateOf(false) }
Card(
modifier = Modifier.clickable { isExpanded = !isExpanded }
) {
Column {
Text("Card Header")
if (isExpanded) {
Text("Expanded content that appears/disappears")
}
}
}
}
Pros of Composable State
- Simplicity: Minimal boilerplate for straightforward UI state
- Locality: State logic lives close to where it's used
- Performance: No unnecessary indirection for simple cases
- Compose-native: Designed specifically for Compose's recomposition model
Cons of Composable State
- Configuration vulnerability: Lost during device rotation (unless using
rememberSaveable
) - Limited scope: Cannot be shared easily across distant Composables
- Business logic concerns: Not suitable for complex data transformations
- Testing challenges: Requires UI testing rather than isolated unit tests
Clear Code Examples
Here's a practical example showing appropriate Composable state usage:
@Composable
fun SearchFilterChips() {
var selectedFilters by remember { mutableStateOf(setOf<String>()) }
LazyRow {
items(availableFilters) { filter ->
FilterChip(
selected = filter in selectedFilters,
onClick = {
selectedFilters = if (filter in selectedFilters) {
selectedFilters - filter
} else {
selectedFilters + filter
}
},
label = { Text(filter) }
)
}
}
}
IV. Understanding Android ViewModels
What is a ViewModel?
Android ViewModel is a core component of Android Architecture Components, designed to store and manage UI-related data in a lifecycle-conscious way. In the context of Android ViewModel Compose integration, ViewModels serve as the bridge between your app's data layer and your Composable UI.
ViewModels are specifically designed to survive configuration changes like device rotation, making them ideal for holding data that represents the current state of a screen or feature.
Purpose and Scope
ViewModels excel at:
- Managing screen-level state and business logic
- Surviving configuration changes automatically
- Coordinating data from multiple sources (repositories, use cases)
- Providing a clean separation between UI and business logic
- Exposing data in a lifecycle-aware manner
Lifecycle and Scope
ViewModels are scoped to:
- Activity: Lives for the entire Activity lifecycle
- Fragment: Lives for the Fragment's lifecycle
- Navigation destinations: Lives for the specific navigation graph entry
This scoping means ViewModels naturally survive configuration changes, making them perfect for holding data that users expect to persist during device rotation or other configuration changes.
Pros of ViewModels
- Configuration resilience: Automatically survives device rotation and configuration changes
- Separation of concerns: Clean boundary between UI and business logic
- Testability: Business logic can be unit tested independently of UI
- Data coordination: Natural place to combine data from multiple sources
- Lifecycle awareness: Designed to work seamlessly with Android's lifecycle
Cons of ViewModels
- Boilerplate overhead: More setup required for simple state
- Potential overkill: Can be excessive for purely presentational Composables
- Learning curve: Requires understanding of Android Architecture Components
Clear Code Examples
Here's a typical ViewModel setup for Compose:
class ProductListViewModel @Inject constructor(
private val productRepository: ProductRepository
) : ViewModel() {
private val _uiState = MutableStateFlow(ProductListUiState())
val uiState: StateFlow<ProductListUiState> = _uiState.asStateFlow()
fun loadProducts() {
viewModelScope.launch {
_uiState.value = _uiState.value.copy(isLoading = true)
try {
val products = productRepository.getProducts()
_uiState.value = _uiState.value.copy(
products = products,
isLoading = false
)
} catch (e: Exception) {
_uiState.value = _uiState.value.copy(
error = e.message,
isLoading = false
)
}
}
}
}
data class ProductListUiState(
val products: List<Product> = emptyList(),
val isLoading: Boolean = false,
val error: String? = null
)
V. Head-to-Head: Compose State vs. ViewModel – Key Differences
Understanding the fundamental differences between Compose State vs ViewModel is crucial for making informed architectural decisions. Here's a comprehensive comparison:
Comparison Table
Feature | Composable State (remember ) |
ViewModel |
---|---|---|
Primary Use | UI element state, transient data | Screen-level state, business logic |
Lifecycle | Tied to Composable in Composition | Tied to Activity/Fragment/Nav Graph |
Config Changes | Lost (unless rememberSaveable ) |
Survives automatically |
Business Logic | Not recommended | Recommended place |
Data Source | Typically internal to UI | Can fetch from repositories, use cases |
Testability | Requires UI tests | Easier unit testing of logic |
Complexity | Simpler for local needs | More setup, better for complex needs |
Sharing Scope | Limited to Composable hierarchy | Can be shared across screen |
Memory Management | Automatic with Composition | Managed by ViewModelStore |
Detailed Elaboration on Key Differences
Lifecycle and Scope
The most fundamental difference lies in lifecycle management. Composable state lives and dies with the Composition—when a Composable is removed from the UI tree, its remember
state disappears. ViewModels, however, are scoped to broader lifecycle owners (Activity, Fragment, or Navigation destination), making them perfect for data that should persist across UI changes.
Data Persistence and Configuration Changes
Configuration changes like device rotation destroy and recreate Activities, which triggers Composition recreation. Composable state is lost unless you use rememberSaveable
with Parcelable data. ViewModels automatically survive these changes, making them ideal for complex state that users expect to persist.
Role in Business Logic
ViewModels are designed to house business logic—data transformations, API calls, and complex state calculations. Composables with remember
should focus purely on presentation logic. Mixing business logic into Composables creates testing difficulties and violates separation of concerns.
Testability Aspects
ViewModel business logic can be unit tested in isolation, making it easier to verify complex behaviors. Composable state typically requires UI testing or Compose testing frameworks, which are more complex and slower than pure unit tests.
VI. The Synergy: How Compose State and ViewModel Work Together (Best Practices)
The real power emerges when you understand that Compose State vs ViewModel isn't always a choice—it's often about using both strategically. The best Compose applications combine ViewModels for screen-level concerns with Composable state for UI-specific needs.
ViewModel as the Source of Truth for Screen-Level State
ViewModels should manage the core data that defines what your screen displays:
@Composable
fun ProductListScreen(
viewModel: ProductListViewModel = hiltViewModel()
) {
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
ProductListContent(
uiState = uiState,
onProductClick = viewModel::onProductClicked,
onRefresh = viewModel::refresh
)
}
Composable State for UI-Specifics
Individual Composables can manage their own presentation state:
@Composable
fun ProductItem(
product: Product,
onProductClick: (Product) -> Unit
) {
var isImageLoaded by remember { mutableStateOf(false) }
var showFullDescription by remember { mutableStateOf(false) }
Card(
modifier = Modifier.clickable { onProductClick(product) }
) {
Column {
AsyncImage(
model = product.imageUrl,
contentDescription = null,
onSuccess = { isImageLoaded = true }
)
if (!isImageLoaded) {
CircularProgressIndicator()
}
Text(
text = if (showFullDescription) {
product.fullDescription
} else {
product.shortDescription
},
modifier = Modifier.clickable {
showFullDescription = !showFullDescription
}
)
}
}
}
State Hoisting: The Crucial Link
State hoisting is the practice of moving state up to the lowest common ancestor that needs it. The decision of where to hoist—to a parent Composable or all the way to the ViewModel—depends on several factors:
Hoist to Parent Composable when:
- Multiple child Composables need to coordinate
- State is still purely UI-related
- No business logic is involved
Hoist to ViewModel when:
- State needs to survive configuration changes
- Business logic is required for state management
- State affects multiple screens or needs external data sources
Events and Actions
Composables should communicate with ViewModels through clear event interfaces:
class ProductListViewModel : ViewModel() {
fun onSearchQueryChanged(query: String) { /* handle search */ }
fun onFilterSelected(filter: ProductFilter) { /* handle filter */ }
fun onProductClicked(productId: String) { /* handle navigation */ }
}
This pattern keeps the ViewModel focused on business logic while maintaining clear boundaries between UI and data layers.
VII. When to Choose What: A Practical Decision Guide
Making the right choice between remember vs ViewModel becomes easier with clear decision criteria:
Use Composable State (remember
/rememberSaveable
) when:
- State is purely UI-related (tooltip visibility, animation progress, focus states)
- State doesn't need to survive configuration changes (or
rememberSaveable
is sufficient) - State is not needed by other screens or distant Composables
- No complex business logic is tied to this state
- You're dealing with simple form inputs during user interaction
- State represents temporary UI modes (expanded/collapsed, selected/unselected)
Use ViewModel when:
- State needs to survive configuration changes automatically
- State represents screen-level data or is shared by multiple Composables on a screen
- Business logic is involved in producing or mutating the state
- Data needs to be fetched from external sources (network, database, repositories)
- You need to separate concerns and improve testability of logic
- State requires coordination between multiple data sources
- You're implementing navigation logic or screen-to-screen communication
Use Both (ViewModel + Composable State) when:
- ViewModel manages overall screen data and business logic
- Individual Composables manage their own transient, UI-specific presentation state
- You want the benefits of both approaches—business logic separation with UI responsiveness
- Building complex screens with both persistent and transient state requirements
VIII. Advanced Considerations & Common Pitfalls
SavedStateHandle in ViewModels
For ViewModels that need to survive process death with complex state, SavedStateHandle
provides a solution:
class DetailViewModel @Inject constructor(
savedStateHandle: SavedStateHandle
) : ViewModel() {
private val productId: String = savedStateHandle.get<String>("productId") ?: ""
}
Common Anti-Patterns
Avoid these mistakes:
- Over-ViewModeling: Putting every tiny UI state into ViewModel unnecessarily creates bloated ViewModels and reduces UI responsiveness
- Business logic in Composables: Complex data transformations and API calls should live in ViewModels, not Composables with
remember
- Forgetting lifecycle awareness: Always use
collectAsStateWithLifecycle()
instead ofcollectAsState()
to prevent unnecessary recompositions when the UI is not visible
Memory and Performance Considerations
Remember that ViewModels live longer than individual Composables, so be mindful of holding references to UI-specific objects that might cause memory leaks.
IX. Conclusion: Architecting Robust Compose UIs
Mastering Jetpack Compose state management requires understanding that Compose State vs ViewModel isn't about choosing sides—it's about choosing the right tool for each specific need. ViewModels excel at managing screen-level state, business logic, and data that must survive configuration changes. Composable state with remember
provides the perfect solution for transient, UI-specific state that makes your interfaces responsive and delightful.
The most successful Compose applications use both approaches strategically: ViewModels as the backbone for business logic and persistent state, with Composable state handling the nuanced UI interactions that make great user experiences. This architectural approach delivers maintainable, testable, and performant applications that scale with your needs.
Understanding these patterns will make you a more effective Android developer and help you build Compose UIs that are both robust and delightful to use.
X. FAQ (Frequently Asked Questions)
Q1: Can I use remember
inside a ViewModel?
A: No, remember
is a Composable function and can only be called from within other Composable functions. ViewModels are not Composables and should use standard Kotlin/Android state management approaches like StateFlow
or LiveData
.
Q2: Is StateFlow
the only way to expose state from ViewModel to Compose?
A: No, while StateFlow
with collectAsStateWithLifecycle()
is the recommended modern approach, you can also use LiveData
with observeAsState()
, or even expose State<T>
directly. However, StateFlow
provides better Coroutines integration and lifecycle awareness.
Q3: What about rememberSaveable
vs. ViewModel for configuration changes?
A: Use rememberSaveable
for simple, Parcelable state within individual Composables that needs to survive configuration changes. Choose ViewModel for more complex state, non-Parcelable data, or when business logic is involved in state management.
Q4: How does Hilt or Koin fit in with ViewModels in Compose?
A: Dependency injection frameworks like Hilt integrate seamlessly with Compose ViewModels. Use hiltViewModel()
or similar functions to inject ViewModels into your Composables, enabling proper dependency management and testability while maintaining the ViewModel lifecycle benefits.