Last active
December 30, 2023 12:46
-
-
Save kishan-vadoliya/4b4799693914ec5041aecd52c186d7ff to your computer and use it in GitHub Desktop.
Modal Bottom Sheet Layout Jetpack Compose
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Ref: https://medium.com/@emreuysall/manage-multiple-content-actions-and-states-modalbottomsheetlayout-jetpack-compose-ec4140b2a043 | |
// 1- Identify User Action | |
// Identify user actions | |
enum class BottomSheetAction { | |
INITIAL, // initial case before user action | |
HIDE, // user wants to hide bottom sheet | |
SHOW, // user wants to show bottom sheet | |
TIMED // user wants to see and hide bottom sheet for some time, and disables screen interaction | |
} | |
// 2- Categorize Types of Bottom Sheet | |
// Categorize bottom sheets | |
enum class BottomSheetType { | |
LIST, // ListBottomSheetContent composable | |
ERROR, // ErrorBottomSheetContent composable | |
REDIRECT, // RedirectBottomSheetContent composable | |
INITIAL // InitialBottomSheetContent composable | |
} | |
// 3- Content Data of Each Type | |
// Content classes and object for each of BottomSheetType | |
sealed class BottomSheetContent { | |
// ERROR type content data | |
data class ErrorSheetContent(val message: String) : BottomSheetContent() | |
// REDIRECT type content data | |
data class RedirectSheetContent(val message: String, val redirect: String) : BottomSheetContent() | |
// LIST type content data | |
data class ListSheetContent(val title: String, val data: List<String>) : BottomSheetContent() | |
// INITAL type content data | |
object InitialSheetContent : BottomSheetContent() | |
} | |
// 4- Hold Current States of Content, Action and Type | |
// Contains current states of user action, bottom sheet type and its content | |
// All of the values initialized to INITIAL state | |
data class ModalBottomSheetState( | |
val bottomSheetContent: BottomSheetContent = BottomSheetContent.InitialSheetContent, | |
val bottomSheetType: BottomSheetType = BottomSheetType.INITIAL, | |
val bottomSheetActionType: BottomSheetAction = BottomSheetAction.INITIAL | |
) | |
// 5- Update State in ViewModel | |
// ViewModel class for managing and containing ModalBottomSheetState | |
class MainViewModel : ViewModel() { | |
// Cover ModalBottomSheetState using StateFlow | |
// Using LiveData in here, is another option | |
private val _bottomSheetState: MutableStateFlow<ModalBottomSheetState> = | |
MutableStateFlow(ModalBottomSheetState()) | |
val bottomSheetState: StateFlow<ModalBottomSheetState> = _bottomSheetState.asStateFlow() | |
// updates ModalBottomSheetState using a coroutine | |
fun updateBottomSheetState( | |
bottomSheetContent: BottomSheetContent, | |
bottomSheetType: BottomSheetType, | |
bottomSheetActionType: BottomSheetAction | |
) { | |
viewModelScope.launch { | |
_bottomSheetState.update { | |
it.copy( | |
bottomSheetActionType = bottomSheetActionType, | |
bottomSheetContent = bottomSheetContent, | |
bottomSheetType = bottomSheetType | |
) | |
} | |
} | |
} | |
} | |
// 6- Dummy Data | |
val errorMessage = "An unexpected error !" | |
val successMessage = "Operation Succeed !" | |
val redirectMessage = "Next" | |
val listHeader = "This is a list" | |
val listContent = listOf( | |
"Compose", | |
"Xml", | |
"Dialog", | |
"Kotlin", | |
"inline", | |
"in / out", | |
"fun", | |
"class", | |
"enum class", | |
"Hello World" | |
) | |
// 7- Bottom Sheet Content Composable of Each Type | |
// Composable corresponding to LIST BottomSheetType | |
@Composable | |
fun ListBottomSheetContent(title: String, data: List<String>, modifier: Modifier = Modifier) { | |
Column( | |
modifier = modifier, | |
verticalArrangement = Arrangement.spacedBy(20.dp) | |
) { | |
Text( | |
text = title, | |
style = TextStyle(fontSize = 16.sp, color = Color.Red), | |
modifier = Modifier.align(Alignment.CenterHorizontally) | |
) | |
LazyColumn( | |
verticalArrangement = Arrangement.spacedBy(10.dp) | |
) { | |
items(data) { | |
Text( | |
text = it, | |
modifier = Modifier.fillMaxWidth(), | |
style = TextStyle(fontSize = 14.sp, color = Color.Blue) | |
) | |
Divider(color = Color.Gray, thickness = 1.dp) | |
} | |
} | |
} | |
} | |
// Composable corresponding to REDIRECT BottomSheetType | |
@Composable | |
fun RedirectBottomSheetContent(message: String, redirect: String, modifier: Modifier = Modifier) { | |
Row( | |
modifier = modifier, | |
verticalAlignment = Alignment.CenterVertically, | |
horizontalArrangement = Arrangement.SpaceBetween | |
) { | |
Text( | |
text = message, | |
style = TextStyle(fontSize = 14.sp, color = Color.Black) | |
) | |
Text( | |
text = redirect, | |
style = TextStyle(fontSize = 14.sp, color = Color.Black), | |
textDecoration = TextDecoration.Underline | |
) | |
} | |
} | |
// Composable corresponding to ERROR BottomSheetType | |
@Composable | |
fun ErrorBottomSheetContent(message: String, modifier: Modifier = Modifier) { | |
Row(modifier = modifier, verticalAlignment = Alignment.CenterVertically) { | |
Text( | |
text = message, | |
modifier = Modifier.fillMaxWidth(), | |
style = TextStyle(fontSize = 14.sp, color = Color.Black) | |
) | |
} | |
} | |
// Composable corresponding to INITIAL BottomSheetType | |
@Composable | |
fun InitialBottomSheetContent() { | |
Spacer( | |
modifier = Modifier | |
.width(1.dp) | |
.height(1.dp) | |
) | |
} | |
// 8- ModalBottomSheetLayout With Multiple Contents | |
// collect state changes for your data using viewmodel | |
val uiState by viewModel.bottomSheetState.collectAsState() | |
/* - create a value that remembers bottom sheet state | |
- set this configuration to ModalBottomSheetLayout | |
- in confirmStateChange it disables user interaction for | |
REDIRECT bottom sheet type and disallows to full expanded format | |
*/ | |
val bottomSheetState = rememberModalBottomSheetState( | |
initialValue = ModalBottomSheetValue.Hidden, | |
confirmStateChange = { | |
when (uiState.bottomSheetType) { | |
BottomSheetType.REDIRECT -> false | |
else -> it != ModalBottomSheetValue.Expanded | |
} | |
}, | |
skipHalfExpanded = false | |
) | |
// wrap show function in a coroutine | |
val showBottomSheet: () -> Unit = { | |
coroutineScope.launch { | |
bottomSheetState.show() | |
} | |
} | |
// wrap hide function in a coroutine | |
val hideBottomSheet: () -> Unit = { | |
coroutineScope.launch { | |
bottomSheetState.hide() | |
} | |
} | |
// wrap show and hide functions and create a delay between them | |
val timedNonCancellableShowHide: (millis: Long) -> Unit = { | |
coroutineScope.launch { | |
showBottomSheet() | |
delay(timeMillis = it) | |
hideBottomSheet() | |
} | |
} | |
//it will work when bottom sheet is visible | |
//if current format is not REDIRECT it will call hideBottomSheet() | |
BackHandler(bottomSheetState.isVisible) { | |
if (uiState.bottomSheetType != BottomSheetType.REDIRECT) { | |
hideBottomSheet() | |
} | |
} | |
// calls the content in it when action type changes in state | |
LaunchedEffect(uiState.bottomSheetActionType) { | |
when (uiState.bottomSheetActionType) { | |
BottomSheetAction.INITIAL -> {} | |
BottomSheetAction.HIDE -> hideBottomSheet() | |
BottomSheetAction.SHOW -> showBottomSheet() | |
BottomSheetAction.TIMED -> timedNonCancellableShowHide(3000) | |
} | |
} | |
// LaunchedEffect registers snapshotFlow for once | |
// snapShotFlow triggers content in it when currentValue of bottomSheetState changes | |
// if current value becomes hidden, it means user completed process and set bottom sheet to initial states | |
LaunchedEffect(Unit) { | |
snapshotFlow { bottomSheetState.currentValue }.collect { | |
if (it == ModalBottomSheetValue.Hidden) { | |
viewModel.updateBottomSheetState( | |
bottomSheetActionType = BottomSheetAction.INITIAL, | |
bottomSheetContent = BottomSheetContent.InitialSheetContent, | |
bottomSheetType = BottomSheetType.INITIAL | |
) | |
} | |
} | |
} | |
ModalBottomSheetLayout( | |
sheetContent = { | |
// check current BottomSheetType | |
// show user related composable with casting BottomSheetContent to related model | |
when (uiState.bottomSheetType) { | |
BottomSheetType.LIST -> { | |
val content = uiState.bottomSheetContent as BottomSheetContent.ListSheetContent | |
ListBottomSheetContent( | |
title = content.title, | |
data = content.data, | |
modifier = Modifier | |
.height(300.dp) | |
.fillMaxWidth() | |
.padding(20.dp) | |
) | |
} | |
BottomSheetType.ERROR -> { | |
val content = uiState.bottomSheetContent as BottomSheetContent.ErrorSheetContent | |
ErrorBottomSheetContent( | |
message = content.message, | |
modifier = Modifier | |
.height(70.dp) | |
.fillMaxWidth() | |
.padding(20.dp) | |
) | |
} | |
BottomSheetType.REDIRECT -> { | |
val content = | |
uiState.bottomSheetContent as BottomSheetContent.RedirectSheetContent | |
RedirectBottomSheetContent( | |
message = content.message, | |
redirect = content.redirect, | |
modifier = Modifier | |
.height(70.dp) | |
.fillMaxWidth() | |
.padding(20.dp) | |
) | |
} | |
BottomSheetType.INITIAL -> InitialBottomSheetContent() | |
} | |
}, | |
sheetState = bottomSheetState, // don't forget to set this parameter | |
modifier = modifier | |
) { | |
// your screen content | |
// for example : Scaffold , Column etc. | |
} | |
// 9- Completed Code for MultipleBottomSheetContentScreen | |
@OptIn(ExperimentalMaterialApi::class) | |
@Composable | |
fun MultipleBottomSheetContentScreen( | |
modifier: Modifier = Modifier, | |
viewModel: MainViewModel = viewModel() | |
) { | |
val coroutineScope = rememberCoroutineScope() | |
val uiState by viewModel.bottomSheetState.collectAsState() | |
val bottomSheetState = rememberModalBottomSheetState( | |
initialValue = ModalBottomSheetValue.Hidden, | |
confirmStateChange = { | |
when (uiState.bottomSheetType) { | |
BottomSheetType.REDIRECT -> false | |
else -> it != ModalBottomSheetValue.Expanded | |
} | |
}, | |
skipHalfExpanded = false | |
) | |
val showBottomSheet: () -> Unit = { | |
coroutineScope.launch { | |
bottomSheetState.show() | |
} | |
} | |
val hideBottomSheet: () -> Unit = { | |
coroutineScope.launch { | |
bottomSheetState.hide() | |
} | |
} | |
val timedNonCancellableShowHide: (millis: Long) -> Unit = { | |
coroutineScope.launch { | |
showBottomSheet() | |
delay(timeMillis = it) | |
hideBottomSheet() | |
} | |
} | |
BackHandler(bottomSheetState.isVisible) { | |
if (uiState.bottomSheetType != BottomSheetType.REDIRECT) { | |
hideBottomSheet() | |
} | |
} | |
LaunchedEffect(uiState.bottomSheetActionType) { | |
when (uiState.bottomSheetActionType) { | |
BottomSheetAction.INITIAL -> {} | |
BottomSheetAction.HIDE -> hideBottomSheet() | |
BottomSheetAction.SHOW -> showBottomSheet() | |
BottomSheetAction.TIMED -> timedNonCancellableShowHide(3000) | |
} | |
} | |
LaunchedEffect(Unit) { | |
snapshotFlow { bottomSheetState.currentValue }.collect { | |
if (it == ModalBottomSheetValue.Hidden) { | |
viewModel.updateBottomSheetState( | |
bottomSheetActionType = BottomSheetAction.INITIAL, | |
bottomSheetContent = BottomSheetContent.InitialSheetContent, | |
bottomSheetType = BottomSheetType.INITIAL | |
) | |
} | |
} | |
} | |
ModalBottomSheetLayout( | |
sheetContent = { | |
when (uiState.bottomSheetType) { | |
BottomSheetType.LIST -> { | |
val content = uiState.bottomSheetContent as BottomSheetContent.ListSheetContent | |
ListBottomSheetContent( | |
title = content.title, | |
data = content.data, | |
modifier = Modifier | |
.height(300.dp) | |
.fillMaxWidth() | |
.padding(20.dp) | |
) | |
} | |
BottomSheetType.ERROR -> { | |
val content = uiState.bottomSheetContent as BottomSheetContent.ErrorSheetContent | |
ErrorBottomSheetContent( | |
message = content.message, | |
modifier = Modifier | |
.height(70.dp) | |
.fillMaxWidth() | |
.padding(20.dp) | |
) | |
} | |
BottomSheetType.REDIRECT -> { | |
val content = | |
uiState.bottomSheetContent as BottomSheetContent.RedirectSheetContent | |
RedirectBottomSheetContent( | |
message = content.message, | |
redirect = content.redirect, | |
modifier = Modifier | |
.height(70.dp) | |
.fillMaxWidth() | |
.padding(20.dp) | |
) | |
} | |
BottomSheetType.INITIAL -> InitialBottomSheetContent() | |
} | |
}, | |
sheetState = bottomSheetState, | |
modifier = modifier | |
) { | |
Column( | |
modifier = Modifier.fillMaxSize(), | |
verticalArrangement = Arrangement.spacedBy(20.dp) | |
) { | |
Button( | |
onClick = { | |
viewModel.updateBottomSheetState( | |
bottomSheetContent = BottomSheetContent.ListSheetContent( | |
title = listHeader, | |
data = listContent | |
), | |
bottomSheetActionType = BottomSheetAction.SHOW, | |
bottomSheetType = BottomSheetType.LIST | |
) | |
}, | |
enabled = uiState.bottomSheetActionType == BottomSheetAction.INITIAL | |
) { | |
Text(text = "List Bottom Sheet") | |
} | |
Button( | |
onClick = { | |
viewModel.updateBottomSheetState( | |
bottomSheetContent = BottomSheetContent.ErrorSheetContent(message = errorMessage), | |
bottomSheetActionType = BottomSheetAction.SHOW, | |
bottomSheetType = BottomSheetType.ERROR | |
) | |
}, | |
enabled = uiState.bottomSheetActionType == BottomSheetAction.INITIAL | |
) { | |
Text(text = "Error Bottom Sheet") | |
} | |
Button( | |
onClick = { | |
viewModel.updateBottomSheetState( | |
bottomSheetContent = BottomSheetContent.RedirectSheetContent( | |
message = successMessage, | |
redirect = redirectMessage | |
), | |
bottomSheetActionType = BottomSheetAction.TIMED, | |
bottomSheetType = BottomSheetType.REDIRECT | |
) | |
}, | |
enabled = uiState.bottomSheetActionType == BottomSheetAction.INITIAL | |
) { | |
Text(text = "Redirect Non-Cancellable Bottom Sheet") | |
} | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment