ModalBottomSheet in Material 3 – das Blatt, das von unten hereinfährt
Ein Menü, eine Detailansicht, eine Auswahl – auf dem Handy kommt so etwas von unten. Material 3 bringt das fertig mit, inklusive Wischgeste und Verdunkelung. Man muss nur wissen, wie man es richtig schließt.
Auf dem Handy ist das untere Drittel der bequemste Platz — dort liegt der Daumen. Deshalb kommen Auswahllisten, Filter und Detailansichten in guten Apps von unten hereingefahren und nicht als Dialog in die Mitte. Material 3 liefert das fertig.
Die kurze Fassung
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun FilterButton() {
var offen by remember { mutableStateOf(false) }
val sheetState = rememberModalBottomSheetState()
Button(onClick = { offen = true }) { Text("Filter") }
if (offen) {
ModalBottomSheet(
onDismissRequest = { offen = false },
sheetState = sheetState
) {
Column(Modifier.padding(horizontal = 20.dp).padding(bottom = 32.dp)) {
Text("Filter", style = MaterialTheme.typography.titleLarge)
// … Inhalt …
}
}
}
}onDismissRequest deckt beides ab: Wischen nach unten und Tippen auf die verdunkelte Fläche. Du musst dafür nichts extra bauen.
Die Stelle, an der es hakt
Ein eigener „Übernehmen"-Knopf im Sheet lässt es normalerweise ruckartig verschwinden — das Sheet springt weg, statt hinauszufahren. Der Grund: offen = false entfernt das Composable sofort, die Animation kommt gar nicht mehr zum Zug. Richtig ist, erst die Animation abzuwarten:
val scope = rememberCoroutineScope()
Button(onClick = {
scope.launch { sheetState.hide() } // fährt sauber hinaus
.invokeOnCompletion {
if (!sheetState.isVisible) offen = false // dann erst entfernen
}
}) { Text("Übernehmen") }Sieht umständlich aus, ist aber der offiziell dokumentierte Weg — und der Unterschied ist im Betrieb sofort sichtbar.
Zwei Einstellungen, die ich fast immer setze
val sheetState = rememberModalBottomSheetState(
skipPartiallyExpanded = true // direkt ganz auf, kein Zwischenhalt
)Bei kurzem Inhalt ist der halbe Zwischenzustand nur ein zusätzlicher Wisch für den Nutzer. Bei langen Listen dagegen ist er sinnvoll — dann lasse ich ihn drin.
Und der Punkt, den man leicht vergisst:
Column(Modifier.padding(bottom = 32.dp)) { … }Die Safe Area unten. Auf Geräten mit Gestensteuerung liegt der Home-Indikator genau dort, wo dein letzter Button sitzt. Ein Abstand nach unten kostet nichts und verhindert, dass der wichtigste Knopf im Sheet halb unter einem Systemelement klebt. ModalBottomSheet kümmert sich um einen Teil davon selbst, aber bei eigenen Fußzeilen prüfe ich das immer auf einem Gerät mit Notch nach.
Wann Sheet, wann Dialog?
Meine Regel: Sheet, wenn der Nutzer etwas auswählt oder erkundet. Dialog, wenn er eine Entscheidung bestätigen muss („Wirklich löschen?"). Ein Sheet lässt sich wegwischen — das ist bei einer Auswahl gut und bei einer Sicherheitsabfrage schlecht.
Du planst eine App und bist dir bei solchen Bedienmustern unsicher? Genau darüber rede ich gern vorab, bevor Code entsteht. Melde dich.
Quellen
Du brauchst mehr als ein Snippet?
Ich entwickle Android-Apps in Kotlin und moderne Websites für Selbstständige und kleine Unternehmen — von der ersten Idee bis zum Release.
Projekt anfragen →Verwandte Snippets
AnimatedVisibility in Jetpack Compose – ein- und ausblenden, das nicht springt
Ein Element einfach mit if wegzulassen, poppt hart weg. AnimatedVisibility blendet es weich ein und aus – mit sinnvollen Voreinstellungen und, wenn du willst, deiner eigenen Kombination aus Faden und Bewegung. Ich zeige dir beides.
kotlinx.serialization: JSON und data class ohne Handarbeit verbinden
Eine API antwortet mit JSON, deine App will eine data class. Dazwischen steht in vielen Projekten ein handgeschriebener Parser. Mit kotlinx.serialization sind es eine Annotation und eine Zeile – und ein Schalter, ohne den deine App beim nächsten API-Update abstürzt.
snapshotFlow in Jetpack Compose – aus Compose-Zustand einen Flow machen
Manchmal willst du auf einen Compose-Zustand reagieren wie auf einen Datenstrom – entprellen, kombinieren, sammeln. Genau das macht snapshotFlow. Ich zeige dir das an der häufigsten Aufgabe: mehr laden, wenn der Nutzer ans Listenende scrollt.