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