Mastering Derived State with DerivedStateOf in Jetpack Compose

Jetpack Compose is a modern toolkit for building native Android UI. It simplifies UI development with a declarative approach and introduces various state management techniques. One such technique is derivedStateOf, which optimizes state derivations, enhancing performance by minimizing unnecessary recompositions.

What is Derived State?

In Jetpack Compose, derived state refers to a state that is computed or derived from one or more other states. It is essentially a way to create a new state that reacts to changes in the source state(s). This is crucial for transforming and combining states in a performant manner.

Why Use derivedStateOf?

Using derivedStateOf offers several benefits:

  • Performance Optimization: derivedStateOf avoids unnecessary recompositions by only updating the derived state when the source state(s) it depends on change.
  • Readability and Maintainability: By clearly defining state derivations, the code becomes more readable and easier to maintain.
  • Efficiency: Computation only occurs when necessary, preventing expensive operations from being performed repeatedly.

How derivedStateOf Works

derivedStateOf takes a lambda function that performs a computation based on other states. The result of this computation is a state that is automatically updated whenever its dependent states change. However, it does this efficiently by remembering its dependencies and only recomputing when those specific dependencies change.

Implementing derivedStateOf in Jetpack Compose

Here’s how you can use derivedStateOf in Jetpack Compose:

Step 1: Add Dependencies (If Needed)

Ensure you have the necessary Compose dependencies in your build.gradle file:

dependencies {
    implementation("androidx.compose.ui:ui:1.5.0") // or newer
    implementation("androidx.compose.runtime:runtime:1.5.0") // or newer
    implementation("androidx.activity:activity-compose:1.8.0")
}

Step 2: Use derivedStateOf in a Composable Function

Create a composable function that uses derivedStateOf to derive a state from one or more other states.


import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.derivedStateOf
import androidx.compose.material.Text
import androidx.compose.material.Button
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.ui.unit.dp

@Composable
fun DerivedStateExample() {
    var text1 by remember { mutableStateOf("Hello") }
    var text2 by remember { mutableStateOf("World") }

    val combinedText by derivedStateOf {
        "$text1 $text2"
    }

    Column(modifier = Modifier.padding(16.dp)) {
        Text(text = "Text 1: $text1")
        Text(text = "Text 2: $text2")
        Text(text = "Combined Text: $combinedText")

        Button(onClick = { text1 = "Compose" }) {
            Text("Update Text 1")
        }
        Button(onClick = { text2 = "Android" }) {
            Text("Update Text 2")
        }
    }
}

@Preview(showBackground = true)
@Composable
fun PreviewDerivedStateExample() {
    DerivedStateExample()
}

In this example:

  • text1 and text2 are two independent mutable states.
  • combinedText is a derived state that combines text1 and text2.
  • The lambda expression inside derivedStateOf is only re-executed when either text1 or text2 changes, preventing unnecessary recompositions.

Advanced Usage

Combining Multiple States

You can derive a state from multiple states, making complex transformations easier to manage.


import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.derivedStateOf
import androidx.compose.material.Text
import androidx.compose.material.Button
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.ui.unit.dp

@Composable
fun MultiStateExample() {
    var number1 by remember { mutableStateOf(10) }
    var number2 by remember { mutableStateOf(5) }

    val result by derivedStateOf {
        number1 * number2
    }

    Column(modifier = Modifier.padding(16.dp)) {
        Text(text = "Number 1: $number1")
        Text(text = "Number 2: $number2")
        Text(text = "Result: $result")

        Button(onClick = { number1 += 5 }) {
            Text("Increment Number 1")
        }
        Button(onClick = { number2 += 2 }) {
            Text("Increment Number 2")
        }
    }
}

@Preview(showBackground = true)
@Composable
fun PreviewMultiStateExample() {
    MultiStateExample()
}

Conditional Derivation

Derive states based on conditions, ensuring the UI responds contextually to different scenarios.


import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.derivedStateOf
import androidx.compose.material.Text
import androidx.compose.material.Button
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.ui.unit.dp

@Composable
fun ConditionalDerivationExample() {
    var isLoggedIn by remember { mutableStateOf(false) }

    val message by derivedStateOf {
        if (isLoggedIn) {
            "Welcome back!"
        } else {
            "Please log in."
        }
    }

    Column(modifier = Modifier.padding(16.dp)) {
        Text(text = message)

        Button(onClick = { isLoggedIn = !isLoggedIn }) {
            Text(if (isLoggedIn) "Log Out" else "Log In")
        }
    }
}

@Preview(showBackground = true)
@Composable
fun PreviewConditionalDerivationExample() {
    ConditionalDerivationExample()
}

Real-World Example: Search Functionality

Consider a search feature where you filter a list of items based on a search query. You can use derivedStateOf to efficiently derive the filtered list from the original list and the search query.


import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.derivedStateOf
import androidx.compose.material.Text
import androidx.compose.material.TextField
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.ui.unit.dp
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items

@Composable
fun SearchExample(items: List) {
    var searchQuery by remember { mutableStateOf("") }

    val filteredItems by derivedStateOf {
        items.filter { it.contains(searchQuery, ignoreCase = true) }
    }

    Column(modifier = Modifier.padding(16.dp)) {
        TextField(
            value = searchQuery,
            onValueChange = { searchQuery = it },
            label = { Text("Search") }
        )

        LazyColumn {
            items(filteredItems) { item ->
                Text(text = item, modifier = Modifier.padding(8.dp))
            }
        }
    }
}

@Preview(showBackground = true)
@Composable
fun PreviewSearchExample() {
    val items = listOf("Apple", "Banana", "Cherry", "Date", "Fig")
    SearchExample(items = items)
}

In this example:

  • searchQuery is the input from the user.
  • filteredItems is a derived state that only updates when searchQuery or items changes, making the search efficient.

Best Practices

  • Keep Computations Simple: The lambda function inside derivedStateOf should be simple and fast to avoid blocking the UI thread.
  • Minimize Dependencies: Only depend on the states that are absolutely necessary to reduce unnecessary recompositions.
  • Use remember: When referencing objects within the lambda, use remember to avoid creating new objects on every recomposition.

Conclusion

derivedStateOf is a powerful tool in Jetpack Compose for creating derived states efficiently. By understanding how it works and applying it correctly, you can significantly improve the performance and maintainability of your Compose applications. Whether it’s combining multiple states, implementing conditional logic, or optimizing complex transformations, derivedStateOf helps ensure your UI remains responsive and efficient.