franztesca
03/27/2023, 2:33 PMScaffoldState
instead of using a unidirectional data flow.
Is it done for convenience (encapsulating part of state management)? Doesn't this break idempotency of the Scaffold
function? Is this a general good practice for custom composable functions?
🙏CLOVIS
03/27/2023, 2:39 PMScaffoldState
really mutable? Looking at its source, I only see https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:compose/[…]arHost.kt%20class:androidx.compose.material.SnackbarHostState which is encapsulated in a MutableState, so tracked by composeCasey Brooks
03/27/2023, 2:49 PMMutableState
rather than normal properties. This makes it so that those properties can be changed by the UI widgets, and it’s not much different from you defining your own mutableStateOf()
to make your UI work.
In general, no this is not something you should reach for first. It’s a case where the Compose/Material designers made the intentional decision that you, as the consumer of a particular component, don’t need/shouldn’t have precise control over the widget like you might with the enabled
property of a button. They chose not make the property “internally mutable” so you don’t need to worry about it and mess with all the extra boilerplate.
Based on how Compose and States work, it’s not technically breaking the UDF pattern, but it does add some nuance to it for the sake of ease-of-use. For your own code, the official recommendation is to lift state as you would expect, not to hide properties like many of the Material widgets do.franztesca
03/27/2023, 3:19 PMScaffoldState
is mutable. The fact that it is observable through the Snapshot system and thus it works with compose it's clear. I was asking more of a conceptual question and trying to realize best practices and how much to follow them.
In our project we have fairly complex UI and composable functions, so we could write some components that encapsulate their own logic and state, while still interacting with the external world through a mutable (observable) state, just like ScaffoldState
or rememberScrollState
.
On one side, practically speaking, following the scaffold approach creates simpler implementation and code that can be resused more easily (without the ceremony of lifting the state every time), on the other side it doesn't seem to follow stricly the state lifting, and idempotent composable function approach, which is promoted by the official documentation. So there is this choice to make in our own code. We are trying to figure out the actual best practiceCasey Brooks
03/27/2023, 3:34 PM@Composable
fun SomeComplexFunction() {
var stringValue by remember { mutableStateOf("") }
var intValue by remember { mutableStateOf(0) }
var booleanValue by remember { mutableStateOf(false) }
SomeComplexFunction(
stringValue = stringValue,
intValue = intValue,
booleanValue = booleanValue,
updateStringValue = { stringValue = it },
updateIntValue = { intValue = it },
updateBooleanValue = { booleanValue = it },
)
}
@Composable
fun SomeComplexFunction(
stringValue: String,
intValue: Int,
booleanValue: Boolean,
updateStringValue: (String) -> Unit,
updateIntValue: (Int) -> Unit,
updateBooleanValue: (Boolean) -> Unit,
) {
// TODO()
}
CLOVIS
03/27/2023, 7:27 PMStylianos Gakis
03/27/2023, 8:16 PMIt’s not recommended for regular UI design components, however.Regarding regular UI components actually, I encountered this case recently, where I opted to use such a
State
object, and I don’t know if I am gonna regret it or not.
I had a design system component, which basically let’s say takes in a string, and exposes a lambda to update it, it basically contains a TextField, and some more things. Now, I was hoisting this information all the way up to the ViewModel for a screen, since I had to have access to it there, for some reasons.
Then there were some other screens that were using this exact same design component, and again, had the requirement where this state was hoisted to the ViewModel. This wiring started to become quite cumbersome, moving it from the Viewmodel’s UiState, to the navigation-destination level composable, then to the screen-level composable, and then often to child composables too. I was doing this for 4+ screens at this point.
I then thought let’s try doing what Scaffold and all these …State
objects do, and had this design component take this one object in, which follows these guidelines like being @Stable and if it contains any mutability it’s all backed by snapshot state. Then the ViewModel also contains inside its exposed UiState, an object of that exact same type. Then passing it down to the child composable is as simple as passing this 1 object (or inside the screen-level UiState). And on the ViewModel, when I need to access it to do something it’s as simple as getting it out of that containing object.
It has been quite a simplification on the code there, but I do wonder if I will go overboard with this approach.Casey Brooks
03/27/2023, 10:03 PMStylianos Gakis
03/27/2023, 10:22 PM…State
objects, just like the ScaffoldState or DatePickerState
from material3, usually contain a way to mutate that state by themselves, shouldn’t be hard to figure that out as I see it.
And this was actually one of the reasons I begun to try this out in the first place. I had a DatePickerState which I wanted to hoist all the way up to the ViewModel, and it was the only way to read and write the date picker state using the material3 component. It received a DatePickerState parameter, which it internally mutated as you select a new date and whatnot, doesn’t give back a callback for when the state is updated. So I kinda had to take this approach there. And this worked well so I tried it out with my own components too. But again, not 100% confident in this technique atm, I might have different opinions in the future.Ben Trengrove [G]
03/27/2023, 10:40 PMStylianos Gakis
03/27/2023, 10:56 PMfranztesca
03/27/2023, 11:06 PM...State
parameter pattern.
To sum up, it's fine (and desirable) to create this state holder helper classes to simplify our composable functions.
Even if this breaks the conceptual idempotency of the composable function, we don't care much because we know that our state holder is using the snapshot system anyway (aka the composable is aware of the state changes) and we help the compiler trust us by using the @Stable annotation?Ben Trengrove [G]
03/27/2023, 11:06 PMBen Trengrove [G]
03/27/2023, 11:07 PMStylianos Gakis
03/27/2023, 11:12 PMBen Trengrove [G]
03/27/2023, 11:51 PMStylianos Gakis
03/28/2023, 12:39 AMI would think the reason you are needing it in the viewmodel is that you have to react to a date being selected and then kicking off some longer running eventThe reason I want it in the ViewModel is that I use the date state to • I take this date selected, and if it is not null is is passed down so that one of the UI elements will show the currently selected date (not the Dialog in this case, but think like a row item which shows what you’ve selected when you used the dialog). • If there is a date selected, I want to show more fields (it’s a screen that works kinda like a form to entry stuff), so this means that the UiState needs to change to reflect that these new items should show on the screen • I also want to use this at the end of entering everything, to perform a network request, and yes this one I understand that I could have the local date picker state in the composable, and on the button click take that current local state and pass it to the lambda which would eventually call the VM. But I am mostly interested in the first two points. So then we go to
Hoisting it to the viewmodel could work as well, the one thing I wouldn’t do would be to expose it as part of some overall UI stateAnd I wonder, would it then have to be part of another StateFlow that the ViewModel is exposing, so you’d have both of these as parts of the VM’s API? And if yes, what happens if it’s existence relies on the other UiState being in a specific situation? Think like:
sealed interface SomeUiState {
object Loading: SomeUiState
data class Content(foo: Foo): SomeUiState
}
And the date picker state only is applicable when the SomeUiState
is Content
but not otherwise. And instead of a simple loading/content situation think of a more complex UI where it has multiple distinct “States” that the entire UI is in which would render quite dinstinct UIs depending on it. This would also be super inconvenient as you’re polluting the public VM API with something that is not always applicable.
If on the other hand it is part of the Content, like let’s say
sealed interface SomeUiState {
object Loading: SomeUiState
data class Content(foo: Foo, datePickerState: DatePickerState): SomeUiState
}
Then you get it only when it’s applicable. With the downside that then you got a StateFlow<SomeUiState>
which internally holds the DatePickerState
which is also mutable internally, meaning that the StateFlow internal item may be mutated without the flow re-emitting. Which well, isn’t a problem (I guess?) when consuming this from compose, but only if you are compose-only. Which in my case is the case anyway.
Would love to hear more of your thoughts around this!Ben Trengrove [G]
03/28/2023, 1:07 AM