https://kotlinlang.org logo
#compose
Title
# compose
t

Tung97 Hl

01/17/2023, 11:10 AM
Hi guys I have trouble with jetpack compose recomposition. The detail in the thread. Please help 🥲
I have a code for the home screen and numpad component (in the attachments)
Anytime I click on a button, the value of button will be fired in stateflow viewModel.getNumber(). I expect that the Text element in center screen should be recomposed. It’s worked well. But I notice all buttons in the numpad is recomposed, too.
I have no idea why it happens and how to prevent it. Thanks in advance.
s

Stylianos Gakis

01/17/2023, 11:22 AM
buttons: List<String>
is marked as
unstable
, maybe consider checking this out https://medium.com/androiddevelopers/jetpack-compose-stability-explained-79c10db270c8 specifically in the tl;dr version, the point that reads: “Collection classes like List, Set and Map are always determined unstable as it is not guaranteed they are immutable. You can use Kotlinx immutable collections instead or annotate your classes as @Immutable or @Stable.” Then as an easy step to see if this is your issue, use https://github.com/Kotlin/kotlinx.collections.immutable and mark the first parameter of
NumPad
as
buttons: ImmutableList<String>
and check if that helps or not.
Right now as I see it, when HomeScreenContent gets a new
inputText
, its body recomposes, then at the bottom when
NumPad
is called, you run this
map + "Delete" + ... +
on every recomposition, and then when the
NumPad
composable is run, it can’t infer if that
List<String>
is the same as it was before, so it recomposes everything. Now I am not sure if what I am saying is not exactly the problem, and if it has something to do with the
LazyVerticalGrid
itself though and you need to provide a
key
to all the items, but I’d say probably no since it by default should use the index of the item as a key. So I’d start with the ImmutableList first.
t

Tung97 Hl

01/17/2023, 11:43 AM
@Stylianos Gakis sadly I tried both solutions but it does not work at all :(
j

Jakub Syty

01/17/2023, 11:48 AM
One question - is that an real issue? Does this impact performance in any visible way? Since recomposition tracking is not that easy this should be the first question to ask yourself - is that really a problem?
Another question - why use LazyGrid for layout which has only 12 items?
Lazy layouts are there for lists that are not fitting on the screen, the only savings are on items that are invisible (thus not rendering). So you have no benefits from Lazy layout and all the complexity of it
Try this out using Rows and Columns and then check the issue persists
t

Tung97 Hl

01/17/2023, 11:51 AM
I am developing an app for very weak device (only 1gb ram) so I want to know how to optimize into every piece
And I used lazy layout just for learning purposes. I will use row and column and check for this again. 🤪
j

Jakub Syty

01/17/2023, 11:55 AM
Well that doesn't really answer the question - this could (and probably is) just fine 60 fps on the very weak device, just try it with release and minimized build
s

Stylianos Gakis

01/17/2023, 11:59 AM
All this is good to consider, but I assumed here we were just discussing understanding how to minimize recompositions for the sake of learning. Could you show me how you did the
ImmutableList
change in code? I am fairly certain that should be enough to avoid recomposition there.
Did you pass it using
persistentListOf(*((1..9).map(Int::toString) + "Delete" + "0" + "Clear").toTypedArray()),
and changing the NumPad signature to
Copy code
@Composable
fun NumPad(
    buttons: ImmutableList<String>,
    onInputTextChange: (String) -> Unit,
)
?
j

Jakub Syty

01/17/2023, 12:06 PM
i do agree that investigating this might be really fun and informative 🙂
t

Tung97 Hl

01/17/2023, 3:35 PM
@Stylianos Gakis I change the code like this. Is this do the same with toTypedArray()?
@Jakub Syty yeah sure I pay attention to optimizing the release build, too. But for now, I just want to figure out the reason for this case 😁
and I have small question, what * inside
persistentListOf(*((1..9)
mean? @Stylianos Gakis
s

Stylianos Gakis

01/17/2023, 3:53 PM
Oh no, that was just to turn a
List
into an
Array
and then using the spread operator
*
to pass it to the varargs of
persistentListOf
. Read about it here https://kotlinlang.org/docs/functions.html#variable-number-of-arguments-varargs or google “Kotlin spread operator”. But yeah what you did should be correct, the NumPad() composable should just skip completely (can even inline
numPadButtons
, there isn’t really a reason for it to be a
val
as you have it there). Did you run this and did it still recompose unnecessarily? Also no need to ping people who are already in the thread, I get a notification for this regardless.
t

Tung97 Hl

01/18/2023, 3:35 AM
Yes, it still recomposes unnecessarily
Hi guys I figure out the root cause. It is
inputChar: ((String) -> Unit)? = null
. Because this callback refers to the function inside ViewModel, but the ViewModel is not stable, the compiler treats this lambda as not stable. So if I
remember
this lambda, the view will not be recomposed unnecessarily.
j

Jakub Syty

01/18/2023, 8:19 AM
i don't think the reason is that ViewModel in unstable, but that all lambdas are unstable by default (because how could these not be). Can you try to do
onInputTextChange = viewModel::setNumber
?
this way you are not recreating lambda every recomposition
t

Tung97 Hl

01/18/2023, 8:30 AM
I did it before but it doesn’t change anything. Numpad still is recomposed.
j

Jakub Syty

01/18/2023, 8:31 AM
you would have to pass is by reference to NumPad too
NumPad(parameters, onInputTextChange = onInputTextChange)
t

Tung97 Hl

01/18/2023, 8:45 AM
Thank you it works like a charm. When I remember the lambda or pass the lambda to a composable function. It always increases memory use when I stress-click on the Numpad. But now I pass the call back by reference, it’s not recomposed and does not increase memory use.
So the root cause is lambda is not stable. The buttons list was not affected in this case.
s

Stylianos Gakis

01/18/2023, 8:48 AM
If you change it back to List<String>, does it work without recomposing then?
j

Jakub Syty

01/18/2023, 8:48 AM
at some point i always tried to pass functions from view model as reference just to avoid issues like that
t

Tung97 Hl

01/18/2023, 8:51 AM
@Stylianos Gakis it works without recomposing
s

Stylianos Gakis

01/18/2023, 8:53 AM
As I said above, don’t ping people again if they’re already in the discussion, I will see this message regardless. You can simply write my name if you want to refer to me. and that’s interesting, I thought List is always marked as unstable, not sure I understand what’s going on in this case then.
j

Jakub Syty

01/18/2023, 8:56 AM
I think LazyLayout might not care about the list itself
s

Stylianos Gakis

01/18/2023, 8:56 AM
Do you mind sharing your ViewModel too? I’d like to test it myself too.
j

Jakub Syty

01/18/2023, 8:56 AM
since it relies on item position
t

Tung97 Hl

01/18/2023, 8:56 AM
ah sorry. I just want to reply but dont know how
about the view model, just it. I save the input number to the database (for learning purposes) and return a flow to view.
s

Stylianos Gakis

01/18/2023, 9:09 AM
Just write your message, maybe adding the person’s name just without the ‘@’ before it. It’s not about LazyLayout, the NumPad function, if taking an unstable parameter, should always recompose if the scope of HomeScreenContent is also recomposing, which it should since its parameter inputText changes. It shouldn’t skip it since it can’t guarantee that
numbers
is stable if it’s a
List
. Just tested it myself in a compose for desktop project and you are right that passing the VM functions in a lambda was forcing recomposition here, since it was capturing ViewModel which is in fact unstable, however when I fix that and use lambda references to the functions, and I turn it into
List<String>
it also recomposes due to what I said above, so I had to pass VM functions by reference and also use ImmutableList, are you sure you tested without the ImmutableList and it still did not recompose?
t

Tung97 Hl

01/18/2023, 10:45 AM
Yes. I tested it so many times and it still did not recompose. I think in my case I used the default parameter for Numpad so the compiler treats the list as stable (?!)
j

Jakub Syty

01/18/2023, 10:48 AM
Yea, compiler does this optimization
t

Tung97 Hl

01/18/2023, 10:48 AM
Yeah when I do not use the button list default parameter for NumPad it will be recomposed
j

Jakub Syty

01/18/2023, 10:49 AM
i mean - compose compiler treats default value as non changed if nothing new is provided
it's really complex compiler plugin
pure magic
t

Tung97 Hl

01/18/2023, 10:50 AM
I have learned many things here thank you guys for your support 🫡
j

Jakub Syty

01/18/2023, 10:50 AM
If you like learning about internals then i highly recommend https://jorgecastillo.dev/book/
it's really great 🙂
t

Tung97 Hl

01/18/2023, 10:51 AM
thanks 😇
z

Zach Klippenstein (he/him) [MOD]

01/18/2023, 4:30 PM
Just wanted to chime in on an earlier comment - simply using lazy lists instead of their eager counterparts is not “optimization”. Lazy lists are not free, they actually have some overhead for the extra work they do to support all the lazy features. That overhead is only worth it if it is less than the cost of eagerly composing a large list. But for small lists, where nearly everything is likely to be on the screen at once, it can be more expensive than just using row or column.
t

Tung97 Hl

01/19/2023, 3:57 AM
Thank you. I got this point. I just wanna use it for learning purposes. In a real project, I will use rows and columns for this case.