Sheet of BottomSheetScaffold automatically expands on recomposition
Asked Answered
B

5

6

In Jetpack Compose, I have a BottomSheetScaffold with some content. This content is observed from the ViewModel, like this:

BottomSheetScaffold(
    sheetContent = { SheetContent(sheetData = viewModel.sheetData) }
) {}

So whenever viewModel.sheetData changes, a recomposition is triggered. Whenever this happens, the bottom sheet automatically expands. Is this a bug or a feature? Can I disable this? I am using the newest version: 1.1.0-alpha01

Edit: Here is an example using LaunchedEffect instead of a ViewModel.

@OptIn(ExperimentalMaterialApi::class)
@Preview
@Composable
fun HomeScreen() {
    var addSheetData by remember { mutableStateOf(false) }

    LaunchedEffect(true) {
        delay(2000)
        addSheetData = true
    }

    BottomSheetScaffold(sheetContent = {
        if (addSheetData) {
            Column {
                Text(text = "Text1", fontSize = 36.sp)
                Text(text = "Text2", fontSize = 36.sp)
                Text(text = "Text3", fontSize = 36.sp)
                Text(text = "Text4", fontSize = 36.sp)
                Text(text = "Text5", fontSize = 36.sp)
            }
        }
    }, sheetBackgroundColor = Color.LightGray) {}
}

The sheet with the 5 texts expands automatically.

Blumenthal answered 17/8, 2021 at 14:25 Comment(2)
If you wanna your question to be answered, you wanna make time for an expert to get working sample with reproducible problem as fast as possible. Perfectly I should just paste your code into my sample project and see the problem as fast as I run it. Please update your code to minimal reproducible example. It should include sample data like strings/numbers, and you can update it over time with LaunchedEffectOlsewski
@Philip Added an example, as requested.Blumenthal
K
6

It seems a bug.
Currently (1.0.x and 1.1.0-alpha01) it happens when the sheetContentHeight < sheetPeekHeight.
In this case the scaffoldState.bottomSheetState result expanded also if the content sheet is not displayed.

You can easily verify it using in your code:

sheetBackgroundColor = if (scaffoldState.bottomSheetState.isCollapsed) Color.LightGray else Color.Yellow

When addSheetData == false the background color becomes Yellow. When you recompose the composable, since the state is expanded, the content sheet is full expanded.

As workaround you can use something like:

  sheetContent = { 
        if (!addSheetData){
            Box(Modifier.fillMaxWidth().height(60.dp))
        }    

        if (addSheetData) {
            Column {
                Text(text = "Text1", fontSize = 36.sp)
                Text(text = "Text2", fontSize = 36.sp)
                Text(text = "Text3", fontSize = 36.sp)
                Text(text = "Text4", fontSize = 36.sp)
                Text(text = "Text5", fontSize = 36.sp)
            }
        }
  }
Koniology answered 17/8, 2021 at 16:25 Comment(2)
Interesting! Apparently it always starts as expanded, really weird. I'll use this workaround for now, thanks so much!Blumenthal
Is there an open issue for this? I don't see it mentioned in 1.2.X release notes Edit: It looks like there is issuetracker.google.com/issues/202868798 Assigned 8 months ago...Herr
E
2

This seems to be a bug with BottomSheetScaffold. However you can try an alternative to this which is ModalBottomSheetLayout. More info about this composable can be found here.

Ermentrude answered 2/2, 2023 at 15:19 Comment(0)
W
1

Try adding an initial value:

val bottomSheetScaffoldState = rememberBottomSheetScaffoldState(
        bottomSheetState = BottomSheetState(
            initialValue = BottomSheetValue.Collapsed,
        )
    )
Weidman answered 30/3, 2022 at 5:37 Comment(1)
This unfortunately did not solve the issueBlumenthal
L
1

I couldn't fix the unwanted recomposition. What happens is whenever it recomposes, it goes back to the initialValue: SheetValue. In my case i'm using rememberStandardBottomSheetState which has a default value of initialValue: SheetValue = PartiallyExpanded so everytime it recomposes my bottomSheet "minimises" ...

My work around is to set my initial value to a saveable state and updated it whenever my state changes so when it recomposes the bottomSheet keeps its state.

var sheetState by remember { mutableStateOf(SheetValue.PartiallyExpanded) }

If your bottomSheet can only change via "dragging" you can intercept the states changes via confirmValueChange found in rememberStandardBottomSheetState in my case i could also expand() / "minimise"() via the dragHandle onClicks. You can take inspiration from my code :

    @Composable
    private fun StickyBottomSheet(
        content: @Composable ColumnScope.() -> Unit,
        title: String? = null,
        onStateChange: (BottomSheetState) -> Unit = {}
    ){
    val scope = rememberCoroutineScope()
    var sheetState by remember { mutableStateOf(SheetValue.PartiallyExpanded) }
    val state = rememberStandardBottomSheetState(
        initialValue = sheetState,
        confirmValueChange = { sheetValue ->
            sheetState = sheetValue
            when (sheetValue) {
                SheetValue.Hidden -> false

                SheetValue.Expanded -> {
                    onStateChange(BottomSheetState.Expanded)
                    true
                }

                SheetValue.PartiallyExpanded -> {
                    onStateChange(BottomSheetState.Minimised)
                    true
                }
            }
        }
    )
    val scaffoldState = rememberBottomSheetScaffoldState(bottomSheetState = state)
    BottomSheetScaffold(
        sheetContent = content,
        scaffoldState = scaffoldState,
        sheetShadowElevation = 8.dp,
        sheetDragHandle = {
            DragHandle(
                title = title,
                onClick = {
                    scope.launch {
                        if (scaffoldState.bottomSheetState.currentValue == SheetValue.PartiallyExpanded) {
                            sheetState = SheetValue.Expanded
                            scaffoldState.bottomSheetState.expand()
                            onStateChange(BottomSheetState.Expanded)
                        } else {
                            scaffoldState.bottomSheetState.show()
                            onStateChange(BottomSheetState.Dismissed)
                        }
                    }
                },
                expanded = scaffoldState.bottomSheetState.currentValue == SheetValue.Expanded
            )
        }
    ) {

    }
}

@Composable
private fun DragHandle(title: String?, onClick: () -> Unit = {}, expanded: Boolean){
Surface(
    modifier = Modifier.height(56.dp)
) {
    AnimatedVisibility(visible = expanded, enter = fadeIn(), exit = fadeOut()) {
        Box(modifier = Modifier.fillMaxWidth()) {
            Box(
                Modifier
                    .padding(top = 16.dp)
                    .size(width = 32.dp, height = 4.dp)
                    .background(wjColorScheme.outline)
                    .align(Alignment.TopCenter)
            )
            IconButton(
                modifier = Modifier.align(Alignment.CenterEnd),
                onClick = { onClick() },
                content = { Icon(R.drawable.ic_close_24 )})
        }
    }
    AnimatedVisibility(!expanded, enter = fadeIn(), exit = fadeOut()) {
        Row(modifier = Modifier.clickable { onClick() }
        ) {
            ListItem(
                leadingContent = { Icon(R.drawable.ic_add_24) },
                text2 = title
            )
        }
    }
}

}

Leschen answered 21/11, 2023 at 7:11 Comment(0)
A
0

I managed to work around the issue by creating a custom state that manages the scaffold state, and shows the sheet content depending on my custom state because the scaffold state always returns to Expanded.

Here is my solution:

@OptIn(ExperimentalMaterialApi::class)
@Composable
fun CustomBottomSheet(
    customBottomSheetState: CustomBottomSheetState = rememberCustomBottomSheetState(remember { CustomBottomSheetState() }),
    topBar: (@Composable () -> Unit)? = null,
    sheetContent: @Composable() (ColumnScope.() -> Unit),
    content: @Composable () -> Unit
) {
    val scaffoldState: BottomSheetScaffoldState = rememberBottomSheetScaffoldState()
    customBottomSheetState.scaffoldState = scaffoldState
    // this to handle updating the state when collapsing by swiping the sheet down
    if (scaffoldState.bottomSheetState.isCollapsed) {
        customBottomSheetState.currentValue.value = CustomBottomSheetValue.Collapsed
    }
    BottomSheetScaffold(
        sheetPeekHeight = 0.dp,
        topBar = topBar,
        scaffoldState = scaffoldState,
        sheetShape = BottomSheetShape,
        sheetContent = {
            if (customBottomSheetState.isExpanded()) {
                sheetContent()
            }
        }
    ) {
        Box(Modifier.padding(it)) {
            content()
        }
    }
}

@Stable
@OptIn(ExperimentalMaterialApi::class)
class CustomBottomSheetState constructor(
    initialValue: MutableState<CustomBottomSheetValue> = mutableStateOf(CustomBottomSheetValue.Collapsed)
) {
    var currentValue = initialValue
    var scaffoldState: BottomSheetScaffoldState? = null
    suspend fun expand() {
        scaffoldState?.bottomSheetState?.expand()
        currentValue.value = CustomBottomSheetValue.Expanded
    }

    suspend fun collapse() {
        scaffoldState?.bottomSheetState?.collapse()
        currentValue.value = CustomBottomSheetValue.Collapsed
    }
    fun isCollapsed(): Boolean {
        return this.currentValue.value == CustomBottomSheetValue.Collapsed
    }

    fun isExpanded(): Boolean {
        return this.currentValue.value == CustomBottomSheetValue.Expanded
    }
}

enum class CustomBottomSheetValue {
    Expanded, Collapsed;
}

@Composable
fun rememberCustomBottomSheetState(bottomSheetState: CustomBottomSheetState = CustomBottomSheetState()) =
    remember(bottomSheetState) { bottomSheetState }

And that's how it is used:

    val bottomSheetState = rememberCustomBottomSheetState()
    CustomBottomSheet(topBar = {
        // topbar
    }, customBottomSheetState = bottomSheetState, sheetContent = {
            // sheet
        }) {
        // screen
    }

You can modify the parameters to include all of the BottomSheetScaffold parameters.

Agile answered 28/10, 2022 at 13:5 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.