CPS251 Android Development by Scott Shaper

Recomposition

Introduction

Have you ever wondered how your app updates when you click a button or change some text? That's where recomposition comes in! Think of recomposition like updating a picture - when something changes, Compose redraws just the parts that need to change, making your app fast and efficient. We'll learn how Compose smartly updates only what needs to change in your UI.

Quick Reference

Concept Description Common Use
Recomposition Smart UI updates that only change what's needed Updating UI when data changes
remember Keeps state between recompositions Storing data that needs to persist
rememberSaveable Keeps state after screen rotation Preserving data during configuration changes

When Recomposition Occurs

Recomposition happens automatically in these situations:

  • When a state variable created with mutableStateOf changes value
  • When a composable's parameters change
  • When a parent composable recomposes

You don't need to manually trigger recomposition - Compose handles this automatically when you use state variables. For example:

@Composable
fun Counter() {
    // When this state changes, Compose automatically recomposes
    // any composables that read this value
    var count by remember { mutableStateOf(0) }

    Column {
        // This Text will automatically recompose when count changes
        Text("Count: $count")
        Button(onClick = { count++ }) {
            Text("Add One")
        }
    }
}

In this example, when you click the button and count changes, Compose automatically recomposes the Text that displays the count. You don't need to do anything special to make this happen - it's built into how Compose works!

Common Options

Option What It Does When to Use It
remember Preserves state during recomposition For normal state management
rememberSaveable Preserves state after screen rotation When state needs to survive configuration changes
mutableStateOf Creates observable state When you need values that can change

Practical Examples

Basic Counter Example

This example shows how recomposition works with a simple counter. Think of it like a scoreboard that only updates the number, not the whole display.

@Composable
fun Counter() {
    // This state will trigger recomposition when it changes
    var count by remember { mutableStateOf(0) }

    Column {
        Text("Count: $count")
        Button(onClick = { count++ }) {
            Text("Add One")
        }
    }
}

What This Example Is Doing

Counter keeps count in state with remember { mutableStateOf(0) }. The column shows "Count: $count" and an "Add One" button. When you click the button, count++ runs, the state changes, and Compose recomposes only the composables that read count—so the Text updates to the new number. The button and the rest of the tree can be reused without being re-created. You don't call any "refresh" method; recomposition is automatic when state changes.

Common Recomposition Patterns

Here are some practical ways to use recomposition in your apps:

Updating Text
@Composable
fun TextUpdater() {
    var text by remember { mutableStateOf("Hello") }
    
    Column {
        Text(text)  // Recomposes when text changes
        TextField(
            value = text,
            onValueChange = { text = it }
        )
    }
}

What This Example Is Doing

TextUpdater keeps text in state (starting as "Hello"). The TextField shows that value and calls onValueChange = { text = it } when the user types, so text updates. Because the first Text reads text, it recomposes and shows the new string. So the displayed text and the text field stay in sync through state and recomposition.

Showing/Hiding Content
@Composable
fun ShowHideExample() {
    var isVisible by remember { mutableStateOf(true) }
    
    Column {
        if (isVisible) {
            Text("This text appears/disappears")
        }
        Button(onClick = { isVisible = !isVisible }) {
            Text(if (isVisible) "Hide" else "Show")
        }
    }
}

What This Example Is Doing

ShowHideExample keeps isVisible in state (initially true). When true, the column shows the text "This text appears/disappears" and a "Hide" button; when false, that text is not in the composition and the button says "Show." Clicking the button toggles isVisible, so Compose recomposes and either adds or removes the Text. So the UI structure itself changes based on state.

Changing Colors
@Composable
fun ColorChanger() {
    var isRed by remember { mutableStateOf(false) }
    
    Box(
        modifier = Modifier
            .size(100.dp)
            .background(if (isRed) Color.Red else Color.Blue)
            .clickable { isRed = !isRed }
    )
}

What This Example Is Doing

ColorChanger keeps isRed in state (initially false). A 100 dp box’s background is red when isRed is true and blue when false; the box is clickable and toggles isRed. So each tap triggers recomposition, and the box’s background modifier is re-evaluated and redraws with the new color.

Handling Screen Rotation

This example shows how to handle state during screen rotation. Think of it like preserving your work when you turn your paper sideways.

@Composable
fun RotationExample() {
    // This will reset when screen rotates
    var count by remember { mutableStateOf(0) }
    
    // This will keep its value even after rotation
    var savedCount by rememberSaveable { mutableStateOf(0) }
    
    Column {
        Text("Regular count: $count")  // Resets on rotation
        Text("Saved count: $savedCount")  // Keeps value after rotation
        
        Button(onClick = { 
            count++
            savedCount++
        }) {
            Text("Add One")
        }
    }
}

What This Example Is Doing

RotationExample shows two counters: one with remember { mutableStateOf(0) } and one with rememberSaveable { mutableStateOf(0) }. Both start at 0; one button increments both. When you rotate the device, the activity is re-created. The "Regular count" resets to 0 because remember only survives recomposition, not process/activity recreation. The "Saved count" keeps its value because rememberSaveable saves and restores state across configuration changes (like rotation). So you see the difference between in-memory state and state that survives rotation.

Smart Counter with Conditional Updates

This example shows how Compose can be smart about what it updates. Think of it like a smart display that only changes what needs to change.

@Composable
    fun SmartCounter() {
        var count by remember { mutableStateOf(0) }
        
        // This Text will recompose when count changes
        Text("Count: $count")
        
        // This Text will never recompose because it's static
        Text("This text never changes!")
        
        // This Text will only recompose when count is even
        Text("Count is ${if (count % 2 == 0) "even" else "odd"}")
        
        Button(onClick = { count++ }) {
            Text("Add One")
        }
    }
    

What This Example Is Doing

SmartCounter has one count state and three text lines. The first Text("Count: $count") reads count, so it recomposes every time count changes. The second Text("This text never changes!") does not read any state, so Compose can skip recomposing it. The third Text reads count (in the "even"/"odd" expression), so it recomposes when count changes. So only the parts that depend on count are recomposed; the static text is left alone.

Profile Card with Smart Updates

This example demonstrates how different parts of the UI can update independently. Think of it like a smart form where only the changed fields update.

@Composable
            fun ProfileCard() {
                // State variables with remember/rememberSaveable to maintain state across recompositions
                var name by rememberSaveable { mutableStateOf("John") }
                var age by remember { mutableStateOf(20) }
                var favoriteColor by remember { mutableStateOf("Blue") }
            
                // Predefined lists for cycling through different values
                val names = listOf("John", "Jane", "Alex", "Sam")
                val colors = listOf(
                    Pair(Color(0xFF2196F3), "Blue"),      // Material Blue
                    Pair(Color(0xFF4CAF50), "Green"),     // Material Green
                    Pair(Color(0xFFF44336), "Red"),       // Material Red
                    Pair(Color(0xFF9C27B0), "Purple")     // Material Purple
                )
            
                // Indices for cycling through the predefined lists
                var nameIndex by remember { mutableStateOf(0) }
                var colorIndex by remember { mutableStateOf(0) }
            
                // Main layout container
                Column(
                    modifier = Modifier
                        .fillMaxSize()
                        .padding(16.dp),
                    horizontalAlignment = Alignment.CenterHorizontally,
                    verticalArrangement = Arrangement.spacedBy(16.dp)
                ) {
                    // Card title
                    Text(
                        text = "Profile Card",
                        style = MaterialTheme.typography.headlineMedium,
                        modifier = Modifier.padding(bottom = 8.dp)
                    )
            
                    // Profile information card with dynamic background color
                    Card(
                        modifier = Modifier
                            .fillMaxWidth()
                            .weight(1f),
                        colors = CardDefaults.cardColors(
                            containerColor = colors[colorIndex].first
                        )
                    ) {
                        // Profile information container
                        Column(
                            modifier = Modifier
                                .padding(16.dp)
                                .fillMaxWidth(),
                            horizontalAlignment = Alignment.CenterHorizontally,
                            verticalArrangement = Arrangement.spacedBy(16.dp)
                        ) {
                            // Profile information text fields
                            Text(
                                text = "Name: $name",
                                style = MaterialTheme.typography.bodyLarge,
                                color = Color.White
                            )
            
                            Text(
                                text = "Age: $age",
                                style = MaterialTheme.typography.bodyLarge,
                                color = Color.White
                            )
            
                            Text(
                                text = "Favorite Color: ${colors[colorIndex].second}",
                                style = MaterialTheme.typography.bodyLarge,
                                color = Color.White
                            )
                        }
                    }
                    ...more code is here...
                }
            }
            

        

What This Example Is Doing

ProfileCard shows a card with name, age, and favorite color. The name uses rememberSaveable so it survives screen rotation; the background color (and color index) use remember so they reset when the configuration changes. Buttons let you cycle through predefined names and colors. So when you rotate the device, the name stays (e.g. "Alex") but the card color goes back to the default (e.g. blue), illustrating the difference between the two state holders.

How these examples render

The snippets above are only part of the code; to see and run the full project, go to my GitHub page and open the chapter6 recomposition.kt file.

The first image shows the app after the name button has been clicked so "Alex" appears and the background color was set to green. The second image shows the app after the device is rotated. The name is still "Alex" because it uses rememberSaveable, but the background color is blue again (the initial color) because the color index uses remember, which does not survive configuration changes. So rememberSaveable preserves state across rotation; remember does not.

NOTE: The rotated screen shows a partial view of the card.

Recomposition Example Recomposition Example

Learning Aids

Tips for Success

  • Only update what needs to change
  • Keep state close to where it's used
  • Use remember for normal state management
  • Use rememberSaveable when state needs to survive rotation

Common Mistakes to Avoid

  • Putting state in the wrong place
  • Forgetting to use remember
  • Updating more than necessary
  • Not handling screen rotation properly

Best Practices

  • Keep recompositions minimal
  • Use appropriate state management
  • Handle configuration changes properly
  • Test your UI with different scenarios