Android Compose: Screen Freeze when Closing Bottom Sheet with Drag or Back Press

1.5k Views Asked by At

I'm working on an Android app using Jetpack Compose, and I've encountered a problem When I interact with a Bottom Sheet (dragging it down or using the back button to dismiss it), my screen becomes unresponsive, and nothing works until I restart the app.. This behavior is not expected, and I'm struggling to identify the root cause. Here are the relevant details:

this is BottomSheet:

fun BottomSheet(
    onDismiss: () -> Unit
) {
    val bottomSheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)  
    val scope = rememberCoroutineScope()
    
    scope.launch {
            bottomSheetState.expand()
    }
    
    ModalBottomSheet(
        onDismissRequest = { onDismiss() },
        sheetState = bottomSheetState,
        dragHandle = { BottomSheetDefaults.DragHandle() }
    ) {
             ColorPicker()     
    }
    
}```

this is where it opens:


Scaffold(
modifier = Modifier.fillMaxSize(),
scaffoldState = scaffoldState,
topBar = {
AppBar(
...
)
},
content = { paddingValue -\>

            Box(
                modifier = Modifier
                    .fillMaxSize()
                    .padding(
                        top = paddingValue.calculateTopPadding(),
                    )
            ) {
                Column(
                    modifier = Modifier
                        .fillMaxWidth()
                        .padding(16.dp)
                        .verticalScroll(rememberScrollState())
                
                ) {
                    // Night Mode setting Section
                        ...
                    // Font setting Section
                        ...
                    // color setting Section
    
                        CustomCard(
                            title = "Color",
                            isExpandedInitially = fontColorSettingExpand,
                            onItemClick = {
                                fontColorSettingExpand = !fontColorSettingExpand
                            }) {
                            BottomSheet(
                                onDismiss = {
                                    //todo save
                                })

}```

What I've Tried:

  • Checked my state management to ensure it's correct.
  • Reviewed my coroutine usage for any potential blocking issues.
  • Verified that I'm using the appropriate Compose components and ScaffoldState.
4

There are 4 best solutions below

0
On BEST ANSWER

Add ModalBottomSheet to compose only when it is visible. Use the following condition to solve your problem:

fun BottomSheet(
    onDismiss: () -> Unit
) {
    val bottomSheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) 
    LaunchedEffect(key1 = Unit) {
            bottomSheetState.expand()
    }
    
    if (bottomSheetState.isVisible) //<-- add this condition to add sheet only when it is visible
        ModalBottomSheet(
             onDismissRequest = { onDismiss() },
             sheetState = bottomSheetState,
             dragHandle = { BottomSheetDefaults.DragHandle() }
         ) {
             ColorPicker()     
           }
    
}
0
On

The answer by Amani caused the Modal to be presented without animation for me. The unresponsive screen seems to be a bug in Material3 that was fixed in Version 1.2.0-alpha08 where the release notes state:

Fixed a bug where ModalBottomSheet was not calling onDismissedRequest when dismissing it by swiping down on the sheet

Updating Material3 to Version 1.2.0-alpha08 or Version 1.2.0-alpha10 (latest version at the time of posting) resolved the issue for me without having the need for any if-statements or LaunchedEffects

0
On

I encountered the same issue and none of the previously suggested solutions effectively resolved my problem—they either caused UI blocking or prevented the bottom sheet from completing its animation, resulting in its disappearance without animation.

To address this, I leveraged the confirmValueChange lambda within ModalBottomSheetState as follows:

val sheetState: SheetState = rememberModalBottomSheetState(
    skipPartiallyExpanded = true,
    confirmValueChange = {
        if (it == SheetValue.Hidden && show) scope.launch { onDismissed() }
        isDismissEnabled
    }
)

The primary challenge was that the onDismissRequest callback of ModalBottomSheet wasn't triggered immediately upon hiding the sheet. Instead, it was delayed until after the sheet had completely disappeared, causing the condition in the if statement to deactivate too late. This delay effectively blocked the UI because the ModalBottomSheet remained in the compose tree longer than necessary.


Below is the complete code for context:

@Composable
fun MyAppPopUp(
    modifier: Modifier = Modifier,
    onDismissed: () -> Unit,
    show: Boolean,
    scrimColor: Color = FibColorPalate.Base.BlackVariant.copy(alpha = 0.55f),
    isDismissEnabled: Boolean = true,
    shape: Shape = FibTheme.shapes.general.medium,
    padding: PaddingValues = PaddingValues(FibTheme.spacing.small),
    content: @Composable () -> Unit,
) {
    val scope = rememberCoroutineScope()
    val sheetState: SheetState = rememberModalBottomSheetState(
        skipPartiallyExpanded = true,
        confirmValueChange = {
            if (it == SheetValue.Hidden && show) scope.launch { onDismissed() }
            isDismissEnabled
        }
    )

    var openBottomSheet by remember { mutableStateOf(show) }

    LaunchedEffect(show) {
        if (show) {
            openBottomSheet = true
            scope.launch { sheetState.expand() }
        } else {
            scope.launch {
                sheetState.hide()
            }.invokeOnCompletion {
                openBottomSheet = false
            }
        }
    }

    if (openBottomSheet) {
        ModalBottomSheet(
            onDismissRequest = onDismissed,
            modifier = Modifier
                .fillMaxWidth()
                .then(modifier),
            sheetState = sheetState,
            containerColor = Color.Transparent,
            scrimColor = scrimColor,
            dragHandle = null,
        ) {
            Box(
                modifier = Modifier
                    .navigationBarsPadding()
                    .padding(padding)
                    .clip(shape)
            ) {
                content()
            }
        }
    }
}
1
On

The problem you’re having is that you’re calling bottomSheetState.expand() within the composition of your function, so when you do a Dismiss or a Drag, this code is executed over and over again, freezing the Screen.

To solve it, you can call bottomSheetState.expand() within a LaunchedEffect, so it runs outside of the composition. It would look like this:

fun BottomSheet(
    onDismiss: () -> Unit
) {
    val bottomSheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
    
    ModalBottomSheet(
        onDismissRequest = { onDismiss() },
        sheetState = bottomSheetState,
        dragHandle = { BottomSheetDefaults.DragHandle() }
    ) {
             ColorPicker()     
    }

  LaunchedEffect(key1 = Unit, block = {
     bottomSheetState.expand()
  })
    
}

Note:

Be careful with the code you expose when asking for help, because the first Snippet shows that the BottomSheet only has onDismiss as a parameter, while the second reflects that you are passing a navController (which is not recommended), you should expose a lambda and then perform the necessary action, but the BottomSheet should not handle navigation), so if the code you show is not up to date, it's difficult to find the possible error.