Skip to main content
mobile

Jetpack Compose State Management: remember() vs mutableStateOf() Complete Guide

Angry Shark Studio
6 min read
compose jetpack-compose android state remember mutablestateof beginner tutorial

Difficulty Level: Intermediate

What You’ll Learn

  • The difference between remember() and mutableStateOf()
  • 5 common state management patterns and mistakes
  • Advanced patterns with derivedStateOf and rememberSaveable
  • Best practices for complex UI state management
  • Performance optimization for state-heavy Compose apps

Quick Answer: Use var state by remember { mutableStateOf(value) } for most UI state. mutableStateOf() makes state observable, remember() preserves it across recompositions. Combine both for reactive, persistent state.

State management in Jetpack Compose is one of the most confusing topics for developers transitioning from the View system. The relationship between remember() and mutableStateOf() causes frequent issues: UI state disappearing on recomposition or never updating despite value changes.

This confusion stems from a fundamental misunderstanding of how Compose handles state differently from traditional Android development. Even experienced developers struggle with these concepts initially.

Understanding the distinct roles of remember() and mutableStateOf() is crucial for building reliable Compose applications. This guide clarifies their purposes and demonstrates proper usage patterns.

The most common source of Compose state bugs is misunderstanding when and how to combine remember() with mutableStateOf(). This confusion leads to lost state, unnecessary recompositions, and UI bugs that are difficult to debug.

This guide clarifies the relationship between these two functions and demonstrates proper usage patterns through practical examples.

The Problem: State That Disappears or Never Updates

New Compose developers often write code like this and wonder why their UI doesn’t work correctly:

// Bad: Common state management mistakes
@Composable
private fun CounterScreen() {
    // Mistake #1: No remember - state resets on every recomposition
    var count = mutableStateOf(0)
    
    Column {
        Text("Count: ${count.value}")
        Button(onClick = { count.value++ }) {
            Text("Increment")
        }
    }
}

@Composable
private fun UserProfileScreen() {
    // Mistake #2: remember without mutableStateOf - state never changes
    val userName = remember { "John Doe" }
    
    Column {
        Text("User: $userName")
        Button(onClick = { 
            // This does nothing! userName can't be changed
            userName = "Jane Doe" // Compilation error
        }) {
            Text("Change Name")
        }
    }
}

@Composable
private fun TodoScreen() {
    // Mistake #3: Wrong delegation pattern
    val todos = remember { mutableStateOf(listOf<String>()) }
    
    Column {
        Text("Todos: ${todos.value.size}")
        Button(onClick = { 
            // Verbose and error-prone
            todos.value = todos.value + "New Todo"
        }) {
            Text("Add Todo")
        }
    }
}

These mistakes cause state to either reset unexpectedly or never update at all, creating frustrating bugs.

Understanding the Building Blocks

What is mutableStateOf()?

mutableStateOf() creates a state holder that Compose can observe for changes:

// Creates observable state
val state = mutableStateOf(0)

// Reading the value
val currentValue = state.value

// Writing the value (triggers recomposition)
state.value = 5

What is remember()?

remember() preserves values across recompositions:

// Without remember - value recreated every recomposition
val expensiveValue = calculateExpensiveValue() // Recalculated each time

// With remember - value preserved across recompositions  
val expensiveValue = remember { calculateExpensiveValue() } // Calculated once

The Key Insight: They Solve Different Problems

  • mutableStateOf(): Makes Compose observe changes (reactivity)
  • remember(): Preserves values across recompositions (persistence)

For UI state, you usually need both!

Correct State Patterns

Pattern 1: Basic State with remember + mutableStateOf

// Good: Proper state management
@Composable
private fun CounterScreen() {
    // Combines both: preserved AND observable
    var count by remember { mutableStateOf(0) }
    
    Column {
        Text("Count: $count")
        Button(onClick = { count++ }) { // Clean syntax with 'by' delegate
            Text("Increment")
        }
        Button(onClick = { count-- }) {
            Text("Decrement")
        }
    }
}

Why this works:

  • mutableStateOf(0) creates observable state
  • remember { } preserves it across recompositions
  • by delegate provides clean syntax

Pattern 2: Complex State Objects

// Good: Managing complex state
private data class UserProfile(
    val name: String = "",
    val email: String = "",
    val age: Int = 0,
    val isVerified: Boolean = false
)

@Composable
private fun UserProfileForm() {
    var profile by remember { 
        mutableStateOf(UserProfile()) 
    }
    
    Column {
        OutlinedTextField(
            value = profile.name,
            onValueChange = { newName ->
                profile = profile.copy(name = newName) // Immutable updates
            },
            label = { Text("Name") }
        )
        
        OutlinedTextField(
            value = profile.email,
            onValueChange = { newEmail ->
                profile = profile.copy(email = newEmail)
            },
            label = { Text("Email") }
        )
        
        Row {
            Checkbox(
                checked = profile.isVerified,
                onCheckedChange = { isChecked ->
                    profile = profile.copy(isVerified = isChecked)
                }
            )
            Text("Verified")
        }
        
        Button(
            onClick = { saveProfile(profile) },
            enabled = profile.name.isNotBlank() && profile.email.isNotBlank()
        ) {
            Text("Save Profile")
        }
    }
}

Pattern 3: List State Management

// Good: Managing list state
@Composable
private fun TodoListScreen() {
    var todos by remember { mutableStateOf(listOf<String>()) }
    var newTodoText by remember { mutableStateOf("") }
    
    Column {
        Row {
            OutlinedTextField(
                value = newTodoText,
                onValueChange = { newTodoText = it },
                modifier = Modifier.weight(1f),
                label = { Text("New Todo") }
            )
            Button(
                onClick = {
                    if (newTodoText.isNotBlank()) {
                        todos = todos + newTodoText // Immutable append
                        newTodoText = "" // Clear input
                    }
                }
            ) {
                Text("Add")
            }
        }
        
        LazyColumn {
            itemsIndexed(todos) { index, todo ->
                TodoItem(
                    text = todo,
                    onDelete = {
                        todos = todos.filterIndexed { i, _ -> i != index }
                    }
                )
            }
        }
    }
}

@Composable
private fun TodoItem(
    text: String,
    onDelete: () -> Unit
) {
    Row(
        modifier = Modifier
            .fillMaxWidth()
            .padding(8.dp),
        horizontalArrangement = Arrangement.SpaceBetween
    ) {
        Text(text)
        IconButton(onClick = onDelete) {
            Icon(Icons.Default.Delete, contentDescription = "Delete")
        }
    }
}

Common Mistakes and Solutions

Mistake 1: Forgetting remember

// Bad: State resets on every recomposition
@Composable
private fun SearchScreen() {
    var searchQuery = mutableStateOf("") // Lost on recomposition!
    
    // UI code...
}

// Good: State preserved across recompositions
@Composable
private fun SearchScreen() {
    var searchQuery by remember { mutableStateOf("") }
    
    // UI code...
}

Mistake 2: Using remember Without State

// Bad: Value can't change, no reactivity
@Composable
private fun GreetingScreen() {
    val greeting = remember { "Welcome" } // Static value
    
    Text(greeting) // Will never change
}

// Good: Use remember for expensive calculations, not simple values
@Composable
private fun GreetingScreen() {
    val currentTime = remember { System.currentTimeMillis() }
    val formattedTime = remember(currentTime) { 
        SimpleDateFormat("HH:mm:ss").format(Date(currentTime))
    }
    
    Text("App started at: $formattedTime")
}

Mistake 3: Mutating State Objects Directly

// Bad: Direct mutation doesn't trigger recomposition
@Composable
private fun UserListScreen() {
    val users = remember { mutableStateOf(mutableListOf<User>()) }
    
    Button(onClick = {
        users.value.add(User("New User")) // Compose won't detect this change
    }) {
        Text("Add User")
    }
}

// Good: Create new instances for state updates
@Composable
private fun UserListScreen() {
    var users by remember { mutableStateOf(listOf<User>()) }
    
    Button(onClick = {
        users = users + User("New User") // New list triggers recomposition
    }) {
        Text("Add User")
    }
}

Advanced State Patterns

Pattern 1: Computed State with derivedStateOf

// Good: Efficient computed state
@Composable
private fun ShoppingCartScreen() {
    var cartItems by remember { mutableStateOf(listOf<CartItem>()) }
    
    // Only recomputes when cartItems changes
    val totalPrice by remember(cartItems) {
        derivedStateOf {
            cartItems.sumOf { it.price * it.quantity }
        }
    }
    
    val itemCount by remember(cartItems) {
        derivedStateOf {
            cartItems.sumOf { it.quantity }
        }
    }
    
    Column {
        Text("Items: $itemCount")
        Text("Total: $${totalPrice}")
        
        LazyColumn {
            items(cartItems) { item ->
                CartItemRow(item) { updatedItem ->
                    cartItems = cartItems.map { 
                        if (it.id == updatedItem.id) updatedItem else it 
                    }
                }
            }
        }
    }
}

Pattern 2: State with Keys for Conditional Remember

// Good: State that depends on external data
@Composable
private fun UserProfileScreen(userId: String) {
    // State recreated when userId changes
    var profile by remember(userId) { 
        mutableStateOf(UserProfile()) 
    }
    
    var isLoading by remember(userId) { 
        mutableStateOf(true) 
    }
    
    // Load profile when userId changes
    LaunchedEffect(userId) {
        isLoading = true
        profile = loadUserProfile(userId)
        isLoading = false
    }
    
    if (isLoading) {
        CircularProgressIndicator()
    } else {
        ProfileContent(profile) { updatedProfile ->
            profile = updatedProfile
        }
    }
}

Pattern 3: State Persistence with rememberSaveable

// Good: State survives configuration changes
@Composable
private fun FormScreen() {
    // Survives screen rotation, process death, etc.
    var formData by rememberSaveable { 
        mutableStateOf(FormData()) 
    }
    
    var isSubmitting by rememberSaveable { 
        mutableStateOf(false) 
    }
    
    Column {
        OutlinedTextField(
            value = formData.name,
            onValueChange = { formData = formData.copy(name = it) },
            label = { Text("Name") }
        )
        
        OutlinedTextField(
            value = formData.email,
            onValueChange = { formData = formData.copy(email = it) },
            label = { Text("Email") }
        )
        
        Button(
            onClick = {
                isSubmitting = true
                submitForm(formData) { success ->
                    isSubmitting = false
                    if (success) {
                        formData = FormData() // Reset form
                    }
                }
            },
            enabled = !isSubmitting
        ) {
            if (isSubmitting) {
                CircularProgressIndicator(modifier = Modifier.size(16.dp))
            } else {
                Text("Submit")
            }
        }
    }
}

Best Practices Summary

Do:

  1. Combine remember + mutableStateOf for UI state
  2. Use by delegate for cleaner syntax
  3. Create new objects for state updates (immutability)
  4. Use derivedStateOf for computed state
  5. Use rememberSaveable for important user data
  6. Key remember when state depends on external data

Don’t:

  1. Use mutableStateOf without remember (state will reset)
  2. Use remember for simple static values (unnecessary)
  3. Mutate state objects directly (won’t trigger recomposition)
  4. Forget to handle loading states in async operations
  5. Over-use state (prefer stateless composables when possible)

Quick Reference Guide

PatternUse CaseExample
var state by remember { mutableStateOf(value) }Basic mutable stateCounters, toggles, form inputs
remember { expensiveCalculation() }Expensive computationsComplex object creation
remember(key) { mutableStateOf(value) }State dependent on external dataUser profiles, search results
derivedStateOf { computation }Computed stateTotals, filtered lists
rememberSaveable { mutableStateOf(value) }Persistent stateForm data, user preferences

Frequently Asked Questions

Q: What’s the difference between remember and mutableStateOf?

A: mutableStateOf() creates observable state that triggers recomposition. remember() preserves values across recompositions. You usually need both together.

Q: When should I use rememberSaveable instead of remember?

A: Use rememberSaveable for important user data that should survive configuration changes (screen rotation) and process death (like form inputs).

Q: Why does my state reset when I navigate away and back?

A: You’re likely missing remember. Without it, state gets recreated on every recomposition. Use remember { mutableStateOf() } to persist state.

Q: Should I use derivedStateOf for computed values?

A: Yes, for expensive computations that depend on other state. derivedStateOf only recalculates when its dependencies change, improving performance.

Q: Can I use mutableStateOf without remember?

A: Technically yes, but your state will reset on every recomposition, making your UI non-functional. Always combine them for UI state.

Conclusion

Understanding the relationship between remember() and mutableStateOf() is crucial for effective Compose development. Remember this simple rule: mutableStateOf() makes state observable, remember() makes it persistent. This becomes even more important when working with complex UIs where proper @Composable annotations are essential and Kotlin null safety prevents state-related crashes.

Most UI state needs both: you want Compose to react to changes AND preserve state across recompositions. The by delegate syntax makes this pattern clean and readable.

Start with basic var state by remember { mutableStateOf(initialValue) } for most cases, then learn the expert patterns as your apps become more complex. Master the fundamentals of Kotlin’s null safety alongside Compose state management for reliable applications. If you’re transitioning from Unity development, these state patterns will feel familiar yet more effective. With these fundamentals solid, you’ll write bug-free Compose UIs with confidence.

Need help building reliable Android apps with Jetpack Compose? Contact Angry Shark Studio for expert Android development services, or explore our mobile development portfolio to see how we’ve built performant, state-driven Compose applications.

Related Reading:

Angry Shark Studio Logo

About Angry Shark Studio

Angry Shark Studio is a professional Unity AR/VR development studio specializing in mobile multiplatform applications and AI solutions. Our team includes Unity Certified Expert Programmers with extensive experience in AR/VR development.

Related Articles

More Articles

Explore more insights on Unity AR/VR development, mobile apps, and emerging technologies.

View All Articles

Need Help?

Have questions about this article or need assistance with your project?

Get in Touch