Lazy layouts in Compose

Video Statistics and Information

Video
Captions Word Cloud
Reddit Comments
Captions
[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]
Info
Channel: Android Developers
Views: 76,670
Rating: undefined out of 5
Keywords: Google Compose, Google Recyclerview, Android Recyclerview, Jetpack Compose, Compose Layouts, Jetpack Compose Layouts, Google I/O, Google IO, I/O, IO, Google I/O 2022, IO 2022
Id: 1ANt65eoNhQ
Channel Id: undefined
Length: 24min 32sec (1472 seconds)
Published: Thu May 12 2022
Related Videos
Note
Please note that this website is currently a work in progress! Lots of interesting data and statistics to come.