Skip to content

Instantly share code, notes, and snippets.

@kishan-vadoliya
Last active December 30, 2023 12:46
Show Gist options
  • Save kishan-vadoliya/4b4799693914ec5041aecd52c186d7ff to your computer and use it in GitHub Desktop.
Save kishan-vadoliya/4b4799693914ec5041aecd52c186d7ff to your computer and use it in GitHub Desktop.
Modal Bottom Sheet Layout Jetpack Compose
// 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