Recomposition — Answers, tips and resources I wish I had when I started — Part 2

Roberto Fuentes
adidoescode
Published in
6 min readSep 15, 2023
Picture of Kenny Eliason in Unplash

Reading “Recomposition — Answers, tips and resources” I wish I had when I started” is strongly advised if you wish to be able to follow the examples I am about to discuss in this article.

The next two items are the essential steps to ensure your UI stays away from being “janky” and are indispensable when working with animations.

  • Defer the “read” as long as possible
  • Skip composition phase

1- Defer the “read” as long as possible

Let’s take the previous example and let’s imagine that we want to pass the offset to another Composable function.
We’ll call it: CustomBox(offset: Float)

You might think that offset is only “read” in CustomBox scope, that’s not true at all. To provide a value through the parameter of a function, first the parent caller (i.e. MyScreen) must “read” the value.

To verify this, let’s check real quick how a Kotlin function works at Bytecode level (It’s exciting I promise! 😉)

You can see in the right panel that a variable is stored somewhere with the number ‘0’, from there in the next line we load that reference with ALOAD 0, therefore we could say that the variable is being “read” (Notice that “read” is in quotes, because this is not technically right). Later the function is invoked with the variable that has been “read”.

What’s the conclusion we can draw here?

We could safely assume that before passing a variable to a function, first needs to be “read”.

Looking again at the example, that function will invalidate MyScreen because we’re “reading” offset, so CustomBox will be invalidated because inputs have changed.

You might think now there’s no need to recompose MyScreen since it’s the parent caller and we’re just passing down the value. You’re half right… but I’ll explain it at the end of the article.

Let’s see how we can avoid recomposing MyScreen.
For this we can defer the “read” to the next function with 2 options:
State and Lambda

State

Instead of using the by keyword in the offset declaration, you’d use = to produce a State<T>.
val offset = remember { mutableStateOf(0) }

You would just pass the State<T> in the CustomBox which now has a State<Int> parameter:
fun CustomBox(offset: State<Int>)

Et voilá! now you are ready to “read” the variable in the CustomBox offset.value

In general, State<T> would work but it adds State keyword to the parameter and when you’re just going through the functions, it just creates too much visual noise.
Instead, we can use Lambda which solves this issue, and Google advocates favourably towards it 😉

Lambda

Google loves that you implement Lambda instead of State, according to the docs:

Preferring lambda-based modifiers when you pass in frequently-changing state.

With lambda, we could keep using the by keyword. You would just need to modify the parameter to receive a Lambda:
fun CustomBox(offsetProvider: () -> Int)

Now you’re ready to just recompose one Composable function instead of two.

Here’s the result!

Without deferring the “read”:

Deferring the “read”:

What if I told you that we could improve it?

Let’s see how can we skip Composition and Layout

2- Skip composition phase

There are 3 phases in Compose:
Composition -> Layout -> Drawing

If you don’t know what each phase does, please take only 1 minute to read each phase’s purpose.

Some of the values in our code are only useful in Layout or Drawing phases, and don’t add anything in Composition phase.
For instance the offset value we’ve been using in the previous example.
The use ofoffset is just to change the place of the Composable and that’s theLayout purpose (measurement and placement).

Notice that in the previous GIFs I’ve pointed out that we’re reading offset in a Composable function (i.e. CustomBox in the deferring “read” example) and that causes to invalidate that scope and recompose the whole function.

Instead of reading it in your Composable function before passing it to the function Modifier:
Modifier.offset(x = offset)
It’s way better to switch to the lambda Modifier:
Modifier.offset { IntOffset(x = offset, y = 0)}.

And you might relate similarities with the same previous strategy (Deferring “read”):
Modifier.offset {} accepts a lambda as a parameter and the “read” is done in the Layout phase.

We have just avoided having to go through Composition phase!

I highly recommend diving into that Modifier on your own to see where it’s “read” as if you haven’t done that yet… it’s your task now 😉!

Here’s what Layout Inspector shows after moving it to Layout

I’m pretty sure you will be animating the background at some point, for that we could skip Composition, Layout and directly go to the Drawing phase by using:
Modifier.drawBehind { drawRect(animatedColor) } or other draw available modifiers.
If you’re curious about what you can do in Drawing phase, then Modifier.graphicsLayer { // lot of cool stuff } is your friend 😉

Et voilà again!, you’re skipping 2 phases!

Conclusion

Deferring “read” enables us to:

  • Skip reading in Composable functions that don’t need the value
  • Skip to the right phase, Layout or Drawing.

Both actions improve the performance of the app and waste less battery.

If you’re still wondering the initial statement: “You might think now there’s no need to recompose MyScreen since it’s the parent caller and we’re just passing down the value. You’re half right… but I’ll explain it at the end of the article.
The following question provides the answer:

Should I defer “read” whenever I have the opportunity?

NO. Recomposition is not bad and it’s fine to let it recompose, don’t stress about it unless you know there’s a value that is going to change frequently (Animation, offset, etc..), then seek a solution.

If you notice a performance issue then you should analyse the values that are causing it and start deferring them in hopes of improving it. For example, some animation value that in 3 seconds causes 300 Recompositions (throwing some random data there) and you know that “read could be deferred to the specific function that needs it or to the Layout or Drawing phase.

If you still find your app to be “janky” then you’ve probably optimized it as much as you could in debug mode.
There’s nothing more to do, test your app in release mode since it will remove all the extra code that is hidden for debugging purposes.
That should highly improve the performance.

Thank you for reading until the end of the article! 😊

The views, thoughts, and opinions expressed in the text belong solely to the author, and do not represent the opinion, strategy or goals of the author’s employer, organization, committee or any other group or individual.

--

--