Scoped recomposition in Jetpack Compose — what happens when state changes?
Tags: android, kotlin, programming, compose
Published: 2021-03-31 18:06:35

{% cta https://blog.zachklipp.com/scoped-recomposition-in-jetpack-compose-what-happens-when-state-changes/ %} This post has moved to blog.zachklipp.com. {% endcta %}

---

Yesterday I posted a [code snippet](https://twitter.com/zachklipp/status/1376941142202220545) on Twitter with [a poll](https://twitter.com/zachklipp/status/1376941143770898436) asking how Compose would choose to re-execute it when the state changes. Given this code, which functions will be called on the next composition?

```kotlin
@Composable fun Foo() {
  var text by remember { mutableStateOf("") }

  Button(onClick = { text = "$text\n$text" }) {
    Text(text)
  }
}
```

1. `Foo`
2. `Button`
3. `Button`'s content lambda
4. `Text`

I had initially meant to reply as a short thread on Twitter directly, but short turned into long and I found out there's actually a limit on the number of tweets you can post in a single thread, so I took it as a sign I should write a proper blog post instead.

# Show me the answer!

So, there are a actually few answers depending how you interpret the options in the poll - my bad for writing so ambiguously.

The most important answer was immediately tweeted by Sean McQuillan ([@objcode](https://twitter.com/objcode/status/1376944869046771712)), and parrots what other members of the Compose team at Google have said many times:

{% twitter 1376944869046771712 %}

In other words, it doesn't matter. If you've followed the Compose best practices, your code will Just Work, regardless of what fancy optimizations Compose decides to do under the hood.

That will be great in a few ~~years~~ months, when you and your team have internalized everything and barely remember what a `LayoutParam` is. It's great to be able to ignore the details and just read/write code. But when learning something new, it's usually helpful to peek behind the curtain and understand why things work the way they do.

## High-level analysis

If you’re writing this code and trying to reason about what _your_ code is doing (and not, for example, how `Button` works internally), the answer you probably care about the most is the third one: "`Button`'s content lambda".

1. When you click the button, the text `MutableState`'s value is changed.
2. The only function that reads that state object is the button’s content lambda, and so the content lambda gets invalidated.
3. The content lambda eventually gets re-executed, which reads the new text value, and passes it to `Text`.
4. (really 3 ½) Because the text is different, `Text` will then be ran as well.

A lot of people expected something else, so I'll run through why the other answers are ~~wrong~~ "less right" in the order they were voted.

# Some fundamentals

Before diving into this particular code snippet, let's go over some basics.

## Recompose scopes

Recompose scopes are an important piece of the Compose puzzle. They do some bookkeeping and help reduce the amount of work Compose has to do to prepare a frame.

- They are the smallest unit of a composition that can be re-executed (recomposed) to update the underlying tree.
- They keep track of what snapshot-based `State` objects are read inside of them, and get invalidated when those states change.

For every non-inline composable function that returns `Unit`, the Compose compiler generates code that wraps the function’s body in a recompose scope. When a recompose scope is invalidated, the compose runtime will ensure the (entire) function body gets recomposed (reexecuted) before the next frame. Functions are a natural delimiter for re-executable chunks of code, because they already have well-defined entry and exit points.

`Foo`’s body, `Button`’s body, the content lambda we pass to `Button`, `Text`’s body, all get their own recompose scopes.

{% collapsible What about inline functions and functions that return a value? %}

Inline functions are inline - their bodies are effectively copied into their caller, so they share their caller’s recompose scope.

Functions that have a non-unit return value don’t get their own scopes because they can’t be re-executed without also re-executing their caller, so their caller can “see” the new return value computed by the function.

{% endcollapsible %}

## Expressions and function calls

Before getting into any details about Compose itself, let's refresh a fundamental concept about how regular old function calls work. In Java/Kotlin, when you pass an expression to a function, that expression is evaluated in the calling function.

```kotlin
println(“hello” + “world”)
```

is basically equivalent to

```kotlin
val arg = “hello” + “world”
println(arg)
```

The values of all the arguments to a function call have to be evaluated before the JVM/ART can call the function, so that it can pass the results of those expressions to the function. You can also see this by doing `println(TODO())` - `println` will be highlighted as unreachable code, because the `TODO()` never returns, so the `println` call will never be hit. 

In the original code snippet, this means that the compiler generates the code to read the `text` state inside the content lambda, and then passes the result of the read to `Text`. The Compose compiler doesn’t know that the value of the read is only used in the one spot. At least right now - it could, potentially, figure this out via data flow analysis, but it is not that fancy at the moment. All it knows is that the content lambda reads `text`.

# Show your work

Let's look at our code snippet again:

```kotlin
@Composable fun Foo() {
  var text by remember { mutableStateOf("") }

  Button(onClick = { text = "$text\n$text" }) {
    Text(text)
  }
}
```

## Why isn’t just `Text` recomposed?

If we think of the `text` read as happening on its own line just above the `Text` call, then look for the nearest recompose scope enclosing that - it’s the button content lambda. That’s the recompose scope that gets invalidated when the text state value is changed. 

So when text is changed, and the lambda’s recompose scope is invalidated, the entire lambda body gets recomposed for the next frame.

## Why isn't all of `Foo` recomposed?

Now that we know how recompose scopes are delimited and invalidated, we can see that nothing directly in `Foo` reads any `State`, so it will never actually recompose.

This might be more apparent if we get rid of the delegate syntax for `text`, which doesn't change behavior but hides what's actually going on a bit:

```kotlin
@Composable fun Foo() {
  val text: MutableState<String> = remember { mutableStateOf("") }

  Button(onClick = { text.value = "${text.value}\n${text.value}" }) {
    Text(text.value)
  }
}
```

It's a lot more clear now that all `Foo` does with `text` is create the `MutableState` value holder. The actual object referenced by `text` doesn't change, it always points to the same `MutableState` instance. When we talk about state "reads", we mean reads of the `State`'s `value` property – e.g. `text.value`.

## Why isn't `Button` recomposed?

Because `Foo` isn't recomposed, nothing calls the `Button` function itself on recomposition.

However, this is a bit of a trick question: There may or may not be functions deep inside `Button`'s implementation that recompose in certain cases, but that doesn't affect our code, so we can ignore those. `Button` does maintain some private state, so it can show its pressed status and the ripple, but they might not actually require recomposition, instead they can just request re-draws.

### What about the `onClick` lambda?

Recompose scopes are only created around _composable_ functions. Event handlers, like `Button`'s `onClick`, are _not_ composable, they're just regular functions. When the click handler is invoked by the framework, it is done so outside of any recompose scope.

# Caveat: Inline composable functions

You might be surprised to find out (and I often forget) that common layouts like `Column`, `Row`, and `Box` are all inline functions. Even the core `Layout` composable itself is inline. Ignoring Compose for a second, remember that when a Kotlin function is marked `inline`, its body is always copied into its call sites by the compiler – it doesn't even exist as an entity in the bytecode. This is most often used in regular Kotlin code to avoid allocating objects to hold lambda functions for functions that are simple wrappers around other functions.

Compose uses inlining for a similar optimization. **Because the body of inline composable functions are simply _copied_ into their call sites, such functions do not get their own recompose scopes.** In the following snippet, when the lambda passed to `Wrapper` gets recomposed, the `App` body will be recomposed as well, since `Wrapper` is inline and does not have its own recompose scope. In other words, any time this code prints "App recomposing", "Wrapper recomposing", or "Lambda recomposing", all three will be printed together.

```kotlin
@Composable fun App() {
  println("App recomposing")
  Wrapper {
    println("Lambda recomposing")
    // Read some state causing recomposition…
  }
}

@Composable inline fun Wrapper(content: @Composable () -> Unit) {
  println("Wrapper recomposing")
  content()
}
```

If you wrap the `Text` in the original code snippet in a `Column` and add trace statements, you’ll see that the invalidated scope is still the entire button content lambda, not just the `Column` content lambda.

# Final thoughts

This means in a regular program, you might end up recomposing a lot more than you’d expect just from looking at the code. And in general, it’s impossible to determine what will be recomposed by just looking at a particular code snippet without knowing if functions are inline. 

It’s also important to remember that a lot of these rules might change in the future. The compose team might change the rules at any time. Luckily, as Sean said, it shouldn’t affect correctness. All this function skipping is just an optimization. This is (one of the reasons) why it’s so important to not do stuff like perform side effects directly in composables. Use `LaunchedEffect`, `DisposableEffect`, and `SideEffect`. Use tools like `remember {}` or `derivedStateOf {}` for expensive computations.

Follow the [documented best practices](https://developer.android.com/jetpack/compose/state) and you won’t need to worry about exactly what gets recomposed when – now or in the future.

If you’d like to read more, [Vinay Gaba](https://twitter.com/vinaygaba) has an [excellent article](https://www.jetpackcompose.app/articles/donut-hole-skipping-in-jetpack-compose) about a specific implication of the behaviors described in this post known as “donut-hole skipping”. Also, checkout [these other official docs](https://developer.android.com/jetpack/compose/mental-model#recomposition) if you'd like to enhance your mental model even further.

_Thanks to [Sean](https://twitter.com/objcode) for reviewing this post!_

_Update: Pulled paragraph about inline composable functions into their own section, added more explanation, and an example._

_Update (9/19/21): Added link to donut-hole skipping article._