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
Room-Migrationen – das Schema ändern, ohne Nutzerdaten zu verlieren
Eine neue Spalte in der Entity, und beim nächsten Start ist der Spielstand weg – weil fallbackToDestructiveMigration die Datenbank einfach löscht. So machst du es richtig, inklusive Test.
stateIn und shareIn – aus einem kalten Flow wird geteilter Zustand
Jeder Sammler eines kalten Flows startet die Arbeit neu – zwei Beobachter, zwei Datenbankabfragen. stateIn und shareIn machen daraus einen Strom, den sich alle teilen, samt aktuellem Wert und Abschalten bei Inaktivität.
viewModelScope – Coroutinen, die mit dem Bildschirm verschwinden
Eine Coroutine, die nach dem Schließen des Bildschirms weiterläuft, schreibt in ein ViewModel, das niemand mehr sieht – und hält im schlimmsten Fall die ganze Activity im Speicher. viewModelScope beendet sie automatisch.