Recomposition — Answers, tips and resources I wish I had when I started — Part 2
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
orDrawing
.
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.