Recomposition is fundamental to Compose, yet it is often misunderstood, which leads to many myths going around our community. In this article I will explain how composition and recomposition work in simple terms.
This article took some parts from the course Advanced Compose I am currently working on. This course dives deep into Compose topics that are important yet often misunderstood by Compose developers.
Composition
Composition is pretty straightforward: composable functions are actually executed like regular functions, but under the hood they receive an additional parameter of type Composer, that allows storing values (remember), reading CompositionLocal values, or emitting UI. Components use it to add components to UI tree. Let's see it in an example:
How is this view built based on the Counter function? When we start composition (using a function like setContent) its lambda is executed to build the UI tree in a process called composition (in this case, I use Compose on WASM, so ComposeViewport starts composition). Counter calls whatever is inside its body, so remember and Row. remember creates a state that persists recomposition and returns its reference. Row emits UI, so adds a node to our UI tree under Counter component. It also needs to get composed, so its lambda gets called. It includes Row, Button and Row children, so as those functions get called, they each emit their own nodes to the UI tree. This process continues recursively until all composables are executed and the whole UI tree is built.
This way a UI tree is built, which is then measured in the layout phase and finally drawn in the drawing phase. However, application state changes, and to have our UI updated, this state must be represented with a snapshot state.
Snapshot state
In Compose, androidx.compose.runtime.State type, known as "snapshot state", has a very special property: when its value changes, Compose automatically triggers recomposition of all composables that read that state.
Recomposition is similar to composition, but it updates the existing UI tree instead of building it from scratch. When recomposition happens, only the parts of the UI that read changed state are recomposed. This composition can propagate to those UI children.
In Compose, we typically create local state with remember and mutableStateOf, which returns a State<T> object. We can also use delegation for easier use:
val state by remember { mutableStateOf(initial) }
However, most state is stored in view models in StateFlow. Reading it in compose would be incorrect, because its changes would not trigger recomposition.
The right way to observe StateFlow in Compose is collectAsStateWithLifecycle, which under the hood creates a snapshot state, remembers it and sets a listener to update it whenever the flow emits a new value. This ensures that recomposition is triggered when the state changes, keeping the UI up-to-date.
So recomposition starts from state readers. As those components get recomposed, they call their bodies again and trigger their children's recomposition. This is how recomposition propagates down the tree.
However, components can skip recomposition. By default, they skip recomposition if their arguments haven't changed since the last composition/recomposition. How do we compare current values with previous ones? That depends on their stability, so we will discuss that in the Stability section.
When "Make popular" gets clicked, artist state value changes. We observe it in ArtistScreen, so it should get recomposed and propagate it to ArtistCard (this is not strictly true, as since artist is directly passed to children, Compose should be able to optimize it and skip recompositon for ArtistScreen). ArtistCard gets recomposed (this one for sure), and pass values to its children. ArtistName recives different value, so it gets recomposed, while ArtistImage recives the same imageUrl, so it will skip recomposition. This way UI gets updated.
Now let's see what would happen if we passed value providers, so lambda expressions returning values, instead of values directly.
Who reads uiState now? ArtistImage and ArtistName. So on state change, only those components will get recomposed. Text inside ArtistImage will skip recomposition, and Text inside ArtistName will recompose. This is why value providers can limit recomposition, them move reading closed to where this state is needed.
That doesn't mean we should use provider functions. Provider functions are less readable and intuitive than values, and we should value readability over premature optimization. They also introduce additional costs related to lambda creation and maintenance (memoization and updating). It is one of recomposition optimization techniques to consider when we have issues with component performance, but you should always track performance before and after using it to know if it makes sense.
Summary
In this article I showed an essential explanation how composition and recomposition work. Those processes are simpler than what most people think. Snapshot state sets listeners to recompose all composition scopes that read their state. Then recomposition can propagate down to children.
Recomposition and other related topics, like stability, derived state, component identity, are covered in the first days of the Advanced Compose cohort course. On later days, there is deep dive into other Compose phases, modifiers, navigation, architecture and other topics that many Compose developers consider though, but that are actually quite simple if explained well and with a focus on practical use and best practices.
Marcin Moskala is a highly experienced developer and Kotlin instructor as the founder of Kt. Academy, an official JetBrains partner specializing in Kotlin training, Google Developers Expert, known for his significant contributions to the Kotlin community. Moskala is the author of several widely recognized books, including "Effective Kotlin," "Kotlin Coroutines," "Functional Kotlin," "Advanced Kotlin," "Kotlin Essentials," and "Android Development with Kotlin."
Beyond his literary achievements, Moskala is the author of the largest Medium publication dedicated to Kotlin. As a respected speaker, he has been invited to share his insights at numerous programming conferences, including events such as Droidcon and the prestigious Kotlin Conf, the premier conference dedicated to the Kotlin programming language.