Optimizing Performance in Jetpack Compose
Do you say what is wrong with this Lazy Column, or why is that button recomposing while I am not even changing its state? If the answer is yes, here is the solution to the common issues.
Introduction
Jetpack Compose offers a modern and efficient way to build Android UIs. However, developers often encounter performance challenges, particularly when working with Lazy Lists and mysterious button recompositions. If you’ve ever wondered what might be causing these issues, read on for insights and solutions.
Troubleshooting Lag in Lazy Lists
Lazy lists are a potent tool in Jetpack Compose, but their optimal implementation can be more nuanced than anticipated. Even with careful development, performance issues may persist. Let’s explore common challenges and effective solutions:
Utilize key for Large Lists
When dealing with substantial lists, incorporating the key
parameter becomes crucial. Assigning unique keys to list items facilitates more efficient item tracking and can significantly enhance Lazy List performance.
For example, you can utilize the product ID as a key.
LazyColumn {
items(products, key = { it.id }) { product ->
...
}
}
Strategic State Hoisting
Evaluate your state management strategy. Ensure that states are hoisted strategically and not unnecessarily, which can lead to unintended recompositions. Keep state management concise and localized to relevant composables.
Resource Optimization
Consider the impact of resource-intensive elements, such as images, within Lazy Lists. Images with high resolutions and large file sizes can contribute to performance degradation. Optimize images or use more efficient formats to alleviate the strain on system resources.
Convert Images to WebP Format
Android Studio offers a built-in WebP Converter tool, providing a convenient solution for optimizing image resources. Converting images to the WebP format can significantly reduce file sizes while maintaining visual quality, improving performance for Lazy Columns, Rows, and other UI components.
To convert an image to WebP, right-click on it and select “Convert to WebP”.
You can see the difference between the original one and WebP.
Also, you don’t have to decrease your image quality to decrease file size. Lossless scaling saves some space.
derivedStateOf for Efficient State Handling
Leverage the derivedStateOf function to compute derived states based on other states efficiently. This can help in scenarios where complex state dependencies trigger recompositions. By using derivedStateOf, you can fine-tune state management and reduce unnecessary updates.
val buttonEnabled by remember {
derivedStateOf { isEmailValid(email) }
}
buttonEnabled
state will change only ifisEmailValid
changes.
Stability
It would help if you made it stable when faced with an unstable class that causes performance issues.
Important: Before you fix stability issues, you should learn to properly diagnose them. For information, see the Diagnose stability issues guide.
@Stable and @Immutable Annotations
A possible path to resolving performance issues is to annotate unstable classes with either @Stable
or @Immutable
.
Annotating a class overrides what the compiler would otherwise infer about your class. It is similar to the !!
operator in Kotlin. It would help if you were very careful about using these annotations. Overriding the compiler behavior could lead you to unforeseen bugs, such as your composable not recomposing when you expect it to.
- The
@Stable
annotation indicates that a particular value will not change over time, ensuring stability in UI components. For instance, you can mark constants or values that remain constant throughout the component's lifecycle.
@Stable
class UiState {
var isLoading by mutableStateOf(false)
}
- The
@Immutable
annotation can applied to classes or data classes to signify that instances of these classes are immutable. Once created, their state cannot be altered, making them safe to share references.
@Immutable
data class Contacts(val names: List<String>)
Warning: These annotations don’t make a class immutable or stable on its own. Instead, by using these annotations you opting in to a contract with the compiler. Incorrectly annotating a class could cause recomposition to break.
Immutable Collections
A common reason why Compose considers a class unstable is collections. As noted in the Diagnose stability issues page, the Compose compiler cannot be completely sure that collections such as List, Map
, and Set
are truly immutable and therefore mark them as unstable.
class Shirt {
…
val colors: List<String>
…
}
This class will mark
Unstable
by Compose compiler.
To resolve this, you can use immutable collections. The Compose compiler includes support for Kotlinx Immutable Collections. These collections are guaranteed immutable, and the Compose compiler treats them as such. This library is still in alpha, so expect possible changes to its API.
You can make colors
stable using an immutable collection. In the class, change the colors‘
type to ImmutableList<String>
:
class Shirt {
…
val colors: ImmutableList<String> = persistentListOf()
…
}
After doing so, all the class’s parameters are immutable, and the Compose compiler marks the class as stable.
Conclusion
In essence, optimizing performance in Jetpack Compose is necessary for the best user experience. By tracking lag issues and unnecessary recompositions through state hoisting, resource optimization, and ensuring stability, developers can elevate their app’s performance. But adding things doesn’t always solve problems. As Antoine de Saint-Exupéry once said:
“Perfection is achieved not when there is nothing more to add, but when there is nothing left to take away.”