package com.suno.android.common_ui.components.bottom_sheet import androidx.activity.compose.BackHandler import androidx.compose.animation.Crossfade import androidx.compose.foundation.background import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.material3.BottomSheetDefaults import androidx.compose.material3.Button import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.ModalBottomSheetProperties import androidx.compose.material3.SheetState import androidx.compose.material3.SheetValue import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberUpdatedState import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.suno.android.common_ui.components.preview.SunoPreview import kotlin.random.Random /** * A stateful bottom sheet component that handles showing/hiding and content updates based on state changes. * * @param state The current state that should be displayed in the bottom sheet * @param isShowing Lambda that determines if the sheet should be shown based on the state * @param contentKey Optional function to determine if content has changed and the sheet should hide and show again * @param onDismissRequest Callback when the sheet is dismissed * @param sheet Content composable for the bottom sheet that receives the current state */ @Composable fun StateBottomSheet( state: T, isShowing: (T) -> Boolean, onDismissRequest: () -> Unit, modifier: Modifier = Modifier, containerColor: Color = BottomSheetDefaults.ContainerColor, showHandle: (T) -> Boolean = { true }, contentKey: ((T) -> Any?)? = null, onBack: () -> Unit = onDismissRequest, sheet: @Composable ColumnScope.(T) -> Unit, ) { val updatedState by rememberUpdatedState(state) var animatedState by remember { mutableStateOf(state) } var isInitialConfirm by remember { mutableStateOf(false) } val density = LocalDensity.current val sheetState = remember(density) { SheetState( skipPartiallyExpanded = true, density = density, initialValue = if (isShowing(animatedState)) { SheetValue.Expanded } else { SheetValue.Hidden }, confirmValueChange = { newState -> val newStateIsShowing = newState != SheetValue.Hidden val isStateTransitioning = updatedState != animatedState val didUserDismiss = !isInitialConfirm && !newStateIsShowing && isShowing(updatedState) && !isStateTransitioning if (didUserDismiss) { onDismissRequest() } val allowUpdateState = newStateIsShowing == isShowing(updatedState) isInitialConfirm = false allowUpdateState }, ) } LaunchedEffect(state) { val hasContentChanged = contentKey == null || contentKey(animatedState) != contentKey(state) val wasShowing = isShowing(animatedState) val willBeShowing = isShowing(state) if (wasShowing && hasContentChanged) { sheetState.hide() } animatedState = state if (willBeShowing) { sheetState.show() } } if (isShowing(animatedState)) { LaunchedEffect(Unit) { isInitialConfirm = true } ModalBottomSheet( sheetState = sheetState, onDismissRequest = { }, containerColor = containerColor, dragHandle = if (showHandle(animatedState)) { { BottomSheetDefaults.DragHandle() } } else { null }, properties = ModalBottomSheetProperties( shouldDismissOnBackPress = false, ), modifier = modifier, ) { BackHandler( onBack = onBack, ) Crossfade(animatedState) { crossfadedState -> Column { sheet(crossfadedState) } } } } } private sealed interface PreviewState { data object Hidden : PreviewState data class State1( val num: Int, ) : PreviewState data class State2( val text: String, ) : PreviewState data object State3 : PreviewState } @Preview @Composable private fun StateBottomSheet_Preview() { var state by remember { mutableStateOf(PreviewState.State1(1)) } SunoPreview { StateBottomSheet( state = state, isShowing = { it != PreviewState.Hidden }, onDismissRequest = { state = PreviewState.Hidden }, onBack = { state = PreviewState.Hidden }, contentKey = { when (it) { PreviewState.Hidden -> 1 is PreviewState.State1 -> 2 is PreviewState.State2 -> 2 PreviewState.State3 -> 3 } }, ) { currentState -> when (currentState) { PreviewState.Hidden -> Unit is PreviewState.State1 -> Text( text = currentState.num.toString(), ) is PreviewState.State2 -> Text( text = currentState.text, ) is PreviewState.State3 -> Spacer( Modifier .fillMaxWidth() .height(20.dp) .background(Color.Green), ) } Button( onClick = { state = when (currentState) { PreviewState.Hidden -> PreviewState.State1(2) is PreviewState.State1 -> PreviewState.State2("text") is PreviewState.State2 -> PreviewState.State3 is PreviewState.State3 -> PreviewState.State1(Random.nextInt(100)) } }, ) { Text("next") } } } }