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.
If you’ve got any questions so far from this series on Compose Layouts and Modifiers, we will have a live Q&A session on March 9th. Leave a comment here, on YouTube, or using #MADCompose on Twitter to ask your questions.
You can also watch this article as a MAD Skills video:
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.
Then, during the layout phase, each element in the tree measures its children, if any, and places them in the available 2D space:
Finally, in the drawing phase, each node in the tree draws its pixels on the screen:
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:
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.
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:
Measure children: A node measures its children, if any.
Decide own size: Based on those measurements, a node decides on its own size.
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:
The Rowmeasures its children.
First, the Image is measured. It doesn’t have any children so it decides its own size and reports it back to the Row.
Second, the Column is measured. It needs to measure its own children first.
The first Text is measured. It doesn’t have any children so it decides its own size and reports it back to the Column.
The second Text is measured. It doesn’t have any children so it decides its own size and reports it back to the Column.
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.
The Columnplaces its children relative to itself, putting them beneath each other vertically.
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.
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:
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:
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:
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.
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.
Got any questions? Leave a comment below or use the #MADCompose hashtag on Twitter and we will address your questions in our upcoming live Q&A for the series on March 9th. Stay tuned!