[MUSIC PLAYING] NICK BUTCHER: Hi, and
welcome to A deep dive into Jetpack Compose Layouts. My name is Nick Butcher. And I'm joined by George Mount. Jetpack Compose
offers a brand-new way to describe your UI
declaratively in code. It offers new composables for
arranging your UI and modifiers for configuring components
and adding behaviors. But this talk is not about
these basic building blocks. If you're not familiar
with these yet, then please go check out
our documentation first. The goal of this talk is to
explain the composed layout model that powers these
composables and modifiers, to deep dive into how
they work under the hood and their capabilities
so that you have a better understanding of how the layouts
and modifiers that you use work to understand how and
when to build your own custom layouts to achieve the exact
design your app calls for. The goals of the
Compose layout system are that layouts are easy
to create, especially custom layouts, that the layout
system is powerful, enabling you to create
whatever your apps need, and that they are
highly performant. Let's see how Compose's layout
model achieves these goals Jetpack Compose
transforms state into UI. But how does it do this? It's a three-step process-- composition, then
layout, then drawing. Composition executes your
composable functions, which can emit UI, creating a UI tree. For example, executing this
SearchResult composable yields a tree like this. This is a pretty simple
example, but your composables can contain logic
and control flow, producing a different
tree in different states. In the layout stage, this tree
is walked, and each piece of UI is measured and placed on
the screen in 2D space. That is, each node determines
their width and height and x, y coordinates. In the drawing stage, the
UI tree is walked again, and all elements render. Today, we went to a deep
dive into the layout stage and see how it works. There are two phases to the
layout stage, measurement and placement. These are roughly
equivalent to on measure on layout in the view world. But in Compose, these
phases are intertwined. So for now, we'll consider
it as a single layout phase. Laying out each node in the UI
tree is a three-step process. Each node must
measure any children, then decide its own size,
then place its children. So applying this to our
example, the UI tree is laid out in a single pass. First, the root layout, the
row, is asked to measure. It, in turn, asks its first
child, the image, to measure. The image is a leaf
node with no children. So it measures itself
and reports the size. It also returns instructions
for how to place any children. The leaf node, this
is usually empty. But all layouts return
these placement instructions at the same time as
setting their size. The row then measures its
second child, the column. The column measures
its children. So the first text measures and
reports its size and placement instructions. Then, the same for
the second text. Now that the column has
measured its children, it can determine its own
size and placement logic. Finally, now that all of
its children are sized, the root row can then determine
its size and placement instructions. Once all elements
are measured in size, the tree is walked again, and
all placement instructions are executed in the
placement phase. So now that we understand
the steps involved, let's take a look at
how this is implemented. Going back to the
composition stage, we represented our UI tree with
these higher-level composables that we work with, rows,
columns, text, and so on. But each of these
composables are actually themselves constructed from
lower-level composables. For example, a
text functionality is built up from a number of
lower-level building blocks. Examining the UI
tree, we can see that all composables that
place elements on the screen contain one or more
layout composables. This layout composable is the
fundamental building block of Compose UI. The layout composable
emits layout nodes. And in Compose, the UI
tree, or the composition, is a tree of layout nodes. Over to George to take
a closer look at this. GEORGE MOUNT: Thanks, Nick. So let's examine the
layout composable to see how this works. This is the function signature. The content is a slot for
any child composables. For the purposes
of layout, content contains the child layouts. It accepts a modifier parameter
for applying modifiers to the layout. We'll come back to this later. Lastly, it accepts
a measure policy, which is how the layout
measures and places items. Typically, you implement this
functional interface in line to implement your custom
layout's behavior, like this. Here in MyCustomLayout
composable, we call the Layout
function and provide the measure policy
as a trailing lambda, implementing the required
measure function. This function receives
a Constraints object, telling the layout how
large or small it can be. Constraints is a
simple class, modeling the minimum and maximum
width and height that the layout can be. For example,
Constraints can express that the layout can be
as large as it likes, not applying any restrictions. Or it can express that the
layout should be an exact size. The measure function also
receives a list of measurables. This is a representation of
the child elements passed in. The measurable type exposes
functions for measuring items. As we covered earlier,
laying out each element is a three-step process. Each element must
measure any children, then decide its own size,
then place its children. Let's see how we implement this. First, let's measure
any children. The measurable type
exposes a measure method to do this which accepts
size constraints. In the simplest case, where
we don't apply any custom measurement logic,
we can just map over the list of measurables,
measuring each one. This produces a
list of placeables. Placeables are the measured
children, and have a size. We can use the
placeables to calculate how big our layout should be. We can then report our size, how
large this layout wants to be, by calling the Layout method. The Layout method requires
a placement block, which we typically implement
as a trailing lambda, allowing you to finally
place each item where you want it to be. There's also a
placeRelative method, which will automatically
auto-mirror coordinates horizontally in
right-to-left locales. API design prevents
you from trying to place an element that
hasn't been measured. The place method is only
available on placeables, which are returned from
the measure method. In the view world,
it was up to you when you called on
measure and on layout. And ordering was not
strongly enforced. We need to settle bugs and
differences in behavior. NICK BUTCHER: While this might
seem a little complex at first, it's really quite
powerful and easy to use once you get the hang of it. Let's walk through
building a few example layouts to illustrate this. Compose ships a column
component for laying elements out vertically. To understand how it
works under the hood and how it uses the
layout composable, let's implement our own column. We'll create MyColumn
composable function that accepts a modifier
and a content block for the items that
want to be laid out in a vertical sequence. We'll use the Layout composable. And as before, we'll
measure each item we're given without further
restricting their size they can be. Our columns' height will then be
the sum of the measured heights of all items. And its width will be that of
the widest item it contains. We report our size, and
then place each item by tracking the y position
and incrementing it by each placed item's height. That was a pretty
simple example. The actual column composable
is far more powerful, supporting layout weights,
alignments, and more. But hopefully, this illustrated
the basics of placement. GEORGE MOUNT: Let's look
at a different example, building a regular grid. We'll create a vertical
grid composable function which accepts a parameter
for the number of columns in the grid defaulted to 2. Again, we'll use the Layout
composable to implement this. Each column will use an
equal portion of the layout's maximum width for its width. We use this to construct
a new Constraints object for the items. We use the copy method to
keep the incoming height constraints, but
specify the exact width. We can then measure each
item with these constraints and then go on to place
them into the grid. This concept of creating new
constraints for child content is how you implement
custom measurement logic. The ability to create different
constraints to measure children is key to this model. There is no negotiation
between the parent and child. The parent passes a range of
allowable sizes, expressed as constraints. And once the child chooses
its size from this, the parent must
accept and handle it. This design has some
nice properties. It means that we can
measure the entire UI tree in a single pass, and
forbid multiple measurement cycles. This was a problem
in the view world, as nested hierarchies that
performed multiple measurement passes could lead to quadratic
number of measurements on leaf views. Compose's design prevents this. In fact, if you
measure an item twice, then Compose throws
an exception. Because of the stronger
performance guarantees, it opens up new possibilities,
such as animating layout. NICK BUTCHER: While creating
generic layouts are useful, another use case for
the Layout composable is creating a single
specific layout that your app design calls for. For example, in their
Jetsnack sample app, our design called for a custom
bottom navigation design where the selected
item displays a label. Otherwise, only an icon. Furthermore, the design
called for the item's size and position to animate
based on selection. We created this
using a custom layout to get precise control over
animating this layout change. Unlike the previous
examples, this composable exposes two specific slots for
the icon and the text label. It also accepts an
animation progress value that callers need to provide. This is a float between 0 and 1. When it's 0, the
item is unselected. So only renders the icon
centered in its bounds. When progress is 1, then
it renders both the icon and the label. Between these values, we animate
the text size and both the text and icon's position to
keep the pair centered. In our custom layout,
we wrap the given icon and text in boxes. This is purely so that
we can apply a layer ID modifier to each. We do this to be able to
identify the measurables in our measure block. It's more robust than
relying on their ordering. We retrieve each measurable
and measure them. We extracted the placement
logic to another function for readability. And finally, here,
we perform the maths to place the text and icon
depending upon the animation progress value. This example is possible
because layouts in Compose are so performant that we
can animate measurement or placement or drive
them with a gesture. This was hard to achieve
in the view system, where animating layouts
was discouraged due to performance concerns. Hopefully, these
examples have given you both an understanding of
how the custom layouts work as well as some ideas of
where they can be useful. I'd suggest going
custom if you need to build a design that
might be hard to achieve with the standard layouts. You can build most things
with enough rows and columns. But sometimes, that proves
hard to maintain or evolve. Custom layouts also give
you very precise control over measurement
or placement logic. This makes it easier to
expose controls to callers. For example, to animate or
drive layout with a gesture. We're working on new APIs
for animating placement. So in the future, you might
be able to achieve this without having to
write your own layouts. Lastly, going custom
gives you complete control over performance
characteristics. More on this later. The standard layouts are
powerful and flexible, but they also need to
cater for many use cases. Sometimes, going custom when
you know the exact details of what you need
to implement can be the right tool for the job. GEORGE MOUNT: So we've looked
at the layout composable and how to build custom layouts. But if you've built
any UI with Compose, then you know that the modifier
plays a key role in layout, configuring size and position. But how do they participate
in the layout model we've explored? Previously, we showed
that the layout composable accepts a modifier
chain as a parameter. Modifiers decorate the
element they are attached to and can participate in the
measurement and placement before the layout's own
measurement and placement. Let's examine how they do that. There are many different
types of modifiers that affect different behaviors,
such as drawing modifiers, pointer input modifiers,
and focus modifiers. Of particular interest to
us is the layout modifier. It offers a measure
method, which is nearly identical to
the Layout composable, with the exception that it only
acts on a single measurable rather than a list
of measurables. This is because modifiers
are applied to a single item. In this measure
method, the modifier can alter constraints or
implement custom placement logic, just like a layout. This means you don't always
need to write a custom layout. If you want to only
act on a single item, you could instead
use a modifier. For example, let's examine
how the PaddingModifier works. The factoring function
builds on the modifier chain, creating a PaddingModifier
object, which captures the desired padding values. The PaddingModifier class
is a layout modifier. It alters measurement by
shrinking the outer constraints by the padding size, and
then measures its content. Then it places the content,
offset by the desired padding. You can, of course, write
your own layout modifiers, or use Modifier.layout,
which makes it simple to add custom
measurement or placement logic to any composable
directly from the modifier chain without having to
create custom layout. For example, here we're
implementing custom measurement and placement directly
in the modifier chain. NICK BUTCHER: While layouts
receive a single modified parameter, this models
a chain of modifiers that are applied in order. Let's walk through
an example of this and see how it interacts
with the layout model. Consider wanting to create
a fixed-size blue box and center it within its parent. We can do that with
this chain of modifiers. Let's see why they
produce this behavior and what's going
on under the hood. We can easily size
and draw the box. But this places it in the
top-left corner of its parent. To center it, we need to
use a layout modifier which affects placement. wrapContentSize is
just such a modifier. It allows content to
measure at its desired size, and then uses its align
parameter to place it. Align defaults to center,
so we can omit it. Unfortunately, this isn't
quite enough to achieve the result we want. Our box is still
in the top left. Because most layouts
wrap their content, we need to make measurement take
up the entire space in order to center a box within it. We can do this by adding a
fillMaxSize layout modifier before the wrapContentSize. Let's examine how these
modifiers achieve this. To be concrete,
imagine that our box is being placed in a container
that can be up to 200 by 300 pixels in size. These constraints will be
passed into the first modifier in the chain. fillMaxSize essentially creates
a new set of constraints, and sets both the min
and max width and height to be equal to the maximum
incoming width and height so that it fills to the max. So exactly 200 by
300 in our example. These constraints get
passed down the chain to measure the next element. So the wrapContentSize
modifier receives them. It creates new constraints which
relax the incoming constraints, letting the content measure
at its desired size. So 0 to 200 by 0 to 300 again. This might look like it's
just undoing the fillMax step, but remember that we're
using this modifier to center the item, not to size it. These constraints are
passed down the chain to the size modifier,
which creates an exact size constraint to measure
the item with. So it specifies that it
should be exactly 50 by 50. Finally, these
constraints are then passed to the box's layout. It measures and returns its
resolved size of 50 by 50 back up the modifier chain. So the size modifier resolves
its size also to 50 by 50 and creates placement
instructions. Then wrapContent
resolves its size and creates placement
instructions to center its content. That is, because
this modifier knows that its size is 200 by 300
and that the next element is 50 by 50, it uses the
centering alignment to create placement
instructions to center it. Finally, fillMaxSize resolves
its size and placement. Hopefully, this example
helps you to understand how the modified chain is executed. It works exactly like
the tree of layouts, except each modifier
has a single child, the next element in the chain. Constraints are passed down,
which subsequent elements use to measure themselves. And then, resolved
sizes come back up, and placement
instructions are created. Hopefully, this also illustrates
how the order of modifiers matters. This design makes it easy to
combine different measurement and layout policies
together using modifiers to compose functionality. GEORGE MOUNT: Now
that we've covered the fundamentals of
the layout model, there are a number
of advanced features that it supports that
are good to be aware of. You might not always
need them, but they can allow you to build more
advanced functionality. First, let's look at
intrinsic measurement. I told you before that Compose
uses a single pass layout system. Well, it isn't entirely true. We can't always do
layouts in one pass. Sometimes you need to know
something about the child sizes before finalizing
the constraints. One example is a menu pop-up. The menu looks easy at first. Here we have a column
with five menu items. It mostly works, but each
menu item is a different size. That's easy to fix. Just make them take
up the maximum size that they're allowed. But that doesn't work either. The menu window grows
to its maximum size. The solution is to use the
maximum intrinsic width to determine how big to be. Here, we've asked
column how big it will be if we give each child
as much room as it wants. For text, that would
be how wide it would be when rendered on a single line. Once the intrinsic
size is determined, it is used to set the
size of the column. The children can then fill
the width of the column. So what happens if we
use min instead of max? It asks what the minimum size
that the column wants to be. Text's minimum intrinsic
width is the width that has one word upon each line. You can see that we end up
with a word-wrapped menu. NICK BUTCHER: The modifiers
that we've looked at so far have been general purpose. That is, they can be
applied to any composable. Sometimes, your
layout might want to offer some behavior that
requires some information from the child. This is where parentData
modifiers are useful. Let's go back to our previous
example of the blue box centered in its parent. This time, we know that the
box is inside another box. The content within a box
runs in a receiver scope called BoxScope. BoxScope defines modifiers
that are only available for use within a box. One modifier that
it offers is a line which provides the exact
functionality that we need to send to the blue box. So if we know that our blue
box is within another box, we can instead use the align
modifier to position it. Align is a parent
data modifier, not a layout modifier like
we've seen before, as it simply communicates some
information to its parent. It can't be used when
we're not within a box. It has information
to the parent box that it uses to
layout its children. You can, of course, write
your own parent data modifiers for your own custom layouts to
allow children to communicate some information to the parent
that they should use in layout. GEORGE MOUNT:
Alignment lines allow us to align by something
other than the top, bottom, or center of a layout. The most commonly used
alignment line is textBaseline. My designer gave me this design
with an icon next to some text. That's pretty easy to do. I'll just use a row with
an icon next to the text. The Android Studio preview
shows me that I made a mistake. The icon is at the top. And that looks weird. I can fix that. We can center both
the icon and the text. Looks good to me. I showed it to my designer,
and she pointed something out. There's a misalignment. The icon should ride along
the baseline of the text. I can fix this. I can tell the row to align
the text to its baseline. First, I use the alignByBaseline
modifier for the text. But what about the icon? It doesn't have a baseline
or any other alignment lines. We can use the alignBy
modifier to tell the icon to align anywhere we want. In this case, I know
that the icon's bottom is where I want it to align,
so I can just align to that. And that's great to me. Let's bring back the
ruler to double check. Yes, perfectly aligned. How do we align
with a nested child? It would be troubling if
we had to modify a button to have a baseline alignment. And that still wouldn't
account for the other custom alignments. Fortunately, the alignments
pass through the parents. Here I'm aligning to the
baseline of the button. But the button just takes
the value from its child. You can even create your
own custom alignments in your custom layouts to
allow other composables to align to it. NICK BUTCHER: An interesting
and powerful provided layout is BoxWithConstraints. In composition, we
can conditionally choose what to display using
logic and control flow. But sometimes, you might
want to make decisions based on how much space is available. For example, choosing a
different representation on larger screens. Returning to the three
stages of Compose, sizing information isn't
available until the layout phase. That is, it isn't generally
available at composition time to use to make decisions
about what to show. That's where
BoxWithConstraints comes in. It's a composable
similar to Box, but it defers composition
of its content until the layout phase, when
layout information is known. Content in a
BoxWithConstraints runs in a receiver scope that exposes
its constraints determined during the layout phase as
either pixel or DPI values. So content within it can use
these constraints to choose what content to compose. For example, choosing
different presentations based on the max width. So those were some of the
advanced layout features that are useful to be aware of. Now, let's turn our
attention to performance. We've already covered how
the single-pass layout model can prevent spending
excessive time in measurement or placement. We also showed that
measurement and placement are distinct sub-phases
of the layout pass. A consequence of this
is that any changes that only affect placement of
items, not measurement, can be executed separately. Consider this example
from the Jetsnack sample. A product details page has
a coordinated scroll effect, where a number of elements
move or scale based on scroll. Let's focus on this title area. It scrolls with
the main content, pinning at the
top of the screen. To implement this,
the different elements are separate composables
stacked in a box. We hoist the scroll
state and pass it into the Body
composable, which uses it to make its content
vertically scrollable. We can then observe
the scroll position from other composables,
like Title. But how we observe this
matters for performance. For example, a
naive implementation might be to use the scroll value
to simply offset the content. The problem with
this approach is that the scroll is an
observable state value, and the scope that
you read it in dictates what compose
needs to re-execute when the state changes. In this example, we're
reading the scroll value in composition,
and then using it to create an offset modifier. Each and every time that
the scroll value changes, this title composable will
have to be recomposed, and the new offset modifier
has to be created and executed. Returning to the three
stages of Compose, because the scroll state
is read in composition, any changes cause recomposition. When you recompose, you also
need to run the later stages, layout and drawing. But we're not changing
what we're showing, just where it is. We can be more efficient. If we change our
implementation to, instead of accepting
the raw scroll position, we pass a function
that can provide it. Then, we can only
call this lambda and read the scroll state
at a different time. We can instead use
a different form of the offset
modifier which accepts a lambda producing the offset. This means that the
modifier doesn't need to be recreated
when the scroll changes. The scroll state's
value is only read during the placement stage. So when the scroll
state changes, we only need to run
placement and drawing. No recomposition or measurement,
improving our performance. In fact, going back to our
custom bottom nav example, this exhibits the same issue. And we can apply the same fix,
accepting a function which provides the current
animation progress so we don't need to rerun
composition, only layout. As a rule, you
should be suspicious whenever you see a parameter
to a composable or modifier that you expect to
change frequently, as this could be a cause
of over-composition. You should only
need to recompose when you change what you show,
not where or how you show it. GEORGE MOUNT: BoxWithConstraints
allows for composition that depends on layout. But what is it doing? It starts a sub-composition
during layout. For performance reasons, we
want to avoid composition during layout as
much as possible. Prefer using layout that
changes based on the size over using BoxWithConstraints. Use BoxWithConstraints when
the types of information change with the size. Let's talk about improving
layout performance. There are times
when layout doesn't need to measure all of its
children to know its own size. In this example, I have a
card with three children. The icon and the title
make up the title bar. And then, there's the body. I know that the size
of the icon is fixed, and the title height is
the same as the icon. When the card is measured, I
only need to measure the body. Its constraints
are just 48 DP less height than the layout side. The size of the card is
just the size of the body plus 48 DP in height. The system recognizes that
only the body was measured, so it is the only child that
is important for determining the size of the layout. The icon and text must
still be measured. So that can be done in
the placement block. Let's suppose that
the title is Layout. When the title has
changed, the system doesn't have to rerun the
measurement of the layout. So the body doesn't
get re-measured, saving unnecessary work. NICK BUTCHER: Returning
to our stated goals for the layout
system, hopefully, you've seen how it's far easier
to implement a custom layout. It's as simple as writing
a single function. Additionally, modifiers allow
you to build and combine layout behaviors,
making it even easier to achieve the exact
functionality that you need. The layout system supports
advanced features, like custom alignment
across nested hierarchies, creating custom parent
data modifiers just for your layout, automatic
right-to-left support, or deferring composition until
layout information is known. And we've seen how
the single-pass layout model or the potential to skip
re-measurement and only run replacement enables you to
write highly performant layout logic that you can even animate
or drive with a gesture. We hope that understanding
the layout system helps you to build exactly the
layouts your design calls for and create excellent apps
that your users love. To learn more, check
out our documentation, learning pathway, and samples. Thanks for watching. [MUSIC PLAYING]