Optimizing Performance in Jetpack Compose

Muhammed Esad Cömert
Teknasyon Engineering

--

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 if isEmailValid 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.”

If you have any questions feel free to reach me;

--

--