---
title: "Basic Intelligent Recomposition in Android Compose"
source: "/writing/basic-intelligent-recomposition-in-android-compose/"
---

Article

# Basic Intelligent Recomposition in Android Compose

Lucas Kivi

July 29, 2024

Robots work together to rebuild a component within a phone's UI.

In the Android Compose UI paradigm, recompositions drive UI changes. So if you are using Android Compose, then your app is recomposing, but you may not be taking full advantage of Compose's capacity for intelligent recomposition. That is OK -- it isn't necessarily obvious how to do so -- this article sets out to teach you a bit about it.

## Why should you care?

A crucial component to a good app experience is buttery smooth UI. Too many recompositions can cause dropped frames and that is public enemy number one to buttery smooth UI. So it behooves us to be _intelligent_ with our recompositions. Often times there is an animating state that requires recomposing a single UI component many times over a period of time. An app that takes advantage of intelligent recomposition will exclusively recompose just the animating component. In the worst case, a poorly written UI could completely rebuild its entire Composable hierarchy every time that animating component updates. This could be the type of error that results in dropped frames -- let's learn how to avoid that.

## The Basics

First let's get the definition of recomposition:

> Recomposition is the process of calling your composable functions again when inputs change. This happens when the function's inputs change. When Compose recomposes based on new inputs, it only calls the functions or lambdas that might have changed, and skips the rest. By skipping all functions or lambdas that don't have changed parameters, Compose can recompose efficiently.

This makes it clear that Compose monitors inputs to composable functions and "recomposes" the ones that are updated because it assumes that if its inputs have changed, it's embedded logic needs to be rerun in order to properly re-render itself. This leads to two basic questions that we need to answer:

1.  The hierarchy of composables are selectively recomposed. So what is special about a composable function that allows it to be added to the composable hierarchy?
2.  Changes in a composable's inputs inspire recompositions. What constitutes a composable input?

Let's go over these two things...

### Composable Functions

Each function or lambda annotated with `@Composable` creates a `[RecomposeScope](https://developer.android.com/reference/kotlin/androidx/compose/runtime/RecomposeScope)` . This nifty compose runtime component creates a recomposition scope within a hierarchy of recomposition scopes to represent a living screen. If an input to a `RecomposeScope` is changed, the scope will be invalidated and its content will be rerun with the updated inputs. If an invalidated `RecomposeScope` contains child `RecomposeScope`s, Compose will skip recomposing those scopes if they don't depend on the changing input. At the same time, if a child `RecomposeScope` is invalidated, it isn't necessary that its parent is too. It should be known that `inline` functions (e.g. `Column` and `Row`) will **not** create a new `RecomposeScope`. Below you can see the hierarchy of `RecomposeScope`s created by `SuperSpecialComponent`:

A code block shows a composable function called `SuperSpecialComponent` with a `Button` that contains a `Column`. `SuperSpecialComponent`'s function body is labeled "Super Special Component Scope" and the `Button`'s content lambda is labeled "Button Scope". Notibly, there is no label on the `Column`'s content lambda.

Hierarchy of recomposition scopes

Both "Button Scope" and "Super Special Component Scope" will observe their own input changes and rebuild themselves whenever necessary. There is no "Column Scope" because `Column` is an inline function, its content becomes a part of "Super Special Component Scope".

If `SuperSpecialComponent` receives a state change that the `Button` doesn't care about, then the "Button Scope" can be skipped for recomposition even though its parent `RecomposeScope` is invalidated.

If the `Button`'s inputs change but the "Super Special Component Scope" doesn't read any of those inputs, `SuperSpecialComponent` will not even be considered for recomposition because a child `RecomposeScope` being invalidated does not invalidate the parent.

This behavior illuminates an important part of the Compose UI paradigm: nodes of the composable hierarchy can recompose independently if you do it right.

Now that we have a basic understanding of how to create a `RecomposeScope` hierarchy, let's discuss what constitutes a composable's inputs.

### Composable Inputs

The obvious inputs to a composable are its parameters. If a composable's parameters change it will recompose. However, Compose runtime won't notice just any state change; it requires a special observable type called `[State](https://developer.android.com/reference/kotlin/androidx/compose/runtime/State)` . This is a magical Compose runtime component that allows [Compose to listen for value changes in order to recompose](https://developer.android.com/develop/ui/compose/state#state-in-composables) when new data is received. `State` related updates are the primary driver of recompositions. This is why you commonly see `ViewModel` data flows converted into state via `Flow<T>.collectAsState()` , and state holders emit state backed by some sort of `State`. If you want your UI to react to your data updates, that data must be backed by `State`!

A crucial point here is that a composable inputs aren't _just_ its parameters, it is also any `State` read that occurs within its `RecomposeScope`. For example, `Component` below has two inputs that can cause its recomposition, `color` and `text`:

private fun MainComponent() {
  val viewModel: MainViewModel = hiltViewModel()
  val text by viewModel.textFlow.collectAsState()

  SuperSpecialComponent(
    text = text,
  )
}

private fun SuperSpecialComponent(
  text: String, // Input one
) {
  val color: State<Color> = animateColorAsState(...)

  Button {
    Column {
      Text(
        text = text,
        color = color.value, // Input two
      )
      ...
    }
  }
}

If `textFlow` is updated, then the `MainComponent` will be recomposed because it is responsible for reading it's value. `SuperSpecialComponent` will recompose due to its parent's `RecomposeScope` being invalidated, at which point Compose runtime will notice that it depends on `text` which has changed. `Button` will also recompose because it depends on `text`.

Additionally, `Button` will recompose each time `color` is updated by its backing function `animateColorAsState(...)`, while `SuperSpecialComponent` will not be recomposed because it never reads `color` within its `RecomposeScope`.

Now we understand that a composable's inputs are any `State` reads that might occur within its `RecomposeScope`, or any parameters it might have that are updated when its parent recomposes. So how can use this information to build a UI that _intelligently_ recomposes.

### What can we do?

Based on what we just covered, recomposition scopes should only read `State` that they actually depend on. If a component is only reading state to then hand it off to another component, there is probably some optimization that can occur, especially if that state is changing rapidly. So let's take a look at a bad, good, and great way to handle changing inputs within a composable.

### For our example...

We will need some animating `State`-- for that we will use this composable function that produces a`State<Int>` and increments its `Int` value every 500 milliseconds.

@Composable
fun produceAnimatingState(): State<Int> {
    val animatingState = remember { mutableIntStateOf((0) }
    LaunchedEffect(Unit) {
        while (coroutineContext.isActive) {
            animatingState.intValue++
            delay(500)
        }
    }

    return animatingState
}

## The Bad Way

Here is an inefficient component that reads `State` in too high of a scope.

@Composable
fun ComponentRaw() {
  val animatingState = produceAnimatingState()

  Row {
    Text(
        text = animatingState.value.toString(),
    )
    Spacer(...)
    Icon(...)
  }
}

There is a GIF of a number incrementing beside a static icon.

A demonstration of the component and its changing state.

When the input to `Text` changes it recomposes, that is great! We want `Text` to be updated; however, `animatingState` is read from within `ComponentRaw`'s `RecomposeScope` which invalidates the entire component whenever `animatingState` changes.

CompnentRaw and Text recompose 83 times while Icon is skipped 83 times.

The recomposition counts for ComponentRaw.

`ComponentRaw` recomposed 83 times. For each of those recompositions each line of logic within the component is evaluated, and each sub-composable is considered for recomposition! The `Icon` is correctly [skipped](https://developer.android.com/develop/ui/compose/mental-model#skips) , as it is static. Since the `Text` is the only state that changed, we'd like the `ComponentRaw` recomposition count to be zero. Let's see how we can do that.

## The Solution

We need to wrap the `Text` composable that cares about `animatingState` 's value in its own `RecomposeScope`. Then, we must defer the reading of `animatingState`'s value until it is within that scope. There are a few ways to do that, but I am going to go over what I will call **The Good Way** and **The Great Way.** In the wild you pretty much only see **The Great Way** -- because it is great -- but I do think going over **The Good Way** is worthwhile because it helps illuminate a point.

## The Good Way

We can create a new `RecomposeScope` as prescribed, and pass the`State` directly to the composable and read it within. Our new composable is `CoolTextState` , and it simply wraps the `Text` that is responsible for reading `animatingValue` .

@Composable
fun ComponentState() {
  val animatingState = produceAnimatingState()

  Row {
    CoolTextState(
      animatingState = animatingState,
    )
    Spacer(...)
    Icon(...)
  }
}

@Composable
fun CoolTextState(animatingState: State<Int>) {
  Text(
    text = animatingState.value.toString(),
  )
}

ComponentState never recomposes, while CoolTextState and Text recompose 83 times.

The recomposition counts for ComponentState.

We can see that we have achieved the desired behavior! `ComponentState` never recomposes. The `Icon` doesn't even need to skip recompositions; it is simply never considered for them. Changes to `animatingState`'s value only recompose the component that cares.

This example shows that it is the actual read of `State` that triggers the recomposition. We can freely pass it around, only to unravel the value where it is actually appreciated. Compose will notice changes in `State` and trigger invalidations of the recomposition scopes that care.

This approach is rare because it has a weakness -- if you want to provide an `Int` that is not contained by a `State` , tough luck. Maybe in some places your component animates and in others it uses a static value. In the latter case it would be overkill to use a `State` backed value.

So how can we maintain these performance benefits but expect an `Int` rather than `State<Int>`?

## The Great Way

The more common and better option is known as the provider pattern. You still need the new `RecomposeScope`, but this time you pass your value through the composable hierarchy with a [_stable_ lambda](https://multithreaded.stitchfix.com/blog/2022/08/05/jetpack-compose-recomposition/#:~:text=Function%20types%20\(lambdas\)-,Gotcha,--%20Unstable%20Lambdas) backed by `State`. (Note that this is just a shorthand description: all lambdas are technically stable and the stability issue is [really related to the data being captured](https://medium.com/androiddevelopers/jetpack-compose-strong-skipping-mode-explained-cbdb2aa4b900#:~:text=this%20by%20default.-,%E2%80%9CUnstable%20lambdas%E2%80%9D,-This%20still%20left) .) The contents of this type of lambda will be intelligently evaluated for changes, and you will experience the exact same performance as `ComponentState`, but you will have a more general solution.

@Composable
fun ComponentProvider() {
  val animatingState = produceAnimatingState()

  Row {
    CoolTextProvider(
      animatingValueProvider = { animatingState.value },
    )
    Spacer(...)
    Icon(...)
  }
}

@Composable
fun CoolTextProvider(animatingValueProvider: () -> Int) {
  Text(
    text = animatingValueProvider().toString(),
  )
}

ComponentState never recomposes, while CoolTextState and Text recompose 83 times.

The recomposition counts for ComponentProvider.

This is the exact same recomposition behavior as `ComponentState`! But if we wanted to, we could still provide a static `Int`, like so:

CoolTextProvider(
  animatingValueProvider = { 42 },
)

## Onwards and Upwards, Efficiently

If you like buttery smooth UI, then you want to take advantage of intelligent recompositions by localizing your `State` reads. This will require you to wrap rapidly changing UI components in composable functions in order to build fine grained `RecomposeScope`'s. With this beautiful recomposition scope hierarchy, you will need to defer `State` reads by using the provider pattern in order to localize those recompositions. With all of that, you will have UI so buttery smooth that your users won't be able to get a grip.

_Lucas recomposes intelligently at_ [_Livefront_](https://livefront.com/) _._

-   Lucas Kivi

    Software Engineer

    [More from Lucas](/team/lucas-kivi/)

    Lucas Kivi

### Tags

[Digital Engineering](/insights/all/?topic=digital-engineering) [Mobile Apps](/insights/all/?topic=mobile-apps)

### Share

-   Copied

## View these next.

[See all](/insights/all/?type=writing)

-   [

    Article

    How To Sabotage Your Project Using Inconsistency

    Collin Flynn

    ](/writing/how-to-sabotage-your-project-using-inconsistency/)
-   [

    Article

    Unit Testing race conditions by creating chaos (Swift)

    Sean Berry

    ](/writing/unit-testing-race-conditions-by-creating-chaos-swift/)
-   [

    Article

    UIApplicationDelegate call sequence reference

    Sean Berry

    ](/writing/uiapplicationdelegate-call-sequence-reference/)
