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

galex

08/03/2020, 7:00 AM
Does it makes sense to pass a
mutableStateOf<User>
to the children as parameter so some
Composables
in the hierarchy can update that state so that all the hierarchy will be recomposed? In this case, I have a screen/composable that will sign-in the user, and when the user is signed in I’d like to update the rest of the hierarchy that it happened. The goal is to show all my screens if a user signed-in or not (I’m using
compose-router
for the backstack)
j

jim

08/03/2020, 1:24 PM
Yes, this makes sense, and is a perfectly reasonable design. And it works for more than just a single object; you can build a tree/graph of objects with
mutableStateOf
for each edge of the graph. As the type of operations you want to perform start becoming more complex touching multiple objects in your object graph, you may want to consider a more event-based architecture like Flux (https://facebook.github.io/flux/docs/in-depth-overview/#:~:text=Flux%20is%20the%20application%20architecture,a%20lot%20of%20new%20code.). But just keep this in your back pocket. You don't necessarily need a heavy-weight solution yet (and might never need it). Your initial solution of passing down a`mutableStateOf<User>` is a perfectly reasonable start.
🔥 1
f

Fudge

08/03/2020, 1:26 PM
Isn't passing down mutable state against the concept of unidirectional data flow?
Since in that way the child can "pass state to the parent"
j

jim

08/03/2020, 1:33 PM
No - so long as the child isn't mutating during composition - the composable function isn't performing any modifications and isn't providing any data to the parent/caller, so it is still single-direction. The parent still owns the data. This is not meaningfully different from state hosting, where a mutable object is passed down, which acts as a container for the composable's internal state. Modifying the object directly is just avoiding the intermediate step of passing an event through a trivial datastore. Unidirectional data flow is more about preventing a parent from asking a child for information, and keeping a single source of truth as high up (close to the root) in the given tree as possible. In this case, there is a single
mutableStateOf<User>
near the root, and that is the single source passed down.
But I'm glad you're thinking about unidirectional data flow. It's a critically important concept and the distinction here is subtle.
b

brandonmcansh

08/03/2020, 1:35 PM
interesting and makes sense
f

Fudge

08/03/2020, 1:36 PM
This is not meaningfully different from state hosting, where a mutable object is passed down, which acts as a container for the composable's internal state.
Yes I think passing any kind of mutable object means you can break unidirectional data flow
I think you are saying, the parent doesn't use the
mutableState
, so it doesn't count as the child giving the parent state?
j

jim

08/03/2020, 1:42 PM
No, even if a parent used the object, it would still be safe. As a thought exercise, imagine you had an extension function on an object which emitted an event to a datastore, and updated the object. From a user's perspective, they called a function on the object, and the object was updated. Or imagine the update method is on the object and the object has a reference to the EventEmitter. Regardless, you're exposing the same API and how the event is routed (trivially or through a complex datastore) is just implementation detail.
f

Fudge

08/03/2020, 1:53 PM
are you referring to mutableStateOf() vs an event based architecture i.e. Flux? I don't mean that one is more safe than the other. I'm talking about passing mutable state in general. Example:
Copy code
@Composable fun Parent() {
    val text = state { "parent text" }
    Text(text.value)
    Child(text)
} 

@Composable fun Child(state: MutableState<String>) {
   Button(onClick = {state = "child text"}) { 
     Text("click")   
 }
}
In this example has the child not reached out and changed the text state of the parent?
j

jim

08/03/2020, 2:07 PM
Your example code is no less unidirectional than:
Copy code
@Composable fun Parent() {
    val ee = remember { EventEmitter() }
    val text = ee.store.getText()    
    Text(text.value)
    Child(text)
} 
@Composable fun Child(text: String, eventEmitter: EventEmitter) {
   Button(onClick = {eventEmitter.emit(new ButtonClicked())}) { 
     Text("click")   
 }
}
f

Fudge

08/03/2020, 2:08 PM
yes, that's the same thing
they are both bidirectional
I'm assuming
emit()
mutates
EventEmitter
in some way to cause
getText()
to return a different value
j

jim

08/03/2020, 2:11 PM
Where the EventEmitter is the dispatcher passing the event to the datastore. I agree they're the same, but that's my point. The second example is virtually the definition of Flux highlighting unidirectional data flow, with EventEmitter being the Flux dispatcher.
f

Fudge

08/03/2020, 2:13 PM
they are the same, they are both bidrectional, and I thought one of the points of compose was to have unidirectional data flow. Isn't that why TextField accepts an immutable TextFieldValue and an onChange callback instead of accepting a mutable
TextFieldValue
and be done with it?
Same thing with slider and such, why don't they accept mutable state?
j

jim

08/03/2020, 2:21 PM
No, we accept mutable hoisted state often. For example, take a look at
ScrollState
Mutability is not the issue here, although it's good to have immutable state whenever practical and it's harder to have multidirectional data flow if you're using immutable data. But the two concepts are distinct.
f

Fudge

08/03/2020, 2:23 PM
Why are textfield and slider different then?
Or more generally when would you accept a mutable state parameter vs an immutable state parameter with an onChange callback
j

jim

08/03/2020, 2:28 PM
There are a variety of reasons that are difficult to distill into a hard-and-fast rule, especially over chat (we will write a doc on this at some point). Basically, it boils down to a few factors. An incomplete list off the top of my head: • How often/likely is it that a user will want to intercept the value prevent/change the update. If likely, use immutable+onChange. If unlikely, use mutable state. • How complex is the data model. If simple, it's easy to rebuild with new values. If complex, mutable state may be more appropriate. • How costly is an allocation. If the update is happening once per keystroke, use immutable data. If an update is happening potentially many times per frame, those allocations start to be more of a consideration. The first factor there is by far the most important one though. There are probably others that I missed, but these are just the ones off the top of my head.
f

Fudge

08/03/2020, 2:30 PM
I see, thank you for that. With this in mind, how would you write actual multidirectional ("the bad") data flow in compose, or is it even possible?
j

jim

08/03/2020, 2:47 PM
Compose makes it more difficult, although never underestimate the ingenuity of the user. Also, unidirectional data flow is more of a holistic concept that requires considering your architecture as a whole. There are signs maybe you're not unidirectional: • If you find that a single action is causing you to write lambdas that update multiple objects • If you find that modifying one object causes other objects to be modified (Jing calls these cascading updates) • If you find that the same piece of data is stored in multiple places (not single-source-of-truth) • If you are "updating derived data" • If your composables update state that was passed in as a parameter. That is to say, the act of rendering a composable causes data to be mutated (also called a side-effect, your composables should be side-effect-free).
g

galex

08/03/2020, 5:14 PM
Thank you so much @jim, really interesting read! We’ll definitely need a lot of docs on the subject to share the best practices for those architectures around Compose!
2 Views