Recomposition — Answers, tips and resources I wish I had when I started
Table of Contents
Content
- Recomposition: Brief explanation
- Simple Clickable example use case
– Composable function returning value - Composable parent scope
- Unstable
– List is immutable, I wish it were
– Lambda is stable, well sometimes not
– Difference between Function Ref and Lambda - Useful content
The article is not about “Do this and should solve that”.
The purpose of the article is to:
- Focus on understanding common problems.
- The reason behind these problems.
- Give you a good sense of Compose.
1 — Recomposition: Brief explanation
I want you to understand what is Recomposition. A small search on google leads us to the following explanation:
Recomposition is the process of calling your composable functions again when inputs change. This happens when the function’s inputs change. When Compose recomposes based on new inputs, it only calls the functions or lambdas that might have changed, and skips the rest
I want you to reread this:
- It only calls the functions or lambdas that might have changed and skips the rest. Functions or Lambdas.
Let’s go with the first example that will help us to understand how a composable behaves in a typical situation.
2 — Simple Clickable button use case
Let’s imagine that we have this basic composable with 2 texts, please take a minute to understand it:
We have a parent composable MyScreen
that holds the offset
declaration and 2 texts.
- “Dummy offset text”: Reads the
offset
- “Click me” increments the
offset`
Easy as it gets. Let’s click on the second Text
When you click on "Click me” Text
composable you’ll update the offset
by increasing it and the “Dummy offset text” Text
reading the offset
will be updated. The nearest composable parent scope will be invalidated MyScreen
.
(I’ll talk about parent scope in the next point)
This is right, but there’s one more thing being recomposed.
You may have guessed it! It’s the clickable Modifier.
We’re going to check this in the next section.
Clickable Modifier: Composable function returning value
Modifier.composed
is the one to blame that is found inside the clickable
Modifier.
Under the hood, clickable
is using composed
. Any Modifier that uses composed
will be damned to be recomposed if the parent scope gets invalidated or the Layout
itself.
composed
is an API that has a state inside it. So basically it’s a “Stateful Modifier” that returns a new Modifier
instance by reusing the values of the previous state. But why’s it like that?
This was done like this so each Layout
(Text, Row, Image or any other Layout) could have its state.
Think about it, if you were to reuse the Modifier in different layouts, do you want to reproduce the same ripple animation in each Layout when clicking on one of them?
Obviously no.
To verify the behaviour do this: Add 2 new Texts to the previous example:
There’s not any solution for this, Google Jetpack Compose team is working on a solution (Check the next youtube video from Leland Richardson) and I believe it’s not something we should worry about.
If there’s no solution, what was the purpose of this section?
That if we build or use something that internally uses composed
(or the composed
itself) this will be recomposed and now we know what’s going on rather than thinking “what am I doing wrong?” and lead to more confusion while trying to learn Recomposition.
To add a bit more, it doesn’t mean you shouldn’t use composed
, otherwise, you wouldn’t have a solution to create stateful Modifiers.
If you want to dive deeper into this topic and get more technical information I highly recommend you these resources:
- Compose Modifiers deep dive by Leland Richardson: https://youtu.be/BjGX2RftXsU
- Modifiers in compose by Jorge Castillo: https://jorgecastillo.dev/composed-modifiers-in-jetpack-compose
Summary:
- Read is done in a function scope and this invalidates the parent scope — Child recomposes because their input has changed
clickable
usescomposed
and that returns a newModifier
instance whenever the parent scope is invalidated.composed
will always be recomposed when the parent scope gets invalidated.
Note: There’s an issue tracker open related to this: https://issuetracker.google.com/issues/241154852
3 — Composable parent scope
In the previous example, I said:
And the nearest composable parent scope will be invalidated
If you’re new into compose or even Recomposition, you probably think that the parent scope is just defined by the open brace {...}
:fun MyScreen() { inside the MyScreen scope }
Well, I thought it was like that when I started with Compose. Let’s take a look again at the previous example.
Shouldn’t Column
be invalidated whenever Text
reads the offset
? Nope. There’s only a Kotlin keyword that discards Column
from being a parent scope.
The only difference between Column
and MyScreen
is the inline keyword. What does the inline do? In technical words:
All inline does is to copy — paste itself into the current scope caller. It doesn’t allocate any extra memory for that function
In simple words:
That means
Column
doesn’t behave like a normal function.
Compose doesn’t save the function in their memory stack as Kotlininline
keyword doesn’t first recognize it as a function.
Like if you wrote the code in place and didn’t use another function to separate it.
There’s no solution.
In the end, all the simple Layouts have this inline
keyword (Box
, Column
, Row
) and I guess that’s the purpose to use them as much as you need in your functions, so Compose doesn’t allocate more extra memory in the stack.
On the other hand layouts like LazyRow
doesn’t have inline
keyword.
This composable is probably aiming to have a lot of content inside that function, like a huge list of items. It’s better for this Layout to act as a parent scope, or you wouldn’t be only recomposing the LazyRow
but its parent, resulting in invalidating a bigger composable function.
Summary:
- A composable scope is one that doesn’t have
inline
keyword. (e.gLazyRow
,LazyColumn
) - Simple layouts like
Box
,Column
andRow
haveinline
, therefore they don’t act as a Composable scope.
Now that we know how Recomposition works and what makes a “composable function” be a scope for the child content, this will help us in the next point: Defer the read.
How do I track Recomposition
Before continuing with some examples, you may wonder how I know which ones are being recomposed, if your question is do I have any kind of magic power to see where Recomposition is happening? Nope but there’s something close to that
The developer tool it’s called Layout Inspector.
We’ll make use of this tool to help us debug the next examples.
4 — Unstable
This point gets a bit more technical, but I believe it’s needed.
Let’s remember what I pointed out at the start:
It only calls the functions or lambdas that might have changed and skips the rest
Until now, we’ve seen that — when a value changes, the Composable reading it gets recomposed and parent scope gets invalidated.
But there are some other cases — where a Composable is recomposed even though the value hasn’t changed. Hence — “might have changed”
Things like List<T>
or Lambdas
are causing Recomposition when the parent scope is invalidated. Let’s start with the most known one, List<T>
(Or any Collection
)
List is Immutable, I wish it were
Consuming a list that is coming from the ViewModel and a high-level composable down to the most low-level Composable, then making some change that is not related to the List
and still getting recomposed is probably one of the most non-understanding problems that I faced at the start.
Let’s add a new composable called MyList
inside the Column from the previous example. Notice that I’m reading offset
in MyScreen
scope on purpose to invalidate it.
What do you think it’ll happen when you click in Text
and update the offset
? Let’s see:
MyList
is recomposing even though we’re not even changing any value from the list and we even have a remember
, why would be recomposed?
Looks like is a minor problem but definitely shouldn’t be ignored if you have a huge UI Tree passing down the List and you start adding some animations there or any other frequent changing value.
First things first. Why is it happening?
Root problem
If we use the Compiler Metrics, it generates the next for CustomBox
and MyList
functions:
restartable skippable scheme("...") fun CustomBox(stable offset: Int)
restartable scheme("...") fun MyList(unstable list: List<String>)
CustomBox
is skippable if offset
hasn’t changed, but MyList
isn’t because it has a parameter that is unstable
. Now it starts to make sense.
If I were to invalidate the parent scope (MyScreen
) because of something else (let’s say some random variable that is in the scope but both MyList
and CustomBox
don’t use), then no matter what MyList
will be recomposed because we have a parameter that is unstable
and thus the function is not skippable
even though our List
hasn’t changed, while CustomBox
wouldn’t be recomposed if offset
hasn’t changed.
But there’s a reason behind it.
Quoting from Ben Trengrove article: Jetpack Compose Stability Explained:
“Unfortunately
Set
(as well asList
and other standard collection classes) are defined as interfaces in Kotlin, this means that the underlying implementation may still be mutable.”
So Compose can’t verify if a list is really Immutable
because we could still manipulate the content. So for this, Compose detects a Collection
and marks the parameter unstable
and thus the function is not skippable
.
Solution
We have 2 possible solutions:
- Use the ImmutableList library by Kotlin
- Create a custom class wrapper for the list and annotate it as
@Immutable
or@Stable
And now MyList
can be skipped when parent scope is invalidated
restartable skippable scheme("...") fun MyList(stable wrapper: ListWrapper)
TLDR:
- We can check if a Composable is skippable or not through Compiler Metrics. For more information, I highly recommend Chris Banes composable metrics article
- Compose can’t make sure that a list will or not be manipulated and Compose marks it as
unstable
.
A lambda is stable, well sometimes not
Last but not least.
This one is tricky to understand but really interesting at the same time.
A lambda
is stable by default in Compose, this is because the Compiler tries to apply remember
under the hood to the lambda. Otherwise, it would create a new lambda
instance each time.
That’s why remember
exists, to avoid creating a new value and recomposing functions using that value. Therefore there’s a small improvement under the hood for the lambdas by wrapping them in remember
.
Example of a stable lambda
But I said “Compose tries to apply remember
” — Sometimes a lambda
can be unstable
. What makes a lambda unstable
are the capturing values inside the lambda
scope being unstable
(i.e. ViewModel)
Example of unstable lambda
:
Because of ViewModel
being unstable
the lambda
turns to be unstable
as well, whenever the parent scope gets invalidated then the Composable (i.e.SomeRandomComposable
) that is taking an unstable lambda
as a parameter will be recomposed.
There’s an open issue tracker about it:
https://issuetracker.google.com/issues/229645237
Solutions:
There’s an amazing article that explains what’s happening under the hood with multiple solutions.
One I personally love using is the Function Reference
.
And from this part, it gets too technical. I couldn’t stop my curiosity.
For some unknown reason, this works even though we’re using the viewModel
, I tried to spot the difference between a lambda
and a Function Reference
under the hood, they’re almost the same!
(Spoiler: I couldn’t find the difference)
After digging into my investigation I saw a thread on Twitter by Andrei Shikov talking about it in Compose. (I recommend reading it)
Andrei is one of the Google Developers responsible for the Compose UI Toolkit.
I told him my conclusions and asked if they were correct (Another Spoiler: They weren’t, don’t use ChatGpt to assert anything — lesson learnt). But you know that getting any information hiding behind this magic it’s always worth it!!
So what comes now is a small discussion on Twitter with him answering my doubts (Thank you Andrei!)
Difference between Function Ref and Lambda — Conversation
Andrei Shikov said:
What makes Function refs special is Kotlin implementation detail.
Where Kotlin marks these asFunction Reference
and adds equals implementation, so function refs of the same method are equal from Compose perspective.
The problem is that sometimes this equality is incorrect, and we are forcing instance checks for function refs now
“function refs of the same method are equal from Compose perspective”
We could take this answer as the reason why Function Refs are not unstable — hence not recomposed. Returning true
from equals is always good 👍 for Compose to not recompose a function.
And this last sentence brought me another doubt:
“We are forcing instance checks for function refs now”
Will in the future Function References
stop being a solution?
So I asked:
So now — function refs will always be recomposed or only when `equals` is incorrect? And in what cases are not equals?
And he answered:
It will behave the same as lambdas — different instances will cause Recomposition
As for the cases when equals was incorrect — There was a bug related to function references being equal when captured value was not
“It will behave the same as Lambdas
”
Does it mean that unstable
values that use Function References
will be recomposed?
So I asked to be sure that I was getting it right:
Does it mean that in future functions refs will be recomposed when the capturing values are unstable (i.e. ViewModel)?
Answer:
It still depends on the case, as sometimes compiler is able to optimize, but Function refs will be recomposed more often, yes
And that’s it 😉.
Conclusions
Are there any solutions?
Yes again, you can check this well-detailed article.😉
Is it worth to make them skippable?
Again, do you have any frequently-changing value in the same scope of the unstable lambda
? Or do you notice any performance issue?
Then that’s a yes.
If not then it’s up to you. I personally like using function refs because it makes the code way much more readable.
Useful content
What’s left from here? There are still plenty of things to learn.
I will share a few articles and videos that are sorted by personal preference. They made a difference for me after reading and watching them a few times. (And I still come back from time to time)
- Donut-hole skipping — This proves how powerful is Compose. Once you understand that only invalidates places that read the value instead of invalidating all the UI tree starting from the parent Composable.
- More performance tips
- 6 Jetpack Compose Guidelines to improve your app
- Performance best practices
- Promise compose compiler and imply when you’ll change
Note: Any referenced article in this blog is just as recommended as the ones in the aforementioned list
Any other tips?
Research — Practice — and if still don’t understand it: Ask — Research again and practice.
Any unsolvable questions?
Go to the #Compose channel in the Kotlin community and post them 😉
Thank you for reading! :)
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.