Lukasz Kalnik
08/29/2022, 5:12 PMStateFlow from the ViewModel which contains a boolean property showBottomSheet.
In my screen's composable, I can then remember (and reinitialize) the bottom sheet state like this:
val modalBottomSheetState = rememberModalBottomSheetState(if (screenState.showBottomSheet) Expanded else Hidden)
That takes care of hiding the bottom sheet.
But how do I keep the showBottomSheet state in the ViewModel synchronized with the composable, when e.g. the user swipes down the bottom sheet? For this I included a callback in the composable: onBottomSheetDismissed.
I called this callback in the confirmStateChange lambda parameter of `rememberModalBottomSheetState()`:
// Composable
val modalBottomSheetState = rememberModalBottomSheetState(
initialValue = if (screenState.showBottomSheet) Expanded else Hidden,
confirmStateChange = {
onBottomSheetDismissed()
true
}
)
// ViewModel
val screenState: StateFlow<ScreenState> = _screenState.asStateFlow()
private val _screenState: MutableStateFlow<ScreenState>
fun onBottomSheetDismissed() {
_screenState.update { it.copy(showBottomSheet = false) }
}
But it appears that this callback is called very early in the dragging process, and my showBottomSheet variable in the ViewModel gets reset to false as soon as the user even drags the bottom sheet one pixel down and the bottom sheet immediately disappears (see video in thread)
Is there a better way to handle opening/closing the bottom sheet from a viewmodel and keeping the state synchronized between the two?Oleksandr Balan
08/29/2022, 6:15 PMLukasz Kalnik
08/30/2022, 6:41 AMLukasz Kalnik
08/30/2022, 8:33 AMLaunchedEffect block from your code, but generally I still have the same problem - onVisibleChange() callback is called too soon (as soon as the user starts dragging the finger down). So the callback to the viewmodel gets called immediately => the viewmodel emits the updated state (to hide the bottomsheet) => the bottomsheet disappears even though the user didn't finish dragging the finger down (or even moved his finger up again because he changed his mind).Lukasz Kalnik
08/30/2022, 8:36 AMModalBottomSheetLayout into a FullScreenPopup, which gets the onDismiss callback. What is the reason for this?Oleksandr Balan
08/30/2022, 8:48 AMthe bottomsheet disappearsHmm 🤔 Maybe you are removing the modal sheet from the composition when
showBottomSheet flag is set to false? Could you share a code where ModalBottomSheetLayout is used?
The popup is a “trick” how to render the ModalSheet above all other UI. See Motivation & Solution sections.… What is the reason for this?FullScreenPopup
Lukasz Kalnik
08/30/2022, 8:58 AMshowBottomSheet flag is false, yes:
val modalBottomSheetState = rememberModalBottomSheetState(
initialValue = if (systemScreenState.showSensorSettings) Expanded else Hidden,
)Oleksandr Balan
08/30/2022, 9:06 AMHidden and make it be driven with the LaunchedEffect and it’s .show() / .hide() ?Lukasz Kalnik
08/30/2022, 10:26 AMinitialValue being always hidden, but it didn't work as well. The problem is simply the confirmStateChange callback is coming before the state change has been triggered, so the viewmodel state updates (hence emits) too early.Lukasz Kalnik
08/30/2022, 10:26 AMDisposableEffect, which is kind of hacky, but works perfectly.
The fix is based on https://stackoverflow.com/a/69052933/4990683Lukasz Kalnik
08/30/2022, 10:27 AMif (modalBottomSheetState.currentValue != ModalBottomSheetValue.Hidden) {
DisposableEffect(Unit) {
onDispose {
onBottomSheetDismissed()
}
}
}Lukasz Kalnik
08/30/2022, 10:29 AMDisposableEffect is added to the composition once the bottomsheet is shown. But once the bottomsheet is hidden, the DisposableEffect is removed from the composition, which triggers its onDispose method. In this way my viewmodel state is updated after the bottom sheet got dismissed.Lukasz Kalnik
08/30/2022, 10:29 AMLaunchedEffect.Oleksandr Balan
08/30/2022, 11:36 AMvisible state and ModalBottomSheetState inside one Composable I get the behaviour you are describing 🤔
But when I extract the visible state to the outer Composable it suddenly works as I would expect 🤔
@Composable
private fun Demo() {
var visible by remember { mutableStateOf(false) }
DemoContent(
visible = visible,
onVisibleChange = { visible = it }
)
}
@Composable
fun DemoContent(
visible: Boolean,
onVisibleChange: (Boolean) -> Unit,
) {
val sheetState = rememberModalBottomSheetState(
skipHalfExpanded = true,
initialValue = ModalBottomSheetValue.Hidden,
confirmStateChange = {
onVisibleChange(it == ModalBottomSheetValue.Expanded)
true
}
)
LaunchedEffect(visible) {
if (visible) {
sheetState.show()
} else {
sheetState.hide()
}
}
ModalBottomSheetLayout(
sheetState = sheetState,
sheetContent = {
Box(
modifier = Modifier
.height(500.dp)
.fillMaxWidth()
.background(Color.Cyan)
)
}
) {
Box(
contentAlignment = Alignment.Center,
modifier = Modifier.fillMaxSize()
) {
Button(onClick = { onVisibleChange(true) }) {
Text("Open")
}
}
}
}Lukasz Kalnik
08/30/2022, 11:37 AMLukasz Kalnik
08/30/2022, 11:38 AMDisposableEffect now is just a temporary solution.Lukasz Kalnik
08/30/2022, 11:39 AMOleksandr Balan
08/30/2022, 11:41 AMdeferred readYea, I think it is due to changing the value right in the
confirmStateChange lambda 🤔Lukasz Kalnik
08/30/2022, 11:41 AM