Reading this post on Reddit, and related stuff, ab...
# compose
r
Reading this post on Reddit, and related stuff, about destructuring declarations for Compose state: https://www.reddit.com/r/androiddev/comments/rqdv5g/there_are_three_ways_to_declare_a_mutablestate/. References: • Twitter thread where @Zach Klippenstein (he/him) [MOD] says "it's a footgun": https://twitter.com/zachklipp/status/1475913455857111040?s=20 • Same Twitter thread where @Leland Richardson [G] says "I think there might be a just-as-compelling argument that the other forms are actually more foot-gun-like than this?": https://twitter.com/Louis_CAD/status/1476180047476232192https://issuetracker.google.com/issues/212452424 Discuss 🙂 To start, I personally like assigning the value to a
val
rather than using a
var
and delegates. But I'm trying to understand the Redditor's examples. Why doesn't the Draggable example work (assuming it doesn't, I haven't tried it)? Their explanation:
When Jetpack invokes the detectDragGestures callback, it's the same lambda instance. That means we never get the new values of offsetX and offsetY.
But shouldn't
DraggableZone
and
Draggable
recompose when the offset changes, thus getting new values of
offsetX
and
offsetY
in
detectDragGestures
?
y
I'm not sure about all cases, but if you use the third form and hoist it up outside of the Composable, say into the viewModel it stops updating. While the first two keep working in that case.
r
That makes sense but in the Reddit example the values are hoisted into another Composable.
a
h
For destructuring declaration to be a foot gun, you have to be playing russian roulette with your foot. Kotlin values are final. Assuming that they might change in the same scope just because you call a lambda is fundamentally wrong. Also, state hoisting is fine and all but there is already
rememberUpdatedState
for lambdas which solves stale value problem. If you are passing a lambda to child composables, especially for state manipulation, be sure to use that.
z
I don't think hoisting has anything to do with it. The core issue is that the lambda passed to
pointerInput
is launched into a coroutine that lives as long as the modifier is applied with the same keys. The first time that modifier is created, the lambda is allocated and captures any properties read inside it as fields in the underlying lambda object. Every time that modifier is recomposed, the original lambda is still being used for the coroutine, and all the values it captured. In the case of the explicit
State
or the delegated one, the lambda captured the
State
object itself. The value inside the state can change over time, but because the lambda captured the state object itself, every time it asks it for the value it gets the latest one stored inside the state. And on subsequent compositions, the lambda doesn't need to be recreated to capture the state object again because it's the same state object. However, in the destructuring case, the lambda captures the initial actual value, not the state object holding it. So when new values are destructured in subsequent compositions, the lambda running in the coroutine is still the original lambda with the original values.
👍 2
👍🏻 1
r
Thanks for that great explanation. Yeah, the aha for me is to recognize that certain lambdas survive recompositions. Knowing that, the behavior of a destructured val is clear and straightforward, and the purpose of
rememberUpdatedState
becomes clear as well!
👍🏻 1
z
I wish it were more obvious which ones survive. In most cases suspending ones do, but I usually just end up looking at the source if the docs don't say. Would be cool if we could do some data flow analysis and figure out if the lambda ends up getting sent to an effect or remembered.
👍 1
r
Short of data flow analysis, a standardized annotation like
@SurvivesRecompose
would allow for developer observability, and for tooling like IDEs and linters to give appropriate warnings.
z
You wanna file a feature request? 🙂
r