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

Roberto Fuentes
adidoescode
Published in
12 min readJun 9, 2023

Table of Contents

Content

  1. Recomposition: Brief explanation
  2. Simple Clickable example use case
    Composable function returning value
  3. Composable parent scope
  4. Unstable
    List is immutable, I wish it were
    Lambda is stable, well sometimes not
    Difference between Function Ref and Lambda
  5. 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.
Picture of Ross Sneddon in Unsplash

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:

Simple composable function with a clickable Text and a Text that will be moved.

We have a parent composable MyScreen that holds the offset declaration and 2 texts.

  1. Dummy offset text”: Reads the offset
  2. Click me” increments the offset`

Easy as it gets. Let’s click on the second Text

When you click on "Click meText composable you’ll update the offset by increasing it and the “Dummy offset textText 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:

Previous image but now with 2 additional Texts.

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:

Summary:

  • Read is done in a function scope and this invalidates the parent scope — Child recomposes because their input has changed
  • clickable uses composed and that returns a new Modifier 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 Kotlin inline 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.g LazyRow, LazyColumn)
  • Simple layouts like Box, Column and Row have inline, 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 as List 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 as Function Referenceand 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)

  1. 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.
  2. More performance tips
  3. 6 Jetpack Compose Guidelines to improve your app
  4. Performance best practices
  5. 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: AskResearch 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.

--

--