[MUSIC PLAYING] SIMONA STOJANOVIC: Hi there. In this talk, we're going to
cover lazy layouts in Compose, starting with some of
the basic use cases and continuing with
the more advanced. We will talk about how to
implement lazy lists and grids, how to add items, animate
inside your content, cover new and upcoming features,
as well as useful optimization tips and insight into
lazy layouts performance. Now, if you've ever used
RecyclerView before, you'll be very familiar
with the main concept behind lazy layouts-- rendering a scrollable
list of items as they become visible in the screen
rather than all at once. In cases where you have a
significant number of items or large data sets to
load, using lazy layouts would allow you to
add content on demand, increasing your app's
performance and responsiveness. Currently, in Compose
1.2, lazy layouts consist of lazy column, a
vertically scrolling list, lazy row, a horizontal one, and
lazy grids, which offers both. There is, however,
a big difference between the RecyclerView
used in the view system and lazy lists used
in the Compose-- the amount of code
you need to write. This is the code you need for
a RecyclerView adapter and view holder, and the RecyclerView's
XML layout, and then the layout for the RecyclerView item, and,
finally, binding the adapter to your RecyclerView. The same amount of work with
lazy lists looks like this. They achieve the same result
with fewer lines of code, which makes them notably easier
and simpler to read and write. To add items to
your lazy lists, you can use its lazy
list scope DSL block to accept the provided content
and display as list items. This approach is
slightly different from other regular
layouts in Compose and allows you to focus on
describing your item content and let lazy lists
handle everything else. The API provides two possible
ways to insert items. You can describe one item using
the item block or multiple ones with the items block. You can also use a combination
of both in the same list to describe your content. In cases where you need to
know the index of each item-- for example, to set a different
background to your even and odd item positions--
you can use the items index block to gain access
to this information. Once your items are
added, the next step would be to think
about how to customize the list's appearance. With lazy lists, you'll see
that this is very easy to do. For example, it's
a common use case to add some padding
around your list content. If all you want to do is
indent your entire list, you can do so easily with
the padding modifier. However, when scrolling the
first and the last item, this will clip your content
to remain within the bounds of the list padding. If, however, you want to keep
the same padding but still scroll your content within
the bounds of your list without clipping
the content, you can do so by passing padding
values to the content padding parameter of your list. This allows you to set the
same padding for each side individually. Now let's refine your
content's UI even more. By default, your
list items would be glued together like this. So to add some neat spacing
in between your list items, you can use the
arrangement spaced by and pass it to your list. As lazy lists are
meant to be used when you have a lot of
content or items to display, chances are you'll have
to do a lot of scrolling through the list. Therefore, one of the
most important features to keep in mind is how to
programmatically observe and react to scrolling. The key here is lazy list
state, an important state object that stores the scroll position
and contains useful information on your list. To ensure that your state is
remembered across compositions, hoist it by using
rememberLazyListState and pass it to your list. Lazy list state grants
you access to the index and offset of your first
visible item, two properties that you'd most often use
when reacting to scroll. For example, you can use
the first visible item to determine whether to
show a button that scrolls to the top of your list. Keep in mind that lazy list
state properties change very often, so simply
reading its properties inside one composition
will trigger a lot of potentially
unnecessary recompositions. To avoid this, wrap them in a
remember and derivedStateOf. This guarantees that the
recomposition will only occur when the state property
used in the calculation changes. For more detailed
information on this, take a look at our Common
Performance Gotchas talk. Lazy list state also provides
other useful information via its layoutInfo object,
such as currently visible items and the total item count. Use it to fetch the indices of
all currently displayed items in cases where, for
example, you wish to play the content
within your items when fully visible
and pause when not. Now, let's take a look
at a practical example and fully implement the
scroll-to-top button. We can use the lazy list
state, as it provides a handy suspend
scrollToItem function that enables us to snap back
or forward to an item of a specific position. If you prefer a smoother
animated transition, you can use
animateScrollToItem instead. Keep in mind both are
suspend functions, so they need to be invoked
within a coroutine scope provided by remember
coroutine scope. We then launch the
scroll function within the coroutine's scope
inside the onClick parameter of the button, and that's it. A few simple steps, and
the button tap now takes us to the top of the list. MIHAI POPA: In
addition to these, Compose also offers
out-of-the-box lazy grids. Lazy grids have recently been
reworked with new capabilities and the API graduated from
experimental to stable in Compose 1.2. Let's look at them
in more detail. Lazy grids can be used via
the lazy vertical grids and the lazy horizontal
grid composables. Lazy vertical grids
will display its items in a vertically scrollable
container spanned across multiple columns, while
the lazy horizontal grids will have the same behavior
on the horizontal axis. Using a grid is really simple. It looks almost identical to
the usage of a lazy column, with an added description for
the vertical grids columns. Here we specify
that the grid should have exactly two of them. Using a grid in Compose is
very similar to using a list. Lists have the same
powerful API capabilities, and they also use a very similar
DSL for describing the content. So, for example, in order to
add spacing between items, we simply use spaced-by
arrangements, same as for this. However, a grid takes both
vertical and horizontal arrangements. Grids accept content padding
and the lazy grid state, which has the same useful
functionality, as lazy list state has four lists. Lazy grid state
exposes information such as firstVisibleItemIndex
and layoutInfo to provide insights
into the current layout state of the grid. It also offers the same APIs
for scrolling-- scrollToItem and animateScrollToItem Being so similar with
the lazy list API, I will not going
to detail on this. Let's instead focus on features
that are specific to grids. We saw before how to
achieve a vertical grid with a fixed number of columns. This means that the grid's
available width will be divided by the number of
columns, and each of them will take that amount of space. If we also add the
spacedBy arrangement, the spacing will be deducted
from the available width before calculating
the column sizes. But there might be cases when
a fixed number of columns is not ideal. In our sunflower
example, we used the grid with exactly two columns. We tested the sample on a
phone, and it looked great. However, when we ran
the sample on a tablet, things did not look so great. The images were too wide,
and because the height of plant cards was fixed, a very
large of the original images was cropped out. In order to solve this,
we used adaptive sizing for columns via the
GridCells.Adaptive API. This allows specifying
width for items, and then the grid will fit
as many columns as possible. Any remaining width
is distributed equally among the columns after
the number of columns is calculated. This adaptive way of
sizing is especially useful for displaying
sets of items nicely across different screen sizes. But what if we have more
complicated column sizing requirements? The good news is that Compose
lets you go completely custom. You can implement grid cells,
the interface implemented by grid cells fixed,
and grid cells adaptive. The interface has
only one method, which calculates the
column configuration. For vertical grids, it gives
you the available width and the requested horizontal
spacing between items. The return value of the
method is a list containing the calculated column widths. In our case, let's say
that the first column should be twice the width
of the second column. We calculate the
width of the columns by sizing the first column
to 2/3 of the available space minus spacing. The second column will
occupy the remaining space. We finish by returning our
list with the calculated width. See how the number
of columns will be equal to the length
of the returned list. Now that we have seen how
the structure of grid cells is defined, what if your design
requires only certain items to have non-standard dimensions? Look at this sunflower
sample design, where the plants are categorized. We want to have a header at
the start of each category, showing the name
of the category. Each header occupies the
entire width of the row. To achieve designs
like this, grid support providing
custom column spans for items, which can be
specified via the span parameter of the item and items
methods in the grid scope DSL. Here we are providing full
row span to the header. Max line span, one of
the spans scopes values, is particularly useful
when using adaptive sizing, as the number of
columns is not fixed. There is one more value
in the span lambda scope, maxCurrentLineSpan,
which represents the span that the item can occupy
on the current line, which will be different
from maxLineSpan when the item is not at
the beginning of the line. Now let's look at
the plant cards. We do not need to
explicitly provide spans for the items because
they each occupy a span of 1, which is the default.
However, let's say we want to highlight some items. For this, we can
define custom spans. In this example, every other
element will have a span of 2. This is how it
looks on a tablet, and this is how it
looks on a phone. Notice how elements that do
not fit on the current line are automatically line
breaking, leaving gaps behind. ANDREY KULIKOV: Up
until now, we've covered how lazy
lists and grids work. But what if you
need to go custom and what you need is not
covered by the functionality of the existing lazy components? In the RecyclerView
world, you would be able to implement
your own layout manager. To achieve the same
goals in Compose 1.2, we are adding a new experimental
API called LazyLayout. Lazy list and grids are built
on top of the LazyLayout API. If you want to implement your
own lazy layout for your custom use case, you can consider
forking one of the existing implementations. For example, Compose for Wear
OS has a custom Wear-specific lazy list which
scales items based on the distance to
the component center and is a perfect use
case for LazyLayout. We will use LazyLayout
API internally in order to build staggered
grids, grids where items are allowed to have
different heights in the vertical orientation. We hope you will be able to
try it out and build something amazing with it. We are looking forward to
your bug reports and feedback. One of the most requested
features is animating list item changes, so in Compose 1.1 we
introduced experimental item reordering animations
for lazy lists. In Compose 1.2, we also
ported this functionality to lazy grids. Each cell can be
animated individually. This API is really simple. You just need to set the
animate item placement modifier to the item content. You can even provide custom
animation specs if you need to. Make sure you provide
keys for your items so it is possible to
find the new position for the moved element. Aside from enabling the
animation, providing keys allows us to handle reordering
correctly-- for example, to move the remembered state
from within the item composable together with the item
when its position changes. However, there is one
limitation on what types you can use as item keys. This type should be supported
by Bundle, Android's mechanism for keeping the states when
the activities are created. It supports types like
primitives, enums, or parcelables. This is required so
that they remember savable inside the
item composable can be restored when the
activities are created or even when you scroll away
from this item and scroll back. Aside from reorderings,
we are currently working on item animations
for additions and removals, so stay tuned. SIMONA STOJANOVIC: Now
let's cover some useful tips so you can make the most
out of your lazy layouts. Tip number one-- don't
use 0-pixel sized items. You might find yourself doing
this in scenarios where, for example, you expect
to asynchronously retrieve some data like images
to fill your list's items at a later stage. To understand why you
should avoid this, let's quickly explain how lazy
layout handle their children. The same principle applies
to all lazy lists and grids, but let's take one as
a concrete example. When first measuring
its items, lazy column will initially measure them with
infinite height constraints, meaning it wouldn't
constrain them in order to keep composing
them until it fills the available viewport
using their measured height for calculation. After that, the list stops
composing for their children. This allows adding
and removing content on demand, the concept which
lazy layouts are built on. Now, if your list
items are initially occupying 0-pixel
height, that will mean that in the
first measurement lazy column would
compose all of its items, as their height is 0 pixels. And it could easily fit
them all in the viewport. A few milliseconds
later, your images load. Recomposition of items
starts to display the images. The lazy list realizes only
a few items can actually fit the viewport
and discards all of the other items that
have unnecessarily been composed the first time around. To avoid this, you should set
default sizing to your items so that the lazy layouts can
do the correct calculation of how many items can in
fact fit the viewport. When the approximate size
of your items after the data is asynchronously
loaded, a good practice is to ensure your item
sizing remains the same before and after loading-- for example, by adding
some placeholders. This will help maintain the
correct scroll position. Tip number two, avoid
nesting components scrollable in the
same direction. To be very precise, this applies
only to cases when nesting scrollable children without a
predefined size inside another same direction-scrollable
parent-- for example, trying
to nest a child lazy column without a fixed
height inside a vertically scrollable column parent. In fact, even if you tried to
do so you wouldn't be able to. Compose would prevent you. I know this might
sound confusing, as in the view
system you can wrap a RecyclerView
inside a scroll view, but this comes at a price. The performance will
be impacted severely. The reason is similar to what we
mentioned in our previous step on 0-pixel size items. When measuring its
children, scroll view would do so with
infinite allowed height, allowing your RecyclerView
to be unconstrained and subsequently allowing it
to create all of its items instantly and invalidating
its recycling purpose. Therefore, Compose
tries to steer you away from this pattern and
prevent this issue. However, the same result
can be achieved simply by wrapping all of your
composables inside one parent lazy column and using
its powerful DSL to pass in different types of content. This enables omitting
single items like a header as well as multiple list
items all in one place. However, keep in
mind that cases where you're nesting different
direction layouts-- for example, a scrollable parent
row and a child lazy column-- are allowed as well as
cases where you still use the same direction layouts
but also set a fixed size to the nest of children. MIHAI POPA: Let's move
on to tip number three. Beware of putting multiple
elements in one item. So far, we have seen list
usages that were straightforward and emitted one element per
item in the DSL lambdas. But since there is no
compile time enforcement, you might rightfully
wonder, what happens if I omit
more than one element. In this example, the
second item lambda emits two items in one block. The good news is
that the lazy layout will handle this gracefully. It will lay out elements
one after another as if they were different items. So then why would you just
not put all the elements in a single item? Well, there are a couple
of problems with doing so. Firstly, when multiple elements
are omitted as part of one item, they are
handled as one entity, meaning that they cannot be
composed individually anymore. If one element becomes
visible on the screen, then all elements
corresponding to the item have to be composed
and measured. As you might suspect,
this can hurt performance if used excessively. In the extreme case of putting
all elements in one item, it completely defeats the
purpose of using a lazy layout. Apart from potential
performance issues, putting more
elements in one item will also interfere
with scrollToItem and animateScrollToItem. This is because the index
parameter of these methods will be interpreted as relative
to the number of items derived from the DSL usage, not to
the actual number of elements. In this example,
calling scrollToItem 2 will actually bring
item 3 on top. However, there are
valid use cases for putting multiple
elements in one item. One of them is having
dividers inside the list. Dividers can be part
of the previous item, as shown in this example. We do not want dividers to
change scrolling indices, as they shouldn't be considered
independent elements. Also, performance
will not be affected, as dividers are small,
so a divider will likely be visible when the item
before it is visible. ANDREY KULIKOV: Let's move
on to tip number four. Consider using
custom arrangements. Usually, lazy lists
have many items, and they could occupy
more than the size of the scrolling container. However, when the list is
populated with few items, your design can have more
specific requirements for how it should be
positioned in the viewport. It is important to test
your list in this state and consider how you want
the items to be displayed. For example, you might need
to have a footer displayed at the viewport bottom when they
don't have enough items to fill the available height. To achieve this, we can use
custom vertical arrangement and pass it to our lazy column. Our TopWithFooter object
underneath implements arranged method. This gives us total size, which
will be the viewport height, and sizes, which will be
the height of the items. The resulting item of sets
needs to be calculated in the outPositions array. Firstly, we are
going to position items one after another. Secondly, if the
total used height is lower than the
viewport height, we are positioning the
footer at the bottom. As you can see,
arrangements can be used to easily customize how
lazy list plays out children and accommodate your
specific design requirements. We know that
scrolling performance is important for you. The first thing
to be aware of is that you can only
reliably measure the performance of a lazy layout
when running in release mode and with array
optimizations enabled. On debug builds, lazy layout
scrolling will appear slower. Also, if you noticed that your
app is slow on first start up and then get
faster, most likely this will be fixed by the
default baseline profile. For more information
on this topic, please watch our Common
Performance Gotchas talk. Let's go over some
of the optimizations we do under the hood in order
to make scrolling of lists more performant. The first optimization
we introduced is composition reusing. It is somewhat similar to
what RecyclerView does. Let's see how it works. After scrolling, when the
previously visible item is not needed anymore, we will
not dispose it straight away. Instead, we keep a
few of such items to be reused when we need
to compose a new item. Firstly, this allows
us to save some time on the work needed
to dispose and start a completely new composition. Secondly, we also try to reuse
what we call layout node. It is an internal representation
of every layout in your UI tree. So when we compose a new
item in a reused composition, we try to automatically reuse
inner layout nodes objects for items with
similar UI structure. In fact, if the omitted layout
nodes are completely the same, so they have the same
values for the size and all other
dynamic properties, we can even skip the whole
re-measuring for them. And this is all
done automatically. But there is one thing we
can't do without your help. In Compose 1.2,
we added a new API which allows you to
specify the content type for each item of the list. Imagine you are composing
a list consisting of two different types of items. In that case, when you
provide the content type, we can reuse compositions
only between the items of the same type. As we've said before,
reading is more efficient when you compose items
of similar structure. Providing the content
types will ensure we do not try to compose
an item of type A on top of a completely
different item of type B, as by doing so we
are losing most of the benefits of
compositions reusing. The second performance
optimization we implemented is called prefetching. Again, it is very similar to
what is done in RecyclerView. When scrolling without the
need to handle any new items, lazy column has no problems
doing what it needs to do within the frame budget. If you take a look at
what are the main steps, we can see that on
the UI thread we only need to do
the layout in order to apply the new positions
and then rerecord new drawing commands. Then the draw information
is carried over to the render thread, which
passes the comments over to the GPU. However, when a new item
comes on the screen, more work is required, as we
first need to compose and then measure the new item content. Such extra steps
can cause a junk when the time needed for all
the steps exceeds the frame boundary. Maybe we can get those
items ready earlier. As you can see in
the previous frame, the UI thread was doing
nothing after it finished all their needed work. Let's move our extra step there. This is what we
call prefetching. There isn't the available
time left in the frame to precompose and then
measure the items that are about to come on the screen. This way, the work
is done earlier instead of just doing it right
when everyone is waiting. So in this section,
we explored what optimizations we
do on the hood, how you can help by providing
content types and making sure you test on the release builds. And we will continue optimizing
the performance even more in future. MIHAI POPA: We've covered
a lot of different topics on lazy layouts, from
the more basic UI tweaks and exciting new
features to tips on how to properly use the lazy APIs. We also gave you a sneak peek on
how to improve the lazy layouts performance under the hood. We hope this has shown you
just how much you can achieve with lazy layouts in
just a few lines of code and that creating
performant lists and grids is simpler and easier
to do with Compose. Now that we've seen what
lazy layouts are capable of, it's up to you to try them out. For more information, check out
some of our existing material. Thanks for watching. [MUSIC PLAYING]