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

Travis Griggs

02/29/2024, 12:55 AM
Are there some limitations on what should be used as keys for remember(...) ? I've been trying to remember on an argument that is an object that implements equals/hashCode. Luckily, the object happens to have a uuid as well. So if I use remember(myObject) { }, the inside of the closure does not run, even though the I have a log statement at the top of the composition that says the composition is running. BUT, if I change to access uuid as the remember() key, then things work.
Copy code
@Composable
fun foo(bar:Bar ... ) {
    // this shows up every time the composition reruns with the parent changing the bar fed to it
    "enteringComposition".logged()
    // this will show up on initial, but on subsequent recalls, will not fire
    var zork by remember(bar) { mutableStateOf(13).logged("simple") }
    // this fires every time that bar is different too
    var yak by remember(bar.uuid) { mutableStateOf(42).logged("derived") }
}
a

Alex Vanyo

02/29/2024, 1:02 AM
Does that object’s
equals
take into account the
uuid
as part of its
equals
?
t

Travis Griggs

02/29/2024, 1:07 AM
yup
a

Alex Vanyo

02/29/2024, 1:08 AM
I’m slightly confused by your snippet - it looks like
var zork by remember(bar.uuid)
and
var yak by remember(bar.uuid)
are the same
t

Travis Griggs

02/29/2024, 1:08 AM
edited/fixed. sorry about that
👍 1
a

Alex Vanyo

02/29/2024, 1:10 AM
Is
bar
a mutable object?
“simple” should be logged if the previous
bar
passed to
remember(bar)
is
!=
to the new
bar
passed to
remember(bar)
. But if the previous
bar
and the new
bar
are the same instance, because it is mutable, then the “previous”
bar
and the “new”
bar
are really the same
bar
and are equal, even if the state inside (
uuid
) has changed.
t

Travis Griggs

02/29/2024, 1:25 AM
the two bars are definitely !=. and they are not === either
bar is a stateful object that has mutating state embedded in it
(bar's equal method)
Copy code
override fun equals(other: Any?): Boolean {
    return (other is DeviceAbstract) && this.oid == other.oid && other.javaClass == this.javaClass
}
(oid is it's "uuid"; class check is because it's a polymorphic class heirarchy and oids can be the same between different leaf types)
In fact, it's simpler than all that. All I have to do is put this line in at the top of my function:
Copy code
var lastValue = remember(value) { value }
where value is my fungible object that I'm passing in. If I print lastValue, it will stick the first value ever used. If I change it to:
Copy code
var lastValue = remember(value.oid) { value }
then, it gets updated when value changes. So there must be some rules I'm supposed to use for "non primitive" things that get used in remember? I've glimpsed @Immutable and @Stable in some comment threads. Are they part of this puzzle? or something unrelated?
For grins, I marked my class as @Stable, and that seemed to fix it. My (guessed) lesson here is that remember( keys ) need to be stable which means they're either immutable collections, primitive types, or marked as @Stable. Hoping someone can clarify that. If so, I think that should be WAY added to the remember ( ) documentation about keys, which are just marked as Any.
(and I am curious if data classes are considered stable or not??)
b

Ben Trengrove [G]

02/29/2024, 2:54 AM
Can we see your Bar class? There is something weird going on here
remember keys don't have to be stable. Remember just calls equals on the object that is saved in the slot table from the last invocation, and the current object being passed in. If they are not the same, it runs the lambda again.
Do you happen to have a default value in the composable declaration? Like
fun foo(bar: Bar = Bar())
data classes, like all classes, are considered stable based on whats inside them. If you have a
var
parameter, its not considered stable
What version of the compiler are you using and do you have strong skipping enabled?
s

Stylianos Gakis

02/29/2024, 7:54 AM
All confusion here would have likely been solved already if we could look inside your Bar class, or better yet, you make a little reproducing project to look at 😊
s

shikasd

02/29/2024, 3:33 PM
if it behaves the way you describe, it sounds like a compiler/runtime bug if anything repro project would go a long way 🙂
t

Travis Griggs

02/29/2024, 5:51 PM
Probably time to wrap this thread up. Thanks for all the replies. My original question was meant to be short/brief. It was basically "are there limitations on the Any" type that is used in a remember(key). Because I have a case in my code base where it seems to matter. @Ben Trengrove [G]’s second comment answers that: not by any intent. It's just supposed to be equal. I am still somewhat fuzzy on the role of @Stable and @Immutable. Are they synonymous? Or two different things? Should I be annotating all of my model objects that will be consumed with @Composable functions with these? To answer a few of the questions: • Compiler versions:
*kotlinCompilerExtensionVersion* = *"1.5.8"
,*
id(*"org.jetbrains.kotlin.android"*) _version_ *"1.9.22"* _apply_ *false*
, Studio =
Hedgehog | 2023.1.1 Patch 2
• I do not know if I have strong skipping enabled. Where/what flag would that be? • The type I am indicating as my compose function argument looks like:
Copy code
abstract class ValveAbstract(override val oid: TwigID) : DeviceAbstract() {
    var lock by mutableStateOf(ValvePosition.Unknown)
    var position by mutableStateOf(ValvePosition.Unknown)
    override var regions by mutableStateOf(emptyList<MappedRegion>())
    ...
actual instances passed in are of one of two concrete subclasses. The superclass indicated there looks like:
Copy code
abstract class DeviceAbstract : Comparable<DeviceAbstract>, NameSaveable {
    override var name by mutableStateOf<String?>(null)
    ...
It was annotating ValveAbstract as @Stable that made it behave as expected. • The only default parameter on the function where this is occuring is the usual modifier. It looks like:
Copy code
@Composable
fun RunIntervalFooter(valve: ValveAbstract, plans: PlansRepo, modifier: Modifier = Modifier) { ... }
Since this didn't turn out to be a simple question with a simple answer, I'll attempt to extract and reduce to a repeatably easily shareable example. And come back with a new question about that if I can reproduce. Thanks for all of the comments and help.
s

shikasd

02/29/2024, 6:24 PM
Can you also share what uuid/oid is in your case?
t

Travis Griggs

02/29/2024, 6:26 PM
typealias TwigID = UInt
s

shikasd

02/29/2024, 7:20 PM
This is somewhat interesting, it might be caused by a compiler optimization, as you mentioned that old
bar
and new
bar
are
!=
on recompositions, so both remembers should trigger, but only the one with
bar.uuid
does. I don't think
@Stable
should have any effect on
remember
, it only matters for skippable functions and lambda memoization. (also stable and immutable are the same, just documentation difference). You could try disabling intrinsic remember optimization which optimizes remember calls with a compiler parameter
plugin:androidx.compose.compiler.plugins.kotlin:intrinsicRemember=false
See https://stackoverflow.com/questions/65545018/where-can-i-put-the-suppresskotlinversioncompatibilitycheck-flag on how to set them