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

Travis Griggs

09/29/2023, 4:38 PM
I feel like I've asked this question a couple of times. I'm still struggling with getting this pattern right. Imagine a tree of composables that looks like this:
Copy code
fun TopContainer(initial:Thing) {
    val thing by remember { mutableStateOf(initial) }
    Text(thing.description)
    ThingEditor(thing, onChange = { newThing -> thing = newThing })
}

fun ThingEditor(thing:Thing, onChange: (Thing) -> Unit) {
    Text("${thing.description.size}")
    Button(onClick = { onChange(thing.modified) }) { Text("Modify") }
}
even though the remembered thing in the TopContainer will be updated, ThingEditor will not recompose. Fair enough. What's not clear is what the "normal" way to solve this is: 1. I can modify ThingEditor to enclose thing in a closure access. Which is OK, but just starts making closures every where if states being hoisted a number of levels up. 2. Replicate the remembered slot in ThingEditor. Initialize it with the value, update it locally (which will cause recomposition of the Editor to happen) and additionally, broadcast the change upwards by triggering the onChange (via a LaunchedEffect or some other flowy thing) Which of these is the preferred pattern? Or is there a 3rd option. I just find myself frustrated when I'm coding along and things Just Work (I have lots of those with Compose which I'm thankful for) and then when it doesn't, I'm always flustered.
k

Kevin Worth

09/29/2023, 4:53 PM
Not to slow things down too much, but, am I understanding you correctly? With the example you’ve given, it doesn’t do what you want it to do? Why would
ThingEditor
not recompose? (why do you say “fair enough”?)
p

Pablichjenkov

09/29/2023, 4:53 PM
I would swear it would work. If you print a log right above the Text in ThingEditor, do you see the log printed? What version of compose is that? I have seen some issues with Text in compose 1.1.x, it was not updating content through fast recompositions. If that's the case, could you try with OutlinedTextField
t

Travis Griggs

09/29/2023, 4:54 PM
I free typed the above, trying to create a simple example from a more domain specific real code instance. I'll go make sure it even compiles, much less works.
🆗 1
sigh. my example works "as expected", so failed to be an example of the case I was seeing. I'll have to dig deeper to figure out how to replicate it. I just know that sometimes I ended up with hoisted single source of truth, and passing it down the tree, it will display initially, but when I bubble up changes, i don't get (re)composition at lower levels (even though I verify the change is bubbling up through the onChange layers). And I always then just slap either of the two listed solutions on it and "it works" and I'm always left confused why i needed that treatment
(working example)
Copy code
@Composable
fun TopContainer(initial: Offset) {
   var thing: Offset by remember { mutableStateOf(initial) }
   Column {
      Text(thing.toString())
      ThingEditor(thing, onChange = { newThing -> thing = newThing })
   }
}

@Composable
fun ThingEditor(thing: Offset, onChange: (Offset) -> Unit) {
   Column {
      Text("${thing.theta}")
      Button(onClick = { onChange(Offset(x = thing.y, y = thing.x)) }) { Text("Transpose") }
   }
}

val Offset.theta: Float get() = (atan2(y, x))

@Preview(showBackground = true)
@Composable
private fun Preview() {
   TopContainer(Offset(42f, 13f))
}
🆒 1
p

Pablichjenkov

09/29/2023, 5:19 PM
Usually things that causes not recomposition is identical state being sent as update or stale lambda captures. Your lambda captured a reference to a State that is not active anymore either because was left out in a recomposition or the original State change and you don't update the reference in the lambda. Things of that nature but loggers are the best friends in compose 😄
k

Kevin Worth

09/29/2023, 5:29 PM
@Travis Griggs another thing that might help us help you is some code examples of your 2 solutions…? I know I’m struggling to understand their descriptions…
m

myanmarking

10/02/2023, 10:36 AM
in the first example, why would ThingEditor recompose? You are using remember with mutableStateOf, only the initial value will update the state, isn’t it ?
k

Kevin Worth

10/02/2023, 11:42 AM
@myanmarking if changing
thing
didn’t cause
ThingEditor
to recompose, then the library simply wouldn’t work. The parent
TopContainer
says to
ThingEditor
, “here is some state, make sure you recompose each time it changes”. On the other hand, the reason for using
remember
is to say, “each time I (
TopContainer
) recompose, I’m going to remember that I need to use the most recently changed version of `thing`” (with the very express purpose of making sure I don’t keep reusing
initial
)“. That is, the
remember
doesn’t say “remember to always use
initial
“, it says “`remember` to use the latest value of
thing
(and initially set its value to
initial
)“.
t

Travis Griggs

10/02/2023, 3:14 PM
I like this explanation @Kevin Worth , thank you. I know some times I've been burned because while I might have some sort of obvservable, if I extract the current state from it and pass that to a subcomposable, then the subcomposable won't recompose. You have to "access" the the observable when setting up the composition for the linkage to happen. Your explanation on remember there made me wonder.... (and I don't have an editor with me atm to check for myself...) what would have happened if at the top of ThingEditor, had created a new remembered state at that level, basically duplicating/mirroring the remembered state from the parent composable?
m

myanmarking

10/02/2023, 3:19 PM
ok. So thing will be recompose with changes triggered by the onChange. Yes, makes sense. I thought you were expecting thing to recompose, because initial was passed with a new value.
t

Travis Griggs

10/02/2023, 3:23 PM
I sneaked away to a laptop to test this out, and I believe this better emulates the problems I was originally having. And it does some to be the nested remember:
Copy code
@Composable
fun TopContainer(initial: Offset) {
   var thing: Offset by remember { mutableStateOf(initial) }
   Column {
      Text(thing.toString())
      ThingEditor(thing, onChange = { newThing -> thing = newThing })
   }
}

@Composable
fun ThingEditor(thing: Offset, onChange: (Offset) -> Unit) {
   val x by remember { mutableStateOf(thing.x) }
   Column {
      Text("${thing.theta}")
      Slider(
         value = x,
         onValueChange = { x -> onChange(thing.copy(x = x)) },
         valueRange = 0f..100f,
         steps = 99
      )
      Button(onClick = { onChange(Offset(x = thing.y, y = x)) }) { Text("Transpose") }
   }
}

val Offset.theta: Float get() = (atan2(y, x))
So in this case, per Kevin's explanation, my theory/observation is that ThingEditor IS recomposing, but because x has been remembered from initial, and not updated with the original, it didn't rederive. Which is a strong hint I should have used derivedStateOf instead
z

Zach Klippenstein (he/him) [MOD]

10/02/2023, 5:28 PM
You don’t need derivedStateOf unless the derived value is very expensive to calculate or changes much less frequently than the dependencies. If you’re just accessing a property that changes every time anyway, it’s unnecessary
p

Pablichjenkov

10/02/2023, 5:35 PM
My advice is to always use remember with keys. If not, it sometimes does not update the value as you would expect. It is due to positional memoization.
z

Zach Klippenstein (he/him) [MOD]

10/02/2023, 5:42 PM
It’s dangerous to say “always” for stuff like this. You can almost always exchange remember-with-keys for updating state on recomposition, but there are complexity tradeoffs
p

Pablichjenkov

10/02/2023, 5:58 PM
Well, saying 'always' is dangerous in general. Just wanted to bring that up because I stumbled upon scenarios where the remembered value doesn't take the new update coming from input parameters but it keeps the previous composition value. I agree with using the state directly being cleaner and avoiding the above scenario. Although remember is handy sometimes right.
z

Zach Klippenstein (he/him) [MOD]

10/02/2023, 6:32 PM
For sure. It always depends 😜
🙂 1
k

Kevin Worth

10/02/2023, 8:41 PM
@Travis Griggs I agree that you don’t want
derivedStateOf
, so are you clear on “remember with keys”?