SIMONA STOJANOVIC: Jetpack Compose
makes it easy to implement designs
with just a couple of layout concepts.
But what if your requirements are more complicated?
In this episode, we'll take a look at more advanced layout
concepts and how they can help you take your design
implementations to the next level.
[MUSIC PLAYING]
The previous episode of our MAD Skill series on Compose
layouts and modifiers, Yolanda talked about the layout phase
of Compose to explain how modifier chaining
order and incoming parent constraints
impact the composable they are passed to.
Today's episode, we zoom in even more on the layout
phase and constraints, cover them from another perspective--
how to harness their power to build
custom layouts in Compose.
To build custom layouts, we'll go over
what the layout phase is capable of, how to enter it,
and how to use its subphases, measurement, and placement
to your advantage for building flexible custom layouts.
After that, we'll cover two important rule-defined Compose
APIs, subcompose layout and intrinsic measurements,
as the last two missing pieces of the layouts puzzle.
These concepts will provide you with additional knowledge
to build intricate designs with very specific requirements
in Compose.
In the previous episodes, we talked
about how Compose transforms data
into UI via its three phases--
composition, layout, and draw, or what to show,
where to place it, and how to render it.
But as the name of our series suggests,
we're mostly interested in the layout phase.
However, the term "layout" in Compose
is used, well, for a lot of different things
and might seem confusing because of its many meanings.
So far in the series, we've learned the following usages.
Layout phase, one of three phases of Compose
where element sizing and positioning is defined.
A layout, an abstract term used to quickly define
any UI element in Compose.
A layout node, a visual representation
of one element in the UI tree created as a result
of the composition phase.
In this episode, we'll also get to learn
a few additional meanings to complete the whole layout
circle.
Let's quickly break them down first.
A more in-depth explanation of these terms
awaits later in the episode.
Layout composable, the core component of the Compose UI
which creates and adds a layout node in the Compose UI tree.
Layout function, the starting point
of placement, which is the second substep of the layout
phase that places children in the parent layout.
Layout modifier, a modifier that wraps one single layout node
and allows sizing and placing it individually.
Now we've seen the different definitions.
Let's go over them one by one, starting with the layout phase,
and zoom in on it.
As mentioned, during the layout phase,
each element in the UI tree measures its children, if any,
which also enables the parent to decide on its own size.
And finally, it places the children
in the available 2D space.
Every out of the box layout in Compose,
such as row and column and many more,
handle all of this for you automatically.
But what if your designs require a nonstandard layout, for which
you need to go custom and build your own layout,
like this complex-looking time graph
from our jetlagged sample.
That's precisely why you need to know more about the layout
phase, how to enter it, and how to use
its subphases of child measurement and placement
to your advantage.
Let's take a look at how we can build a custom layout
in Compose from a given design.
Let's go over and explain the most important basic steps
for building a custom layout.
However, if you wish to follow a detailed step-by-step video
guide on how and when to create a custom
layout from a complex design, please
take a look at the custom layouts and graphics in Compose
video or directly explore the custom layout
from our jetlagged sample.
You can also find the links in the description.
Calling the layout composable is the starting
point of both the layout phase and building a custom layout.
It accepts a composable content as it's
children and a measure policy for measuring and positioning
its elements.
All higher-level layouts, like column and row,
use this composable under the hood.
Layout composable has currently three overloads.
Layout for measuring and placing zero more children, which
accepts one composable as content.
Layout for leaf nodes of the UI tree,
with precisely zero children, so it doesn't
have a content parameter.
And layout that accepts contents list
for passing multiple different composables.
Once we entered the layout phase,
we see that it consists of two steps, measurement
and placement, in that specific order.
Sizes of the child elements are calculated
during the measure pass and their positions
during the placement pass.
The order of these steps is enforced
with Kotlin DSL scopes.
During measurement, the content of the layout
can be accessed as measurables, components that
are now ready to be measured.
Inside layout, measurables come as list by default.
Depending on the requirements of your layout,
you could either take this list and measure
every item with the same incoming constraints
to keep its original predefined size
or tweak its measurements as needed by copying over
constraints you wish to keep and overriding
the ones you wish to change.
Very important feature of the layout phase
is single pass measurement.
This means that a layout element may not
measure at any of its children more than once.
Single pass measurement is good for performance,
allowing Compose to efficiently handle deep UI trees.
Measuring a list of measurables would, in return,
give a list of placeables, or a component
that is now ready to be placed.
The placement step starts by calling the layout function
and entering the placement scope.
At this point, the parent layout will
be able to decide on its own size,
for example, by summing up the widths
and the heights of all its child placeables.
Placement scope now allows us to use all placeables that came
as a result of measurement.
To start placing children, we need their starting x
and y-coordinates.
Once we define where we want the children to be positioned,
we call Place to conclude with the placement pass.
And with that, we end the placement step, as well as
the layout phase.
Your custom layout is now ready to be used and reused.
Using the layout composable to create a custom layout
enables you to manipulate all children elements
and manually control their sizing and positioning.
However, there are cases when creating a custom layout just
from controlling one specific element
is an overkill and not necessary.
In these cases, rather than using custom layout,
Compose offers a better and simpler solution, the layout
modifier, which allows you to measure and layout
just one single wrapped element.
Let's look at an example where a UI element
is being squashed by its parent in a way that we don't really
like.
We want just one element in this simple column
to have more width than the parent is enforcing
by removing the surrounding padding, for example,
to achieve edge-to-edge appearance.
To make the third element control itself
and remove the enforced padding, we set a layout modifier on it.
The way it works is very similar to the layout composable.
It accepts a lambda that grants you access
to the element you're measuring, passed as a single measurable,
and the composable's incoming constraints from the parent.
You then use it to modify how a single wrapped element is
measured and laid out.
We then change this element's maximum width
in the measurement step to add some extra DPs.
And then we place it as we originally intended.
As we said previously, one of the strengths of the Compose
is that you can choose your own path when problem-solving.
There are usually multiple ways to achieve the same thing.
If you knew the exact static size you want for this element,
another approach would be to set required width modifier on it
so that the incoming constraints in the parent layout
don't override its set width and instead respect it.
In contrast, use of regular width modifier
would have the set width overridden by the parent layout
and incoming constraints in the measurement phase.
Now that we've covered how you can use the layout
phase and the composable for writing your own custom
layouts, let's take a step back to Compose phases.
In earlier episodes, we talked about the phases of Compose
and the rule of their precise ordering-- composition, layout,
and drawing.
The layout phase then breaks down
to measurement and placement subphases.
While this applies to the vast majority of layout composables,
there is one rule-breaking layout that doesn't follow this
schema, but with a good reason--
subcompose layout.
Think about the following use case.
You're building a list of 1,000 items that simply cannot fit
on the screen at the same time.
In that case, composing all these child items
would be unnecessary waste of resources.
Why compose so many items up front if the majority of them
cannot even be seen?
Instead, a better approach would be
to measure all list items to obtain their sizes,
then, based on that, calculate how many items
can fit the available viewport.
And finally, compose only items that would be visible.
This is one of the main ideas behind subcompose layout.
It needs to do the measurement pass first for some child
composables, and then secondly to use that size information
to determine whether to compose other children.
Meaning the measurement step in the layout phase
needs to happen before the composition
phase of some children.
This deferred composition is called subcomposition,
and it is precisely why lazy components are
built on top of subcompose layout, which
enables them to add content on demand while scrolling.
Box with constraints also uses subcompose layout
under the hood, but this use case is slightly different.
It grants you access to incoming constraints, like max height,
from the parent, which are not available in regular layouts.
This is because the measurement step of some children
is done before the composition of others.
You can then use obtain constraints
to decide whether to compose a child or not.
A subcomposed layout changes the usual flow of compose phases.
There are certain costs and limitations
when it comes to performance.
Therefore, it's very important to understand
when subcompose layout should be used, and when it's not needed.
A good, quick way of knowing when
you might need subcompose layout is
when at least one child's composable's composition phase
depends on the result of another child's composable measurement.
We've seen valid use cases for this in lazy components and box
width constraints.
If, however, you just need one child's measurement
to measure other children, you can do so with a regular layout
composable.
The second rule of Compose we previously mentioned
is the single pass measurement in the layout phase, which
helps greatly with overall performance of this step
and the layout system in general.
Think about the amount of recompositions
that can happen in a short time and how
limiting the measurement of the entire UI tree
can improve the overall speed.
Therefore, you should only attempt
to measure your children once when writing custom layouts.
In fact, manually trying to measure children
twice throws a runtime exception.
However, there are use cases when the parent layout needs
to know some information about its children
before measuring them so that it can
use this information for defining and passing down
constraints.
And this is precisely what intrinsic measurements do.
They let you query children before they're measured.
Let's look at a following example.
We want this column items to have the same width
or, to be more precise, have each item's width as that
of the widest child.
In our case, the and modifiers item.
But we also want that child to take as much width as it needs.
Our first step doesn't really work.
Each item takes only the space it requires.
We can try out the following.
However, this expands every item and the parent
column to the max width available on the screen.
Remember, we want the width of the widest item for all items.
As you can tell, we're aiming to use intrinsics here.
By using intrinsic size max on the parent column,
we're querying its children and asking,
what is the maximum width you need to display
all of your content properly?
Since we're displaying text and the phrase "and modifiers"
is the longest, it will define the column's width.
Conversely, if you need to use intrinsic size min,
the question will be, what is the minimum width
you need to display all of your content properly?
In the case of text, the minimum intrinsic width
is the one that has one word on each line.
You can use a combination of both min
and max on width and height to get your desired behavior.
However, intrinsic measurements don't really
measure children twice.
Instead, they do a different kind of a calculation.
You can think of this as a premeasure step that
is cheaper and easier.
While this doesn't exactly break the single measurement rule,
it does bend it a little bit.
When creating a custom layout, intrinsics
provide a default implementation based on approximations.
However, sometimes this might not work for you.
To specify the intrinsics of your custom layer,
you can override all of the values inside the measure
policy.
We've covered a lot today.
All the various meanings of the term "layout"
and how they relate to one another,
how to enter and control the layout phase to your advantage
when building custom layouts, and then we
concluded with subcompose layout and intrinsic measurements
as the additional APIs for achieving very specific layout
behaviors.
And with this, we conclude our MAD Skills series in Compose
layouts and modifiers.
We hope you've learned new things about Compose, renewed
old knowledge, and most importantly, that you feel
much more prepared and confident to migrate everything
to Compose.
And of course, don't forget to subscribe
to our channel for more updates.
[MUSIC PLAYING]