Blog My Happy PlaceGemma Lara Savill

Recomposition in Jetpack Compose. Imposing stability to improve your Android app performance.

By Gemma Lara Savill
Published at January 23, 2024

Recomposition

Improving your Android app performance is a big deal. There are many things to keep in mind and a lot of scenarios you can fine tune.

This post is about how you can increase the stability of your app and improve its performance and overall user experience.

A quick win is to use Android Studio's Layout Inspector to monitor how your Compose UI is performing.
You can access Layout Inspector from the Tools menu in Android Studio.

Layout Inspector will show you how many times the composables on your UI screen are recomposed (redrawn) or skipped.
You will see a tree with the composables that make up your layout and two columns to the right. The column on the left marks the times each composable has been recomposed and the column further to the right will show you the times it has been skipped.
Even better, you can configure the inspector to highlight the parts of the screen that are recomposing.

Ideally you only want to recompose a composable when it's internal state changes. You want it to be redrawn on the screen to show the user its new state. A Switch composable comes to mind as an easy example.
What you don't want is a whole part of the screen being recomposed when the state has not changed. To improve performance we need to aim for efficient recomposition.

Compose is quite smart at recognizing when a composable has changed or not. When a composable is considered stable it will not be redrawn. If a composable has stable parameters that have not changed, Compose skips it.
If a composable has unstable parameters, Compose always recomposes it when it recomposes the component's parent.

Using layout inspector can you help find cases where the Compose system might need some help deciding if a Compose needs recomposition or if it is OK to skip it.

Primitive types like String and Boolean are automatically marked as immutable.
If you are using a data class in your composable, it will be flagged by Compose as immutable if all its parameters are primitives defined with the val keyword.

But what if one of the parameters is a List? Even if your List is stable and never changes, when a recomposition takes place your Composable will be reloaded.

You can prevent this by marking your List as @Stable. You can't annotate parameters, but you can wrap it in a data state class and annotate that as @Stable.
Here you are telling the Compose compiler that this has not changed.

So the options we have are:

  • Stable: Indicates a type whose properties can change after construction. If and when those properties change during runtime, Compose becomes aware of those changes.
  • Immutable: Compose marks a type as immutable if the value of its properties can never change and all methods are referentially transparent. All primitive types are marked as immutable.

In this example you can use layout inspector to see that the switch composable is recomposed when it's state changes, but the DefaultCard is also recomposed as it is using a List.

@Composable
fun DefaultCard(data: List<String>) {
    Text(data.toString())
}

This is an unwanted recomposition, as the state of the card has not changed.

You can make Compose skip the card recomposition by marking it as Stable or Immutable.

Here are a couple of examples:

@Composable
fun StableCard(data: StableState) {
    Text(data.status.toString())
}

@Stable
data class StableState(
    val status: List<String>
)

@Composable
fun ImmutableCard(data: ImmutableState) {
    Text(data.status.toString())
}

@Immutable
data class ImmutableState(
    val status: List<String>
)

So you can use Layout Inspector to find unwanted or unexpected recompositions and annotate your classes with @Stable or @Immutable to fix it.

Going through your screens with Layout Inspector to review how your recomposition, and making sure you are only recomposing when necessary, will make your app perform better and will improve the overall user experience.

Here is the whole example shown in the Layout Inspector example image:

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            ComposeStabilityTheme {
                Column(
                    Modifier
                        .fillMaxSize()
                ) {
                    Text(text = "Recomposition Example")

                    // mutable variable, Components that use it will recompose
                    var isChecked by remember { mutableStateOf(false) }
                    Switch(checked = isChecked, onCheckedChange = { isChecked = it })

                    DefaultCardStableType(
                        data = "String: by default marked as stable"
                    )

                    DefaultCard(
                        data = listOf("List: by default marked as unstable")
                    )

                    StableCard(
                        data = StableState(status = listOf("@Stable state class containing a List"))
                    )

                    ImmutableCard(
                        data = ImmutableState(status = listOf("@Immutable state class containing a List"))
                    )
                }
            }
        }
    }
}

Read more