Blog Infos
Author
Published
Topics
Published

Generated using Midjourney

Part 2 of this blog post series can be accessed here.

Jetpack Compose is a modern UI toolkit for building native Android apps using Kotlin. It provides a declarative approach to building user interfaces, allowing developers to easily create and manage UI components. It’s fairly new but still looks very powerful and allows for a lot of flexibility when it comes to creating UI on Android.

Even though creating a basic UI in compose is no rocket science, it is important to have some basic understanding of how Compose framework works under the hood and what are some of the best practices we can adopt. Having this basic knowledge will empower us to create performant UIs and also be in a better position to debug in case something is not working or performing as we expected it to.

By the end of this two-part blog series, you will see the UI, with the exact same functionality, recomposing less than 99% of the time what it was doing originally.

Jetpack Compose is a modern UI toolkit for building native Android apps using Kotlin. It provides a declarative approach to building user interfaces, allowing developers to easily create and manage UI components. It's fairly new but still looks very powerful and allows for a lot of flexibility when it comes to creating UI on Android.

 

This part talks about
  1. Phases of Compose
  2. Remember Block
  3. Stable Parameters and Skippable Composables
Phases of Compose

Every composable (with a few exceptions) follows these 3 phases before it can be rendered on the screen:

If you are already aware of these phases, feel free to skip to the next section.

Source: https://developer.android.com/jetpack/compose/phases

Jetpack Compose is a modern UI toolkit for building native Android apps using Kotlin. It provides a declarative approach to building user interfaces, allowing developers to easily create and manage UI components. It's fairly new but still looks very powerful and allows for a lot of flexibility when it comes to creating UI on Android.

Job Offers

Job Offers

There are currently no vacancies.

OUR VIDEO RECOMMENDATION

, ,

Migrating to Jetpack Compose – an interop love story

Most of you are familiar with Jetpack Compose and its benefits. If you’re able to start anew and create a Compose-only app, you’re on the right track. But this talk might not be for you…
Watch Video

Migrating to Jetpack Compose - an interop love story

Simona Milanovic
Android DevRel Engineer for Jetpack Compose
Google

Migrating to Jetpack Compose - an interop love story

Simona Milanovic
Android DevRel Engin ...
Google

Migrating to Jetpack Compose - an interop love story

Simona Milanovic
Android DevRel Engineer f ...
Google

Jobs

The initial code for the UI shown above can be found here.

We will use logcat logs to understand the current behaviour and also to verify if our fixes worked as expected or not.

📌 Source code for every optimisation mentioned in the blog post is available in this repo. Each step has its folder (step0, step1, etc..). Step0 is the starting point

If you try and run step0 of this code and look at the logs, you will notice a lot of tasks being repeated as you scroll through the list. Our UI is definitely doing much more processing than necessary. Let’s see if we can reduce any of this

Jetpack Compose is a modern UI toolkit for building native Android apps using Kotlin. It provides a declarative approach to building user interfaces, allowing developers to easily create and manage UI components. It's fairly new but still looks very powerful and allows for a lot of flexibility when it comes to creating UI on Android.

📌 For ease of understanding, the logs in this example are tagged. You can filter for all relevant logs using “ComposePerformance”. Logs can further be filtered based on whether there is a calculation happening (ComposePerformance-ReAllocation) or a recomposition happening (ComposePerformance-Recomposition)

Introducing remember blocks

As we read in the first section, a composable can be composed many times based on state changes. This basically means all the code that we write inside a composable function can be executed multiple times. While this is important to keep the UI up-to-date, it also adds to the possibility of redundant code execution which was not required to execute every time any state changed.

To prevent blocks of code from being executed every time a composable is composed, we use remember block. Any code inside the remember block is executed only if the key parameter changes.

val data = remember (key = key1) {
    // these lines will only be executed when the value of key1 changes
    // irresepetive how many times the composable is composed.
}

Let’s see how can we use this remember block to prevent some unnecessary code execution and object creation.

If you filter logcat with ComposePerformance-ReAllocationtag, you will see all the allocations being done some of which can be prevented by placing them in the remember block:

Jetpack Compose is a modern UI toolkit for building native Android apps using Kotlin. It provides a declarative approach to building user interfaces, allowing developers to easily create and manage UI components. It's fairly new but still looks very powerful and allows for a lot of flexibility when it comes to creating UI on Android.

Firstly, inside the parent composable, we are generating a list of 100 strings to be shown in the list. This list should only be created once.

Before

Logger.d(
        message = "Recreating item list",
        filter = LogFilter.ReAllocation
    )
    val random = Random(10)
    val items = IntRange(0, 100).map {
        val randomNumber = random.nextInt().absoluteValue % 200
        Item(
            desc = "[$randomNumber] Item with index = $it",
            id = randomNumber
        )
    }

After

val items = remember {
        Logger.d(
            message = "Recreating item list",
            filter = LogFilter.ReAllocation
        )
        val random = Random(10)
        IntRange(0, 100).map {
            val randomNumber = random.nextInt().absoluteValue % 200
            Item(
                desc = "[$randomNumber] Item with index = $it",
                id = randomNumber
            )
        }.sorted()
    }

Similarly, we can refactor to include a few more code blocks within the remember block

Before

Logger.d(
        message = "Recalculating scroll progress",
        filter = LogFilter.ReAllocation
    )
    val scrollProgress = scrollState.value / (scrollState.maxValue * 1f)
    Logger.d(
        message = "Recalculating showScrollToTopButton",
        filter = LogFilter.ReAllocation
    )
    val showScrollToTopButton = scrollProgress > .5

After

val scrollProgress = remember(scrollState.value, scrollState.maxValue) {
        Logger.d(
            message = "Recalculating scroll progress",
            filter = LogFilter.ReAllocation
        )
        scrollState.value / (scrollState.maxValue * 1f)
    }
    val showScrollToTopButton = remember(scrollProgress) {
        Logger.d(
            message = "Recalculating showScrollToTopButton",
            filter = LogFilter.ReAllocation
        )
        scrollProgress > .5
    }

Another place where we could use remember to optimise a bit more would be inside the ScrollPositionIndicator component

Before

Logger.d(
        message = "Recalculating progressWidth",
        filter = LogFilter.ReAllocation
    )
    val progressWidth = maxWidth - (8.dp)

After

val progressWidth = remember(maxWidth) {
        Logger.d(
            message = "Recalculating progressWidth",
            filter = LogFilter.ReAllocation
        )
        maxWidth - (8.dp)
    }

this will ensure that progressWidth is only recalculated when the value of maxWidth changes.

Let’s try running now and verify the logs

https://gist.github.com/httpsvimeocom839639052

As you can see, the list is no longer being created again and again and neither is progressWidth being calculated. We are still doing scroll-related calculations because the scroll value is getting updated constantly.

The source code for this step can be accessed here.

Stable Parameters and Skippable Composable

While discussing phases of composable, we saw that compose runtime does certain optimisations and skips re-composing certain sections of the UI tree based on the state changes. If it can detect that for a particular composable, no state has changed, then it can skip its recomposition for much better performance.

While compose runtime is smart enough to do this, it needs certain conditions to be fulfilled before it can correctly decide whether to skip composition for a certain composable or not

  1. Parameters

When we pass parameters to a composable function, they could be of any type. More importantly, they could be immutable or mutable; stable or unstable. Let’s look at each of these terms

1.1 Immutable params — Read-only values. The only way a new state can be passed into these parameters is by creating a new instance of these types. This ensures compose runtime can figure out whether the value of this parameter has changed or not. A data class with only val properties is a good example.

1.2 Mutable params — Variables which allow both reading and writing. Since we can edit the values in these types of variables without having to create a new instance, compose runtime can never be sure if something has changed or not within these parameters. Data class with one or more var is a good example of this type.

1.3 Stable params — Variables which are mutable but notify the compose runtime of any state change so that it can keep track. MutableState is a good example of this type of parameter.

2. Skippable Composable

For a composable to be skippable the compose runtime should be able to figure that out. This basically boils down to all parameters of that composable being Immutable or Stable.

📌 Compose treats all collection classes (List, Map, etc.) as unstable params. The same applies to all data classes declared in a module with no compose compiler plugin as dependency.

To find out whether a composable is skippable or not, we can use compiler reports. For more details on how to use them, please read here. For more on Compose Stability, you may refer to this blog post.

Coming back to our example, if you notice the ItemList function, none of the values changes as we scroll through the list.

@Composable
private fun ItemList(
    modifier: Modifier = Modifier,
    items: List<Item>,
    scrollState: ScrollState = rememberScrollState()
)

Filtering through the logs, however, we see Recomposing ItemList being printed on every scroll change

https://gist.github.com/httpsvimeocom839635345

Let’s take a look at the output of compiler reports for this function.

restartable scheme("[androidx.compose.ui.UiComposable]") fun ItemList(
  stable modifier: Modifier? = @static Companion
  unstable items: List<Item>
  stable scrollState: ScrollState? = @dynamic rememberScrollState(0, $composer, 0, 0b0001)
)

We see one unstable param, the items list. Even though the list is of immutable type, compose treats it as an unstable parameter. To work around this, we shall wrap it in an immutable data class

// data class with only val types are immutable by default. 
// Had that not been the case, adding the @immutable or @stable annotations are
// another ways of letting the compose runtime know when it cannot itself 
// figure out.
@Immutable
data class ItemHolder(
    val items: List<Item>
)

Now the updated composable becomes

@Composable
private fun ItemList(
    modifier: Modifier = Modifier,
    itemHolder: ItemHolder,
    scrollState: ScrollState = rememberScrollState()
)

and the compiler reports for this function says

restartable skippable scheme("[androidx.compose.ui.UiComposable]") fun ItemList(
  stable modifier: Modifier? = @static Companion
  stable itemHolder: ItemHolder
  stable scrollState: ScrollState? = @dynamic rememberScrollState(0, $composer, 0, 0b0001)
)

Now all params are marked as stable. This in turn marks the function as skippable which basically means it can now be skipped if no change in state is detected. Let’s try running the code and look at the logs again.

https://gist.github.com/httpsvimeocom839635780

And we have got rid of that extra recomposition!

📌 Instead of a data class, You can use Kotlinx immutable collections too

The source code for this step can be accessed here.

This brings us to the end of the first blog in this two-part series. There are still some interesting optimisations we can do further improve our UI. Continue reading as we further optimise this UI in part 2 here.

As always, thanks for reading and do leave your feedback or queries in the comments section.

This article was previously published on proandroiddev.com

YOU MAY BE INTERESTED IN

YOU MAY BE INTERESTED IN

blog
It’s one of the common UX across apps to provide swipe to dismiss so…
READ MORE
blog
In this part of our series on introducing Jetpack Compose into an existing project,…
READ MORE
blog
In the world of Jetpack Compose, where designing reusable and customizable UI components is…
READ MORE
blog

How to animate BottomSheet content using Jetpack Compose

Early this year I started a new pet project for listening to random radio…
READ MORE

Leave a Reply

Your email address will not be published. Required fields are marked *

Fill out this field
Fill out this field
Please enter a valid email address.

Menu