DEV Community

David Rufai
David Rufai

Posted on

Jetpack Compose Optimization - Making Your App Run Like a Well-Oiled Machine

tuning
I think we all know how easy it has become when it comes to building the UI in our Android apps since the introduction of Jetpack Compose, we've gone from the days of requiring 3 separate components to display a simple list(no shades to RecyclerView), to achieving the same with just a simple composable - 😍 the LazyColumn. But the thing with new technologies is that they often introduce a new level of complexity, in the case of Compose minimizing the number of re-compositions can be a very huge performance booster. This article aims at providing tips/techniques on how to do just that.

Before jumping right in we first need to understand how a composable is rendered on screen.

compose phases

Composition: In this initial phase, Compose constructs a UI tree, a hierarchical representation of your UI. It executes your composable functions and reading state and builds the structure of your UI. This phase determines what UI elements to display and their relationships.

Layout: Once the UI tree is established, the Layout phase takes over. It measures and positions each UI element within the tree, calculating its size and placement on the screen. This phase ensures that elements are arranged correctly and don't overlap.

Drawing: Finally, the Drawing phase renders the UI to the screen. It takes the layout information generated in the previous phase and translates it into pixels, visually displaying your app's interface. This phase uses the device's graphics hardware to optimize performance.

When the device configuration(e.g, a screen rotation) or a state that the Composable depends on changes, the Composable is "re-composed". meaning it goes through these three phases again, updating the UI if necessary. This ensures that the UI remains consistent with the current state and configurations.

Now that we understand how Compose renders its UI components, let's jump into optimizing our apps.

Writing stable Composables

One way of reducing unnecessary recomposition in your app is by writing stable composables, but what do I mean by that? Stable composables are composables whose parameters are immutable or can be determined to have not changed between recompositions, on the other hand, Unstable composables have parameters that are mutable or cannot be determined to have changed, requiring full recomposition even if only a portion of the data has changed.

take the code block below for example.

@Composable
fun ItemList(items: List<String>) {
    Column {
        items.forEach { item ->
            Text(item)
        }
    }
}

// Usage with un-stable state
@Composable
fun MainScreen() {
    val items =  mutableListOf("Apple", "Banana", "Cherry")
    DisplayItems(items)
}

Enter fullscreen mode Exit fullscreen mode

while this seems normal, this code would always cause DisplayItems to recompose even when the items hasn't particularly changed, this is because items is of a mutable data type, and the compose compiler doesn't know when it actually changes, so to err on the side of caution re-composition is always triggered. To fix this we can use the DisplayItems composable this way.

@Composable
fun MainScreen() {
    // Immutable list is stable
    val items = remember { listOf("Apple", "Banana", "Cherry") } 
    DisplayItems(items)
}
Enter fullscreen mode Exit fullscreen mode

Here, items is stable because it’s created as an immutable list and wrapped in remember, meaning Compose will treat it as stable and won’t trigger recompositions unless items actually changes.

Use derivedStateOf for Computed States

Using derivedStateOf in Compose is helpful when you need to create a computed state that depends on other states, especially if the computation is expensive or the UI doesn’t need constant updates. With using this method computed values could trigger recompositions anytime the dependent state changes, even if the actual computed value doesn’t change.

@Composable
fun SearchScreen() {
    var query by remember { mutableStateOf("") }
    val items = listOf("Apple", "Banana", "Cherry", "Date")

    // Filtering directly without derivedStateOf
    val filteredItems = items.filter { it.contains(query, ignoreCase = true) }

    Column {
        TextField(
            value = query,
            onValueChange = { query = it },
            label = { Text("Search") }
        )
        filteredItems.forEach { item ->
            Text(item)
        }
    }
}

Enter fullscreen mode Exit fullscreen mode

In the example above, filteredItems is recomputed every time the user types in the search bar, even if the filtered list remains unchanged, which isn't really efficient. With derivedStateOf, we ensure that filteredItems only updates when the query value actually changes, minimizing recompositions.

@Composable
fun SearchScreen() {
    var query by remember { mutableStateOf("") }
    val items = listOf("Apple", "Banana", "Cherry", "Date")

    // Using derivedStateOf for optimized filtering
    val filteredItems by remember(query) {
        derivedStateOf { items.filter { it.contains(query, ignoreCase = true) } }
    }

    Column {
        TextField(
            value = query,
            onValueChange = { query = it },
            label = { Text("Search") }
        )
        filteredItems.forEach { item ->
            Text(item)
        }
    }
}

Enter fullscreen mode Exit fullscreen mode

Using Lazy Composables Effectively

The LazyColumn and LazyRow components in Compose are handy, but they’re only as efficient as your use of them. Avoid nesting too many Lazy components, and use item scopes smartly to minimize view updates. Use the LazyListState to manage scroll positions and track updates efficiently.

Using the Profiler

One of the most reliable ways to ensure an optimized app is by measuring its performance. The Android Studio Profiler is like a diagnostic tool, showing you where bottlenecks are in your app. Keep an eye on the CPU and memory usage of composables, particularly the frequency of recomposition events. You can also consider using the 'Profile GPU Rendering' *or *'Profile HWUI Rendering' tool in your device's developer options settings screen, to view frame rendering times and ensure smooth animations. For Compose, a smooth operation is usually 60 frames per second or better.


Final Thoughts

Jetpack Compose offers plenty of ways to write clean, fast, and stunning apps. And just like a car needs regular maintenance, your app needs its optimizations to stay slick and speedy. In my experience, a few small adjustments can prevent a lot of user frustration and keep them coming back to your app. Keep building & write clean code :)

Top comments (0)