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 observed
  • mutableStateOf(): Creates a mutable state holder
  • remember: Preserves state across recompositions within the Composition
  • rememberSaveable: Like remember, 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 of collectAsState() 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.