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

svl

05/19/2023, 9:30 AM
why does datepicker in material3 (and similar components) have state as parameter instead of state flowing down events up?
3
c

curioustechizen

05/19/2023, 9:40 AM
I'm not sure I understand the question. Having state as a parameter is what enabled state to flow down. Could you describe what API you would have expected?
s

svl

05/19/2023, 9:43 AM
from the documentation and codelabs I did, it was always said you should send immutable state down and event should fire up. I would probably do Component(state, onStateChange = { newState -> state = newState })
that's what I see most often
j

Jakub Syty

05/19/2023, 9:46 AM
I've wondered about that too. There is no clear "date selected" callback, we have to do a LaunchedEffect on the state to detect the changes. It's cumbersome
I get that the datepicker state is more complex - hence the interface - but it would be nice to have some helpers that covers 99% of usage
s

svl

05/19/2023, 9:47 AM
I don't have a problem with it being a seperate interface
the docs give this example
Copy code
val state = rememberDatePickerState(initialDisplayMode = DisplayMode.Input)
    DatePicker(state = state, modifier = Modifier.padding(16.dp))

    Text("Entered date timestamp: ${state.selectedDateMillis ?: "no input"}")
I would expect it to be Datepicker(state = state, onStateChange = { state = it }) or something similar
c

curioustechizen

05/19/2023, 10:14 AM
I get what you mean now. There are other components that have similar APIs. Some examples of components that I work with a lot are: 1.
Horizontal/VerticalPager
which accepts a
PagerState
parameter. You don't get a callback when a page in the pager is selected. Instead you need to observe properties on
PagerState
itself (like
currentPage
). You also use the same state to perform actions like scroll to a specific page. 2.
ModalBottomSheetLayout
which accepts a
ModalBottomSheetState
. You don't get callbacks when the bottom sheet is expanded/hidden. You observe properties for this. Again, you use the same state object if you want to perform actions like expand/collapse the sheet.
Personally I don't think this is contradicts the state flows down, events flow up pattern. It is an implementation of the state holder pattern. State still flows down (i.e, a calling composable provides the state) and events still flow up (i.e., the calling composable still can observe to events). I guess the specific implementation of how the events flow up is different from what you see in simple examples.
But now even I'm curious as to why the APIs did not offer event lambdas.
j

Jakub Syty

05/19/2023, 10:32 AM
I do agree that this approach is fine for Pager, since we don't really care about the moment of change, just the final state
But for date picker i often want to save the new date in viewModel, or in local storage
again - i can do that using LaunchedEffect, but still there is a tradeoff here
one lambda for "date change" would solve that for me 😄
s

Stylianos Gakis

05/19/2023, 10:43 AM
You could consider hoisting that state entirely to the ViewModel, like we often do with the state that drives TextField as suggested here. Then your ViewModel won’t need any callbacks for this, it will just know about it, right?
s

svl

05/19/2023, 11:05 AM
sure, but what about when you need to save it to local storage? how would you know when to do it if you don't have a specific submit button?
s

Stylianos Gakis

05/19/2023, 11:06 AM
Save what to persistent storage, the date selection before it’s used somewhere?
s

svl

05/19/2023, 11:09 AM
if you have a datepicker for example and you want to persist it as soon as state changes. hoisting to viewmodel only helps with saving state for ui
s

Stylianos Gakis

05/19/2023, 11:11 AM
Persist it where and why? That's what I'm focusing on now.
s

svl

05/19/2023, 11:12 AM
persist it in local storage for example
s

Stylianos Gakis

05/19/2023, 11:12 AM
That state has a .Saver implementation, so you can use that to save it to and from a bundle, so you can use rememberSaveable or savedstatehandle in your ViewModel
s

svl

05/19/2023, 11:14 AM
the point is that there is no event that flows up, that you could react to if you pass state as argument to composable
it just changes and the ui will change with it, but you aren't be able to react to it in any way
s

Stylianos Gakis

05/19/2023, 11:15 AM
Persist the transient date picking in local storage as the user is picking from a dialog? Why? Just trying to understand
it just changes and the ui will change with it, but you aren't be able to react to it in any way
You can by observing the state it exposes
s

svl

05/19/2023, 11:16 AM
it's not about the datepicker. it's about the general pattern of passing state as parameter. I was just wondering why the material3 components use it instead of state down events up
s

Stylianos Gakis

05/19/2023, 11:17 AM
We've done a full loop in this conversation
c

curioustechizen

05/19/2023, 11:47 AM
I think it is a valid question: When should you use callbacks for events versus a state holder?
l

Landry Norris

05/19/2023, 12:15 PM
I'd imagine you'd always want some way to react to the date selected outside of just updating other UI. Let's say I'm making a calendar app. When the user selects a date, I want to update the event's date in the database. This would be easiest if DatePicker took a state variable and an onDateChanged. It seems silly to have a date picker that only affects other UI without persistence.
s

Stylianos Gakis

05/19/2023, 12:17 PM
Yeah, but do you want to save to persistent storage on each date change? Usually this date picker is shown in a dialog, and after you’re done selecting, you press an OK button in order to confirm that choice. That’s when I’d save to persistent storage by taking what the current date picker state looks like. Are you sure you can’t do that and you want to persist each change as the user is going through the dates and is possibly selecting many dates in the process?
l

Landry Norris

05/19/2023, 12:18 PM
I guess the easiest way is to use the dialog and get the state in the confirmButton onClick, but that's an extra button press.
s

Stylianos Gakis

05/19/2023, 12:19 PM
How else does your user dismiss the dialog which shows the date picker?
o

Oleksandr Balan

05/19/2023, 12:43 PM
The new
BasicTextField2
also has only
state
argument without
onChange
lambda 🤔 Maybe it is new “this is the way” mandalorian 🤔 https://developer.android.com/reference/kotlin/androidx/compose/foundation/text2/package-summary
s

Stylianos Gakis

05/19/2023, 12:51 PM
And it exposes a
forEachTextValue
which allows you to do something per new state change. Which internally is literally just
textAsFlow().collectLatest(block)
where
textAsFlow()
is just
fun TextFieldState.textAsFlow(): Flow<TextFieldCharSequence> = snapshotFlow { text }
. So exactly what we discussed above here too, taking the state that the date picker exposes, and observe its changes. SnapshotFlow and flows in general give you this super clean api on how to do this.
c

curioustechizen

05/19/2023, 1:04 PM
@Stylianos Gakis I understand where you're coming from. However it still doesn't answer the question: how do we decide when to use event callbacks versus observable properties in the state object? I think we need an answer that is general and not specific to DatePicker.
s

Stylianos Gakis

05/19/2023, 1:07 PM
Yeah that I can agree with. Me included, I don’t think I can intuitively decide when to do that and when it’d be an overkill. With that said, it seems to me like when things start to become more involved is when you can go for such a state holding object. If you look at the internals of the date picker you’ll see it’s doing quite a lot of stuff. But of course “when it’s too complicated” isn’t a good enough description, and I’d love to get a better understanding for it too.
c

curioustechizen

05/19/2023, 1:07 PM
Also I don't agree with some of your conclusions above: 1. It is not always a good choice to hoist Compose-UI specific state to ViewModels. This is especially true if you are using multiplatform a Presenter layer but not Compose UI multiplatform. 2. Based on 1, there still ought to be a good way to know when a Date has been selected without the user clicking on a Save button. The DatePicker API already provides a way for this but it is more verbose than a simple event callback.
s

Stylianos Gakis

05/19/2023, 1:11 PM
The point of it not being easy to listen to see changes gets repeated a lot. Do you find that doing a snapshotFlow on the state and collecting those changes something that is too complicated vs a lambda?
Copy code
val fooState = rememberFooState(
  initialState,
  { newState ->
    useNewState(newState)
  }
)
vs
Copy code
val fooState = rememberFooState(initialState)
LaunchedEffect(Unit) {
  snapshotFlow { fooState.internalState }.collect { newState ->
    useNewState(newState)
  }
}
For #1, for a multiplatform presenter sure, that may get more potentially. But then you’d fall back to having another value up there, which you’d need to keep in sync. I wonder if you could also just still hoist it up there, and have it work as the state holder that it is, and have the other platforms still forward calls to inside it. What the DatePicker component does anyway is call functions to set and read the state, it’s not something more special than that. You may be able to do the same?
c

curioustechizen

05/19/2023, 1:19 PM
Do you find that doing a snapshotFlow on the state and collecting those changes something that is too complicated vs a lambda?
It is fine in isolation in one place but I fear it might become tedious and repetitive if you have several components all of which expect you to LaunchedEffect + snapshotFlow + collect every time you want to react to a simple change.
a

Alex Vanyo

05/19/2023, 5:16 PM
One thing that can happen is needing multiple sources of truth for when there would be asynchronicity when trying to have just a single source of truth. Let’s say you have a slider updating some user preference that is being saved to disk. You can try having a single source of truth (the disk) for the entire round trip: on changing the slider position, update the value on disk, wait for that update to propagate, and update the UI. However, that full round trip is asynchronous, and it might look janky or glitchy if the UI is lagging behind what the user is trying to do. Instead, you can sort of split out the source of truth: 1. The persisted value on disk, which you load to set the initial value display in the slider. 2. The “in-flight”, user-is-currently-editing state that will follow the user’s actions synchronously without glitchiness. But now you intentionally have created a separate source of truth that you need to reconcile with the persisted value: an “in-flight” state that you need to sync that back to the persisted value on disk. That’s where the
LaunchedEffect
comes in. And you could replace “on disk” with “over the network” and now the
LaunchedEffect
becomes even more important. The “awkwardness” raises important questions like “how often should I try to update the value?” “what happens if the update fails due to some I/O error?” “what happens if the value was updated somewhere else?” “what scope should the
LaunchedEffect
be launched in which impacts when an update is cancelled?” A lot of those considerations can depend on your exact use case. Maybe you can synchronize the value immediately always, or maybe you have an “OK” and “Cancel” to confirm a change
c

Christopher Mederos

06/26/2023, 7:28 AM
Thanks for the old thread just worked through this myself. Seemed like the best approach was to... • Put a selectedDate val in my viewModel • Wrap
DatePickerDialog
in my own composable and create a transient DatePickerState using rememberDatePickerState() • Pass an updateDate func from the viewModel as the onClick param for the `DatePickerDialog`'s confirmButton param
227 Views