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

BenjO

07/24/2020, 11:41 AM
Hey everyone, I’m trying to understand why a recomposition occurs. As a dumb example, let’s say I have a
Foobar
composable displaying
Foo
and
Bar
children composables.
Foobar
has two state variables.
Copy code
@Composable
fun Foobar(fooFlow: StateFlow<Long>, barFlow: StateFlow<Long>) {
    val foo by fooFlow.collectAsState()
    val bar by barFlow.collectAsState()

    Column {
        Foo(foo)
        Bar(bar)
    }
}

@Composable
fun Foo(value: Long) {
    Text("Foo $value")
}

@Composable
fun Bar(value: Long) {
    Text("Bar $value")
}
Every time either
fooFlow
or
barFlow
receives a new value, both
Foo
and
Bar
are recomposed (onCommit is triggered) . I was hoping that only the composable using the flow value would be refreshed. The only solution I see is to pass each StateFlow down to the children. I’m I correct ?
z

Zach Klippenstein (he/him) [MOD]

07/24/2020, 12:37 PM
As far as compose knows, the Foobar function is the one that is observing both flows, so as far as it's concerned that's the function that needs to be recomposed when they emit. Since Long is considered a "stable" type, I believe the invocation of Foo and Bar will be skipped if the value hasn't changed since the last composition. Zooming out though, this is a perfectly fine way to write this code, and keeping the state flow out of the children is probably the right choice - it reduces coupling and makes your child composables more reusable and testable. The recomposition of the parent shouldn't be a problem in most cases, why are you concerned?
b

BenjO

07/24/2020, 12:42 PM
Thanks for the answer.
I believe the invocation of Foo and Bar will be skipped if the value hasn’t changed since the last composition.
This is not what I’ve seen during my tests. (I also tried to use StructurallyEquals to be sure)
this is a perfectly fine way to write this code, and keeping the state flow out of the children is probably the right choice
I know !
why are you concerned?
I’m writing a dashboard screen displaying multiple sensors informations (speed, location, orientation). Each sensor is represented by a flow, and I don’t want to refresh the whole screen because one flow emits more frequently than another
z

Zach Klippenstein (he/him) [MOD]

07/24/2020, 12:45 PM
From everything I've ever read and watched about Compose, this is something the runtime should handle for you, and not be something app developers have to worry about 99% of the time. If the function calls aren't being skipped yet, that seems like a bug although maybe it just hasn't been implemented yet. Is your actual code using Longs or some other type?
2
b

BenjO

07/24/2020, 12:46 PM
At first I thought it was because I’m using data classes. Then I tried using primitive types (as you can see in my sample). I get the same behavior.
Maybe I’m wrong somewhere, but I tried multiple things. The only thing that works is when the state in down below the hierarchy
z

Zach Klippenstein (he/him) [MOD]

07/24/2020, 12:54 PM
In order for Compose to determine that it can skip a function call it needs to be confident the arguments haven't changed. Currently, in order for it to do that the parameter types must be considered "stable", which means that the type will not change without notifying Compose itself. A type is considered stable if it's annotated with
@Stable
or a related annotation (eg
@Immutable
, which assures the compiler that values of the type will never change at all), or (I thought) if it's a primitive type. The documentation on the Stable annotation has more information. For types that don't satisfy this requirement, the compiler plays it safe since it can't be sure that two values which return true for
equals
are actually identical. If you're using data classes, this means the functions calls would only be skipped if your data class is annotated with one of these annotations. If all your properties are Immutable, use
@Immutable
for example.
At any rate, I'm pretty sure this is something that Compose should be handling for you by the time the library hits 1.0, and so I would not spend a lot of time working on manual optimizations to achieve the same thing at this point, especially ones that introduce code smells.
b

BenjO

07/24/2020, 1:02 PM
😮 nice catch,
@Immutable
did the trick 👏 (on my personal project with my data classes)
🙌 1
From the documentation it says
Copy code
This is a stronger promise than `val` as it promises that the value will never change not only that values cannot be changed through a setter.
I think it was exactly what I was looking for. You’re also right not to try optimizing things too early before the 1.0
z

Zach Klippenstein (he/him) [MOD]

07/24/2020, 1:04 PM
Huh, I'm surprised it didn't work for primitives then.
b

BenjO

07/24/2020, 1:05 PM
If you’re curious, you can try my sample. You just need to emit at a different rate.
It would be interesting to see if I missed something
z

Zach Klippenstein (he/him) [MOD]

07/24/2020, 1:08 PM
I believe you, I'm just surprised, but I'm just reading the same docs you are 😅 @Leland Richardson [G] probably knows what's going on.
👍 1
l

Leland Richardson [G]

07/27/2020, 3:59 PM
Sorry I’m just now seeing this thread! @BenjO the original example you have, with Foo/Bar accepting Longs, those composables should definitely skip if the values are unchanged. Longs are considered Stable as they are primitive. In this conversation you’re saying that you saw different behavior? Can I ask how you checked to see if Foo/Bar were recomposing or not?
For some additional context here, since this thread is kind of bringing it up: 1. right now a composable function won’t skip unless all of the passed arguments are stable types. This ends up being a requirement that is really easily broken with very reasonable looking code. The requirement is unlikely to change, but our ability to infer a type as stable will likely improve over time. 2. Right now
data class Foo(val value: Int)
is not inferred as a stable type. Why? Well, the fact that
val value: Int
is a property that can’t change is only clear based on the declaration, but not on the public API that is produced. In terms of JVM ABI, it would be perfectly fine to change this class to be
class Foo { val value get() = Random.nextInt() }
. So the problem is that
val
just signals whether a property is assignable, it does not indicate whether or not its value will ever change. If we infer the latter, we need to make sure that the inferred result is true, even if the jar it was compiled against is replaced with a different one with a different implementation. 3. In order to do (2) properly, we need to change our analysis strategy to something that can be optimized away during full program analysis (such as R8). We have a plan for how to do this, but it is unlikely to be implemented before alpha, but will likely be a requirement for 1.0. 4. In the interim, unfortunately, it means that annotations like
@Immutable
and
@Stable
can have an outsized impact on the performance of otherwise very reasonable code.
b

BenjO

07/28/2020, 11:51 AM
Nice and detailled answer ! To check if Foo/Bar were recomposing, I added a log in the
onCommit()
effect of each composable. I double checked my tests and now I understand why I said that equal primitive parameters caused a recomposition. Here is the trick : At first I wrote
Foobar
,
Foo
and
Bar
as accepting generic types. Here no matter what type you passed in (primitive or not), Foo and Bar were both recomposed each time any value changes in Foobar. Then, to ask my question I quickly rewrote the test, removed the generic and replaced it with
Long
types. In this case, you were both right, recomposition only occurs for values that change. Here is a sample Activity illustrating both cases. I don’t know if it is an intended behavior, but I fell into the trap ^^
Regarding the additional context you provided, all four points totally make sense 👍
@Leland Richardson [G], I'm curious to know if the generic thing is an expected behavior
l

Leland Richardson [G]

07/30/2020, 2:11 PM
It is expected but not required. We have a strategy to get around this that is related to what I talked about above but we have not employed it yet
b

BenjO

07/30/2020, 3:31 PM
Ok, thanks 👍
2 Views