Jetpack Compose LazyColumn Re-Rendering | List Duplication in LazyColumn
Does your UI recompose more than it should? A pesty LazyColumn mistake that impacts performance | List Duplication using LazyColumn
Hello Hello,
Welcome to my space on the internet where I talk about my experiences in the tech space whether it be projects I’m actively working on or events I attend. If you are a tech lover, searching for an answer to your question within the mobile space, or just looking for a fellow women in tech feel free to subscribe and stick around. Happy to have you here in my corner. 😆
Hey everyone! I've been a bit quiet lately, but I’m back and bringing you an article about Jetpack Compose Performance. So have you ever worked with the LazyColumn
composable and noticed any re-rendering, laggy scrolling, or just extra recompositions? I sure did 😣. Poor performance in lists can make your app feel sluggish and unprofessional, leading to a bad user experience 👎🏾.
I was trying to display a list using the LazyColumn
composable but I encountered an annoying issue: every time I scrolled all the way down to the bottom of the list, scrolled up to the top, and then scrolled to the bottom again, it would duplicate the list which was obivously not wanted. On some of the phone devices in the emulator, it would continously add the list everytime I scrolled down but on some tablets, it would crash the app because of a duplicate key issue. The duplicate key issue pointed to the list attempting to be recomposed, creating new items with the same keys as the existing ones.
In Jetpack Compose, composition is the process of building the UI tree, while recomposition is the process of updating that UI tree when data changes. LazyColumn relies on unique keys to efficiently update the list. Without them, it can't differentiate between items.
The list would be shown in a specified state (the success state) and a placeholder screen would be shown if there was an error or if it ended up in the error state (if a problem occurred when fetching data). Now, I noticed that the placeholder screen never duplicated, no matter how many times I scrolled up and down so I knew this was a huge clue! It meant the problem had to be in how and where I was calling the code that generated the main list (the one that should be shown in the 'success' state). So let’s get into the code, what I initially did wrong, how I fixed it, and why that is the fix.
Let's dive into the code that caused me so much grief. Here's what I initially did. Do not use this!:
@Composable
fun MyLazyColumn(){
LazyColumn {
item(key = “text numbers”){ //Adding a single item here, that WRAPS another list! Problem!
addItems(20)
}
}
}
fun LazyListScope.addItems(count: Int){
items(count){ index → // PROBLEM: ‘items’ is meant to be directly inside ‘LazyColumn’
Text(text = “List Item $index”)
}
}
Notice in this code I have an items{}
nested inside of an item{}
. Since addItems extends the LazyListScope
you can directly use items{}
using the LazyListScope
in the LazyColumn
itself. However, in this code I was nesting the items{}
inside of an item{}
which is bad practice because this triggers recomposition. This adds the content of that items{}
code section on the screen every time my success state was hit because it is being treated as a single item. LazyColumn thinks it's just one item, it has to compose the entire list at once, even if only a few items are visible. In other words, nesting items{}
within an item creates a recomposition trap. Every time the composable recomposes, the content inside of the outer item block is re-executed, causing the new items to be added to the LazyColumn
. Remember, LazyColumn
is supposed to efficiently render only the visible items! By forcing the entire list into a single item, we're bypassing this mechanism and forcing the entire list to recompose. Now lets look at what the code SHOULD be, (code you should use):
@Composable
fun MyLazyColumn(){
LazyColumn { // Correct! ‘addItems’ directly inside ‘LazyColumn’
addItems(20)
}
}
fun LazyListScope.addItems(count: Int){
items(count){ index →
Text(text = “List Item $index”)
}
}
Now with this code, the addItems function is directly called in the LazyColumn
which ensures the data/list is only draw once without duplication which also does not enable the recomposition when scrolling occurs. In order words, this ensures that the list data is only drawn once which prevents unnecessary recompositions when scrolling and eliminates the duplication issue. The items block is a direct child of the LazyColumn
which is its intended use.
This took a little time for me to fix but nonetheless, it is an important aspect to understand for future projects or UI issues. Some key takeaways are to try to get down to the details of why something works or doesn’t work because that information can take you a long way when you encounter a similar issue in the future.
In the next article we’ll go through a deeper dive on how LazyColumn is designed for efficiently rendering a larger collection of items by incorporating updates and virtualization!
Thanks for reading! What performance challenges have you faced with LazyColumn
? Share your questions and experiences in the comments below! And if you found this helpful, subscribe for more mobile development content. See you next time!