Compose phases

Episode 2 of MAD Skills: Compose Layouts and Modifiers

Jolanda Verhoef
Android Developers

--

In the previous MAD Skills article, you learned about the out-of-the-box APIs that Jetpack Compose offers for writing beautiful apps. In this article we’ll create a mental model of how those APIs actually transform data into UI.

You can also watch this article as a MAD Skills video:

With Compose being a declarative toolkit, we know at the basic level that it transforms data into UI. The three phases in this transformation process are:

  1. Composition: What to show
  2. Layout: Where to place it
  3. Drawing: How to render it

These three phases are executed one by one:

Three phases

Three phases

During the composition phase, the Compose runtime executes your composable functions. It outputs a tree data structure that represents your UI. This UI tree consists of layout nodes. Together, these layout nodes hold all the information needed to complete the next phases.

Two boxes. The left box shows code of a composable function, highlighted line by line. An arrow moves to a next box, where a tree structure is built node by node. This box is annotated with the term “UI tree”. An arrow points to one of the nodes saying “Layout Node”.
The Composition phase executes composable functions and creates a tree that represents your UI.

Then, during the layout phase, each element in the tree measures its children, if any, and places them in the available 2D space:

Two boxes. The left box shows the tree structure from the previous image. An arrow points to the right box. The right box shows a phone screen, with an animation of dotted orange rectangles being added to that screen. Those boxes represent the various elements of the UI.
The Layout phase measures and places each layout node in the UI tree.

Finally, in the drawing phase, each node in the tree draws its pixels on the screen:

Two boxes. Left box shows the screen with orange dotted lines from the last image. The right box shows those dotted lines being animated into a screen of a sample application, where each dotted box maps to an element on the screen.
The drawing phase draws the pixels on the screen.

Composition phase

Let’s zoom in and focus only on the bottom part of the screen, where you see the author of the article, the publication date, and the reading time:

Screenshot of app article screen, highlighting a round image showing Florina’s avatar, with her name next to it and another line of text that says “July 09, 1 minute read”

During the composition phase, we transformed composable functions into a UI tree. As we’re looking only at the author element, we can zoom in on a subsection of our code and UI tree:

In this case, each composable function in our code maps to a single layout node in the UI tree. This is a pretty simple example, but your composables can contain logic and control flow, producing a different tree given different states.

Layout phase

When we move to the layout phase, we use this UI tree as input. The collection of layout nodes contain all the information needed to eventually decide on each node’s size and location in 2D space.

During the Layout phase, the tree is traversed using the following 3 step algorithm:

  1. Measure children: A node measures its children, if any.
  2. Decide own size: Based on those measurements, a node decides on its own size.
  3. Place children: Each child node is placed relative to a node’s own position.

At the end of the phase, each layout node will have an assigned width and height, and an x, y coordinate where it should be drawn.

So for our composable, this would work as follows:

  1. The Row measures its children.
  2. First, the Image is measured. It doesn’t have any children so it decides its own size and reports it back to the Row.
  3. Second, the Column is measured. It needs to measure its own children first.
  4. The first Text is measured. It doesn’t have any children so it decides its own size and reports it back to the Column.
  5. The second Text is measured. It doesn’t have any children so it decides its own size and reports it back to the Column.
  6. The Column uses the child measurements to decide its own size. It uses the maximum child width and the sum of the height of its children.
  7. The Column places its children relative to itself, putting them beneath each other vertically.
  8. The Row uses the child measurements to decide its own size. It uses the maximum child height and the sum of the widths of its children. It then places its children.

One key take-away here is that we visited each node only once. With a single pass through the UI tree we were able to measure and place all the nodes. This is great for performance. When the number of nodes in the tree increases, the time spent traversing it increases in a linear fashion. In contrast, if we were to visit each node multiple times, the traversal time would increase exponentially.

Drawing phase

Now that we know the sizes and x, y coordinates of all our layout nodes, we can continue to the drawing phase. The tree is traversed again, from top to bottom and each node draws itself on the screen in turn. So in our case, first the Row will draw any content it might have, such as a background color. Then the Image will draw itself, then the Column, and then the first and the second Text:

Modifiers

Great! We’ve seen how the three phases are executed for our composable. But we took some shortcuts.

If we go back to the composition phase, we said we execute the code and build the UI tree.

But looking closer at the code, we can see that it actually uses modifiers to change the look and feel of some of our composables. In our UI tree, we can visualize these as wrapper nodes for our layout nodes:

Two boxes. Left one shows composables with modifiers added to them. Right side shows the tree structure from before, but now with boxes wrapping the nodes, representing how the modifiers are wrapper nodes.

When we chain multiple modifiers, each modifier node wraps the rest of the chain and the layout node within. For example, when we chain a clip and a size modifier, the clip modifier node wraps the size modifier node, which then wraps the Image layout node.

In the layout phase, the algorithm we use to walk the tree stays the same, but each modifier node is visited as well. This way, a modifier can change the size requirements and placement of the modifier or layout node that it wraps.

Now, interestingly, if we would look at the implementation of the Image composable, we can actually see that it itself consists of a chain of modifiers wrapping a single layout node. Similarly, a Text composable is implemented with a chain of modifiers wrapping a layout node as well. And finally, the implementations of our Row and Column are simply layout nodes that describe how to lay out their children:

The tree structure from before, but now each node is just a simple layout, with a lot of modifier wrapping nodes around it.
It’s modifiers all the way down!

We’ll get back to this in a later blog post, but for now it’s good to think about a modifier as wrapping a single modifier or layout node, while a layout node can lay out multiple child nodes.

Wrapping up

So, with this mental model you now have a better understanding of how the different phases in Compose work.

In the next post we’ll dive a bit deeper into the layout phase, and learn to reason about how exactly layouts and modifiers influence the measurement and placement of their children.

This blog post is part of a series:

Episode 1: Fundamentals of Compose layouts and modifiers
Episode 2: Compose phases
Episode 3: Constraints and modifier order
Episode 4: Advanced Layout concepts

--

--