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
Berechtigungen und Foto-Auswahl in Compose – rememberLauncherForActivityResult
Kamera-Erlaubnis abfragen, ein Bild aus der Galerie holen, ein Dokument öffnen: Dafür gibt es in Compose genau einen Weg, und er ist erfreulich kurz. onActivityResult und startActivityForResult brauchst du nie wieder.
callbackFlow – Listener-APIs in einen Flow verwandeln
Android steckt voller Callback-APIs mit `register`/`unregister`. Mit `callbackFlow` machst du daraus einen ganz normalen Flow – inklusive automatischem Abmelden, wenn niemand mehr zuhört.
CompositionLocal – Werte durch den Compose-Baum reichen, ohne sie durchzuschleifen
Wenn ein Wert durch acht Composables durchgereicht wird, nur damit ihn das neunte benutzen kann, ist etwas schief. CompositionLocal löst das – aber nur für eine sehr bestimmte Sorte von Werten.