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 stateremember { }
preserves it across recompositionsby
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:
- Combine
remember
+mutableStateOf
for UI state - Use
by
delegate for cleaner syntax - Create new objects for state updates (immutability)
- Use
derivedStateOf
for computed state - Use
rememberSaveable
for important user data - Key
remember
when state depends on external data
Don’t:
- Use
mutableStateOf
withoutremember
(state will reset) - Use
remember
for simple static values (unnecessary) - Mutate state objects directly (won’t trigger recomposition)
- Forget to handle loading states in async operations
- Over-use state (prefer stateless composables when possible)
Quick Reference Guide
Pattern | Use Case | Example |
---|---|---|
var state by remember { mutableStateOf(value) } | Basic mutable state | Counters, toggles, form inputs |
remember { expensiveCalculation() } | Expensive computations | Complex object creation |
remember(key) { mutableStateOf(value) } | State dependent on external data | User profiles, search results |
derivedStateOf { computation } | Computed state | Totals, filtered lists |
rememberSaveable { mutableStateOf(value) } | Persistent state | Form 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:

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