Enhancing Performance in Jetpack Compose

Guilherme Santana
1st.digital
Published in
4 min readJan 22, 2024

Hello, Android Developers!

If you’re deep diving into the dynamic world of Jetpack Compose, you already know that this modern UI toolkit for Android opens up a world of conveniences and possibilities for developers. In this article, we’ll explore essential practices for enhancing performance in Jetpack Compose, focusing on both the dos and don’ts. With practical code examples, we’ll show how few changes in your code can significantly boost the efficiency and performance of your applications.

1 — Use @Immutable and @Stable to avoid unnecessary recompositions

By default, Jetpack Compose considers classes as unstable if they contain mutable variables (var) or mutable collections like ArrayList. This means Compose might frequently recompose these classes, assuming their state could have changed. But using these two annotations, we can avoid this wrong behavior. Let's understand.

@Stableis basically a promise to tell the Compose compiler that this object might change, but whenever it changes, Compose runtime will be notified. So, in the end we are telling to compose "Hey, I’ll always pass a Immutable list to you and I know that if I pass a mutable List this can lead issues to the app". Let’s see in the example how to use.

@Stable
class ProductListState(
val products: List<Product>,
val isLoading: Boolean
)
//usage
@Composable
fun ShowProducts(
modifier: Modifier = Modifier,
productListState: ProductListState
) {
LazyColumn(modifier = modifier) {
items(productListState.products) {
Text(text = it.name)
}
}
}

@Immutable is used to mark classes that are completely immutable. Once an object of this class is created, nothing in it can change in this object, reducing unnecessary recompositions. Warning: if you add or delete an item to the list, the composable will never be recreated because you use “@Immutable”

@Immutable
class ProductListState(
val products: List<Product>,
)
@Composable
fun ShowProducts(
modifier: Modifier = Modifier,
productListState: ProductListState
) {
LazyColumn(modifier = modifier) {
items(productListState.products) {
Text(text = it.name)
}
}
}

2 — Use Stateless Composables

Use stateless composables in Jetpack Compose offers significant performance benefits. They rely on external data, reducing unnecessary recompositions since they only update when these data change. This also enhances reusability and testability, making the code cleaner and easier to maintain.

Wrong:

@Composable
fun MyOwnButtonWrong(item: String) {
//managing state inside the composable - WRONG
val buttonState = remember { mutableStateOf("Not Clicked!") }
Button(onClick = { buttonState.value = "clicked!" }) {
Text(text = buttonState.value)
}
}

Correct:

@Composable
fun MyOwnButton(item: String, onClick: () -> Unit) {
Button(onClick = onClick) {
Text(text = item)
}
}
//usage
@Composable
fun HomeScreen() {
val buttonState = remember { mutableStateOf("Not Clicked!") }
MyOwnButton(
item = buttonState.value,
onClick = { buttonState.value = "Clicked!" }
)
}

3 — Use the ‘key’ parameter in Dynamic Lists

Using the key parameter helps Compose understand which items have changed, remained the same, or been removed, optimizing performance. This avoids unnecessary recomposition of unaltered items, saving significant resources and leading to notably improved performance, especially in large or frequently changing lists. Let's see how to use bellow:

@Composable
fun DynamicListExample(items: List<Product>) {
LazyColumn {
items(
items = items,
key = { it.id }
) { item ->
Text(text = item.name)
}
}
}

4 — Avoid unnecessary Side Effects

Avoiding unnecessary side effects in composables is key to improving application performance. Sometimes we have to manipulate states inside composables, but unnecessary Side effects occur when a composable modifies states or performs actions beyond its core function, potentially leading to unpredictable behavior and unexpected recompositions. Let’s see an example:

Wrong:

@Composable
fun ExempleSideEffect() {
val state = remember { mutableStateOf(0) }
// Side effect: manage state inside composable
Button(onClick = { state.value++ }) {
Text("Click here")
}
//another Side effect
LaunchedEffect(key1 = state.value) {
Log.d("Exemple", "State changes to ${state.value}")
}
}

Correct:

@Composable
fun ExempleCorrectSideEffect(onClickIncrement: () -> Unit, count: Int) {
// callback to handle the click button
Button(onClick = onClickIncrement) {
Text("Click here")
}
// shows the counter value
Text("Count: $count")
}

5 — Optimize loading images with Lazy Loading

Lazy loading of images in Jetpack Compose optimizes performance and enhances user experience:

  • Loads images only when needed, reducing memory usage and improving app loading time.
  • Saves resources by avoiding the loading of off-screen images.
  • Improves app responsiveness, providing a smoother user experience.
  • Integrates with popular libraries, like Coil and Glide, for efficient image management.

Let’s see a good and bad example:

Correct:

@Composable
fun LazyLoadingWithCoil(url: String) {
Image(
painter = rememberImagePainter(
data = url,
builder = {
crossfade(true)
}
),
contentDescription = null,
modifier = Modifier.fillMaxWidth()
)
}

Wrong:

@Composable
fun LoadImageWithouLazyLoading(url: String) {
val bitmap = loadImageBitmap(url) //hipotetic function that loads a bitmap
Image(
bitmap = bitmap,
contentDescription = null,
modifier = Modifier.fillMaxWidth()
)
}

Conclusion

Throughout this article, we’ve explored several key strategies for optimizing performance in Jetpack Compose. By embracing good practices during the development you can can significantly enhance the efficiency and responsiveness of their Android application. I hope you found these insights helpful and that they inspire you to explore new horizons in your Android development journey. Enjoy the process, and see you soon!

--

--