Simone Rosani
Simone Rosani

Reputation: 31

Initial value must have an associated anchor ModalBottomSheetLayout

I created a ModalBottomSheetLayout composable that takes its content dynamically with two implementations: The first opens the bottom sheet on a FAB click:

    @OptIn(ExperimentalMaterialApi::class)
    private fun setupFabClickListener(arrayRolesResponse: Set<String>?) {
        binding.btnAdd.setOnClickListener {
            val arrayString = arrayLocalDate.map { item -> item.toString() }
            arrayString as ArrayList<String>

            if (arrayRolesResponse != null && "admin" in arrayRolesResponse) {
                mainViewModel.toggleBottomSheet()
            } else {
                getCalendarActivityIntent(this, true)
            }
        }
    }
    private var _openBottomSheet =
        MutableLiveData(ModalBottomSheetState(ModalBottomSheetValue.Hidden))
    val openBottomSheet: LiveData<ModalBottomSheetState> = _openBottomSheet

    fun toggleBottomSheet() {
        _openBottomSheet.postValue(
            if (_openBottomSheet.value?.currentValue == ModalBottomSheetValue.Hidden) {
                ModalBottomSheetState(initialValue = ModalBottomSheetValue.Expanded)
            } else {
                ModalBottomSheetState(initialValue = ModalBottomSheetValue.Hidden)
            }
        )
    }

And it works as expected, while the second one, implemented like this, throws an IllegalArgumentException with The initial value must have an associated anchor.

    @OptIn(ExperimentalMaterialApi::class)
    private fun setupCompose(date: String) {
        binding.composeView.setContent {
            val isLoading by viewModel.isLoaded.observeAsState()
            val reservations by viewModel.reservationsInDates.observeAsState()
            val modalBottomSheetState = rememberModalBottomSheetState(ModalBottomSheetValue.Hidden)
            val coroutineScope = rememberCoroutineScope()

            ReservationsListPage(
                reservationsList = reservations?.data?.content,
                isLoading = isLoading,
                date = date,
                bottomSheetState = modalBottomSheetState
            ) { coroutineScope.launch { modalBottomSheetState.show() } }
        }
    }

@OptIn(ExperimentalMaterialApi::class)
@Composable
fun ReservationsListPage(
    reservationsList: List<Content>?,
    isLoading: Boolean?,
    date: String,
    bottomSheetState: ModalBottomSheetState,
    onBottomSheetCall: () -> Unit
) {
    Column {
        AppBar(onBottomSheetCall)
        if (isLoading == true) {
            LoadingSpinner()
        } else {
            ReservationsListHeading(
                month = getAbbreviatedMonth(date.getMonth()),
                dayOfWeek = formatDayOfWeek(date.getDayOfWeek()),
                numberOfMonth = date.getDayOfMonth()
            )
            ReservationsList(reservationsList)
            BottomSheet(state = bottomSheetState) {
                ListItem() {
                    Text(text = "Show / Hide rejected reservations")
                }
            }
        }
    }
}

@Composable
fun ReservationsList(reservationsList: List<Content>?) {
    Column(
        modifier = Modifier.padding(16.dp)
    ) {
        LazyColumn(
            modifier = Modifier.fillMaxSize()
        ) {
            reservationsList?.forEach {
                item { ReservationsListItem(it) }
            }
        }
    }
}

@Composable
fun ReservationsListItem(reservation: Content) {
    val pending = painterResource(R.drawable.ic_pending)
    val accepted = painterResource(R.drawable.ic_accepted)
    val rejected = painterResource(R.drawable.ic_rejected)

    Card(
        shape = MaterialTheme.shapes.small,
        backgroundColor = lightWhite,
        modifier = Modifier.padding(top = 8.dp, bottom = 8.dp),
    ) {
        Row(
            modifier = Modifier
                .fillMaxWidth()
                .padding(top = 8.dp, bottom = 8.dp, start = 16.dp, end = 16.dp),
            verticalAlignment = Alignment.CenterVertically,
            horizontalArrangement = Arrangement.SpaceBetween
        ) {
            Column(Modifier.weight(2f)) {
                Text(
                    text = "${reservation.seat?.room?.description}",
                    style = MaterialTheme.typography.h6
                )
                Text(
                    text = "${reservation.user?.username}",
                    style = MaterialTheme.typography.subtitle1
                )
            }
            Row(
                modifier = Modifier.weight(1f),
                horizontalArrangement = Arrangement.SpaceBetween
            ) {
                Text(
                    text = "${reservation.seat?.description}",
                    style = MaterialTheme.typography.subtitle1,
                )
                Icon(
                    painter = when (reservation.reservationStatus?.status) {
                        "ACCEPTED" -> accepted
                        "REJECTED" -> rejected
                        else -> pending
                    },
                    contentDescription = "Reservation status",
                    tint = Color.Unspecified,
                )
            }
        }
    }
}

@Composable
fun AppBar(onBottomSheetCall: () -> Unit) {
    var isMenuExpanded by remember { mutableStateOf(false) }

    Column() {
        TopAppBar(
            title = { Text(text = "Reservations", color = Color.White) },
            actions = {
                IconButton(onClick = { isMenuExpanded = true }) {
                    Icon(
                        imageVector = Icons.Default.MoreVert,
                        contentDescription = "More options",
                        tint = Color.White
                    )
                }
                AppBarDropdownMenu(
                    isMenuExpanded = isMenuExpanded,
                    onBottomSheetCall = onBottomSheetCall,
                    onDismissRequest = { isMenuExpanded = false }
                )
            },
            backgroundColor = primary,
        )
    }
}

@Composable
@OptIn(ExperimentalMaterialApi::class)
fun AppBarDropdownMenu(
    isMenuExpanded: Boolean,
    onDismissRequest: () -> Unit,
    onBottomSheetCall: () -> Unit
) {
    DropdownMenu(expanded = isMenuExpanded, onDismissRequest = onDismissRequest) {
        DropdownMenuItem(onClick = { /*TODO*/ }) {
            ListItem(
                icon = { Icon(painterResource(R.drawable.ic_sort), "Sort") }
            ) { Text(text = "Sort", style = MaterialTheme.typography.body1) }
        }
        Divider()
        DropdownMenuItem(onClick = onBottomSheetCall) {
            ListItem(
                icon = { Icon(painterResource(R.drawable.ic_filter), "Filter") }
            ) { Text(text = "Filter", style = MaterialTheme.typography.body1) }
        }
    }
}

@Composable
fun ReservationsListHeading(month: String, dayOfWeek: String, numberOfMonth: String) {
    Row(
        modifier = Modifier
            .fillMaxWidth()
            .padding(top = 16.dp, start = 16.dp, end = 16.dp),
        verticalAlignment = Alignment.CenterVertically
    ) {
        HeadingLeft(month = month, dayOfWeek = dayOfWeek, numberOfMonth = numberOfMonth)
    }
}

@Composable
fun HeadingLeft(month: String, dayOfWeek: String, numberOfMonth: String) {
    Row(
        verticalAlignment = Alignment.CenterVertically,
    ) {
        Text(text = month, style = MaterialTheme.typography.h3)
        Column(
            modifier = Modifier.padding(8.dp)
        ) {
            Text(text = dayOfWeek, style = MaterialTheme.typography.body1)
            Text(text = numberOfMonth, style = MaterialTheme.typography.subtitle1)
        }
    }
}

And here's my implementation of ModalBottomSheetLayout.

@Composable
@OptIn(ExperimentalMaterialApi::class)
fun BottomSheet(
    state: ModalBottomSheetState,
    content: @Composable () -> Unit
) {
    ModalBottomSheetLayout(
        sheetContent = {
            Column(
                Modifier
                    .fillMaxWidth()
                    .padding(top = 8.dp, bottom = 8.dp)
            ) {
                content()
            }
        },
        sheetElevation = 0.dp,
        sheetState = state,
    ) {}
}

Any idea on why this might be happening? I couldn't really find much either here on SO or on google in general.

Upvotes: 2

Views: 2640

Answers (2)

Jo&#227;o Eudes Lima
Jo&#227;o Eudes Lima

Reputation: 895

In my case I solved it by putting a defaultMinSize when the content of the BottomSheet is dynamic, applying in your example:

    @Composable
    @OptIn(ExperimentalMaterialApi::class)
    fun BottomSheet(
        state: ModalBottomSheetState,
        content: @Composable () -> Unit
    ) {
        ModalBottomSheetLayout(
            sheetContent = {
                Column(
                    Modifier
                        .fillMaxWidth()
                        .defaultMinSize(minHeight = 100.dp)
                ) {
                    content()
                }
            },
            sheetElevation = 0.dp,
            sheetState = state,
        ) {}
    }

Upvotes: 2

Simone Rosani
Simone Rosani

Reputation: 31

Resolved

I completely missed the optional parameter content: (@Composable () -> Unit)? which according to the androidx.compose documentation is The content of rest of the screen. I resolved the issue by putting the BottomSheet composable at the beginning of the function and the rest of the view as the content parameter.

fun ReservationsListPage(
    reservationsList: ArrayList<Content>,
    isLoading: Boolean?,
    date: String,
    bottomSheetState: ModalBottomSheetState,
) {
    Column {
        BottomSheet(
            pageContent = {
                Column {
                    AppBar(bottomSheetState)
                    if (isLoading == true) {
                        LoadingSpinner()
                    } else {
                        ReservationsListHeading(
                            month = getAbbreviatedMonth(date.getMonth()),
                            dayOfWeek = formatDayOfWeek(date.getDayOfWeek()),
                            numberOfMonth = date.getDayOfMonth()
                        )
                        ReservationsList(reservationsList)
                    }
                }
            },
            content = {
                Column {
                    Text("Hello")
                }
            },
            state = bottomSheetState,
        )
    }
}

Upvotes: 1

Related Questions