Flutter: Bildschirmabhängiges Design mit LayoutBuilder
Zuletzt aktualisiert:
8 Min. Lesezeit

LayoutBuilder reicht Ihnen beim Build die BoxConstraints seines Parents weiter und lässt Sie abhängig davon einen anderen Widget-Baum zurückgeben. Eine Karte, die auf dem Smartphone untereinander und auf dem Tablet nebeneinander anordnet; ein Seitenpanel, das nur erscheint, wenn die Breite reicht; ein Bild, das achtzig Prozent des zugeteilten Platzes einnimmt: All das entsteht mit diesem Widget. Dieser Beitrag konzentriert sich auf das Konzept selbst: den Unterschied zwischen LayoutBuilder und MediaQuery, den Inhalt des constraints-Objekts und die Herkunft der Fehler zu unbegrenzter Höhe und „intrinsic dimensions". Danach geht es um ein Breakpoint-Layout für Telefon, Tablet und Desktop auf Basis der Material-3-Fenstergrößenklassen. Die anteilige Skalierung am Bildschirm beschreibe ich separat im Beitrag zu MediaQuery.
Live-Demo: responsives Design mit LayoutBuilder
Ziehen Sie den senkrechten Teiler, um das Beispiel breiter und schmaler zu machen, und beobachten Sie, wie sich das Layout ändert:
💡 Falls das Beispiel oben nicht lädt, klicken Sie auf DartPad, um es in einem neuen Tab auszuführen.
Grundlegende Verwendung
LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
return Text('Verfügbare Breite: ${constraints.maxWidth}');
},
)Anders als eine normale build-Methode wird builder in der Layout-Phase aufgerufen, also nachdem die Constraints des Parents feststehen. Deshalb kann sich der zurückgegebene Baum am tatsächlich verfügbaren Platz orientieren. Sobald sich die Constraints ändern (der Bildschirm dreht sich, das Fenster wird skaliert, ein Seitenpanel öffnet sich), läuft der Builder erneut.
Das BoxConstraints-Objekt enthält vier Grenzen: minWidth, maxWidth, minHeight, maxHeight. Dazu kommen einige Helfer: hasBoundedWidth und hasBoundedHeight sagen, ob es auf dieser Achse eine Obergrenze gibt, isTight, ob min und max gleich sind, und biggest liefert die größte Size, die hineinpasst. In der Praxis wird maxWidth am häufigsten gelesen; bevor Sie maxHeight lesen, prüfen Sie immer hasBoundedHeight, warum, erkläre ich weiter unten.
LayoutBuilder und MediaQuery im Vergleich
Der wichtigste Gedanke ist dieser: MediaQuery.sizeOf(context) liefert die Größe des Bildschirms, LayoutBuilder den Platz, den der Parent Ihnen zugeteilt hat. Beide nennen nur dann dieselbe Zahl, wenn Ihr Widget den ganzen Bildschirm ausfüllt.
Stellen Sie sich eine Produktkarte vor. Dieselbe Karte wird auf dem Smartphone in einer Liste über die volle Breite und auf dem Tablet in einem 300 Pixel breiten Seitenpanel verwendet. Fragt die Karte MediaQuery „ist der Bildschirm breiter als 900?", um horizontal anzuordnen, versucht sie im Seitenpanel des Tablets ein horizontales Layout in 300 Pixel zu quetschen und läuft über. Fragt sie LayoutBuilder „habe ich weniger als 500 bekommen?", verhält sie sich überall richtig.
Faustregel: Geht es um den eigenen Platz des Widgets, LayoutBuilder; geht es um das Gerät (Ausrichtung, Tastaturhöhe, sicherer Bereich, Textskalierung), MediaQuery.
Breakpoints und Fenstergrößenklassen
Material 3 teilt die Fensterbreite in Klassen ein: unter 600 dp compact (Telefon), 600 bis 839 medium (kleines Tablet, Telefon im Querformat), ab 840 expanded (Tablet, Desktop). Für sehr breite Bildschirme gibt es zusätzlich die Klassen large und extra-large ab 1200 dp. Die Schwellenwerte aus diesen Klassen zu übernehmen, ist konsistenter, als beliebige Zahlen zu wählen:
enum ScreenType { compact, medium, expanded }
ScreenType screenTypeFor(double width) {
if (width < 600) return ScreenType.compact;
if (width < 840) return ScreenType.medium;
return ScreenType.expanded;
}
LayoutBuilder(
builder: (context, constraints) => switch (screenTypeFor(constraints.maxWidth)) {
ScreenType.compact => const MobileBody(),
ScreenType.medium => const TabletBody(),
ScreenType.expanded => const DesktopBody(),
},
)Halten Sie die Schwellenwerte in einer einzigen Funktion; ist die Zahl < 600 über zehn Stellen verteilt, wird eine spätere Änderung mühsam. Die Spaltenzahl können Sie aus derselben Quelle ableiten: Für den crossAxisCount einer GridView passt sich eine Rechnung wie constraints.maxWidth ~/ 180 (mit einem Ergebnis von mindestens 1) geschmeidiger an als feste Breakpoints.
Wie sich das Gerüst pro Klasse ändert
Ein Breakpoint ist nicht nur ein Schwellenwert; mit jeder Klasse ändert sich auch das Gerüst des Screens. Eine verbreitete Aufteilung:
- Compact: Eine Spalte, der Inhalt fließt vertikal; die Hauptnavigation sitzt unten.
- Medium: Die Navigation kann an die Seite wandern; Liste und Detail rücken nebeneinander. Um die restliche Breite auf Spalten zu verteilen, ist Expanded das praktischste Werkzeug:
Row(children: [SizedBox(width: 240, child: SideMenu()), Expanded(child: Content())]). - Expanded und größer: Mehrere Bereiche stehen nebeneinander, und der Inhalt wird auf eine Maximalbreite begrenzt.
Der Grund für den letzten Punkt ist die Lesbarkeit: Laufen Textzeilen auf einem breiten Monitor über den ganzen Bildschirm, verliert das Auge beim Sprung zum Anfang der nächsten Zeile den Faden; den Inhalt links zu stauchen und rechts alles leer zu lassen, wirkt dagegen halbfertig. Den Inhalt zu zentrieren und in der Breite zu begrenzen, löst beides:
Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 1200),
child: const Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(flex: 2, child: ContentArea()),
SizedBox(width: 24),
Expanded(child: Sidebar()),
],
),
),
)Die 1200 sind keine feste Regel, sondern die lesbare Breite Ihres Designs. Dasselbe Layout funktioniert auch in einem Fenster: Verkleinert der Nutzer auf dem Desktop das Fenster, läuft LayoutBuilder erneut, und das Layout wechselt in eine kleinere Klasse.
Anteilige Größen
Verhältnisse wie constraints.maxWidth * 0.8 funktionieren für Breiten gut. Skalieren Sie aber keine Schriftgrößen mit der Breite: Auf einem kleinen Smartphone wird der Text unlesbar, auf einem breiten Monitor plakatgroß. Die Schriftgröße sollte aus dem Theme und den Barrierefreiheitseinstellungen des Nutzers kommen. Wenn Sie nur eine anteilige Breite wollen, brauchen Sie nicht einmal LayoutBuilder; FractionallySizedBox(widthFactor: 0.8, child: ...) erledigt das in einem einzigen Widget.
Wann verwenden – und wann nicht?
Greifen Sie zu LayoutBuilder:
- Wenn ein Widget abhängig vom eigenen Platz eine andere Struktur bauen muss (Wechsel Row/Column, Panel ausblenden, Spaltenzahl wählen).
- Wenn Sie eine wiederverwendbare Komponente schreiben und nicht wissen, wo sie landet.
- Wenn Sie herausfinden müssen, ob der Parent unbegrenzt ist, und entsprechend reagieren wollen.
Wechseln Sie zu einem anderen Werkzeug:
- Für Entscheidungen auf Geräteebene
MediaQuery(orientationOf,paddingOf,textScalerOf). - Für die reine Unterscheidung Hoch-/Querformat
OrientationBuilder; er nutzt intern LayoutBuilder, liest sich aber klarer. - Um Restplatz aufzuteilen,
ExpandedundFlexible; sie brauchen keine Messung. - Um Überlauf in die nächste Zeile zu schieben,
Wrap; das erspart Breakpoints bei Chip- und Tag-Listen.
Häufige Fehler
1. maxHeight in einer Column oder ListView lesen
Symptom: BoxConstraints forces an infinite height. oder ein Layoutfehler, weil die Höhe eines Container Infinity ist. Column und scrollbare Widgets geben ihren Kindern entlang der Hauptachse unbegrenzte Höhe; constraints.maxHeight liefert dort double.infinity, und das in eine Größe zu verwandeln, bricht das Layout.
LayoutBuilder(
builder: (context, constraints) {
final height = constraints.hasBoundedHeight
? constraints.maxHeight * 0.5
: 200.0; // eine sinnvolle Konstante, falls unbegrenzt
return SizedBox(height: height, child: const Placeholder());
},
)Lösung: hasBoundedHeight prüfen oder den LayoutBuilder an eine Stelle mit bekannter Höhe verschieben (Expanded, SizedBox, Scaffold body).
2. „LayoutBuilder does not support returning intrinsic dimensions"
Symptom: genau dieser Fehler in der Konsole. LayoutBuilder kennt sein Kind erst, wenn die Constraints da sind, und kann deshalb die Frage „was ist deine natürliche Größe?" nicht beantworten. Diese Frage stellen IntrinsicHeight, IntrinsicWidth, Table mit intrinsischen Spaltenbreiten und, Achtung, der Inhaltsbereich von AlertDialog. Ein LayoutBuilder direkt in einem Dialog löst diesen Fehler aus. Lösung: den intrinsischen Wrapper entfernen, dem Dialog eine konkrete Größe wie SizedBox(width: 400, child: ...) geben oder die Entscheidung von außen als Parameter hereinreichen statt über LayoutBuilder.
3. setState im Builder aufrufen
Symptom: setState() or markNeedsBuild() called during build. Der Builder läuft während des Layouts; dort State zu ändern lässt den Baum inkonsistent zurück. Statt einen breitenabhängigen Wert in den State zu schreiben, berechnen Sie ihn im Builder als lokale Variable und verwenden ihn direkt.
4. LayoutBuilder zum Messen des Bildschirms verwenden
Symptom: Ein Widget in einem Seitenpanel fällt auf das „Mobil"-Layout zurück statt auf „Desktop". LayoutBuilder meldet den Platz des Parents, nicht den Bildschirm; in einem 300 Pixel breiten Panel lesen Sie immer 300. Wollen Sie eine Geräteentscheidung, nutzen Sie MediaQuery.sizeOf(context).width. Streng genommen ist das kein Fehler, sondern genau das, was LayoutBuilder tun soll; machen Sie sich Ihre Absicht klar, damit es Sie nicht überrascht.
Mini-Szenario: Eine Produktkarte, die überall passt
In einer E-Commerce-App wird die Produktkarte sowohl in der Liste über die volle Breite auf der Startseite als auch in der schmalen Spalte des Warenkorbs verwendet. Bekommt die Karte weniger als 420 Pixel, setzt sie das Bild nach oben; darüber steht das Bild links und der Text rechts.
class ProductCard extends StatelessWidget {
const ProductCard({super.key, required this.title, required this.price, required this.imageUrl});
final String title;
final String price;
final String imageUrl;
@override
Widget build(BuildContext context) {
return Card(
clipBehavior: Clip.antiAlias,
child: LayoutBuilder(
builder: (context, constraints) {
final isNarrow = constraints.maxWidth < 420;
final image = AspectRatio(
aspectRatio: isNarrow ? 16 / 9 : 1,
child: Image.network(imageUrl, fit: BoxFit.cover),
);
final info = Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(title, style: Theme.of(context).textTheme.titleMedium),
const SizedBox(height: 8),
Text(price, style: Theme.of(context).textTheme.bodyLarge),
const SizedBox(height: 12),
FilledButton(onPressed: () {}, child: const Text('In den Warenkorb')),
],
),
);
if (isNarrow) {
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisSize: MainAxisSize.min,
children: [image, info],
);
}
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(width: 160, child: image),
Expanded(child: info),
],
);
},
),
);
}
}Die Karte weiß nicht, wo sie verwendet wird, und muss es auch nicht wissen: In einer Liste auf dem Smartphone bekommt sie 360 Pixel und wählt die vertikale Anordnung; in einem 700 Pixel breiten Bereich auf dem Tablet wechselt sie zur horizontalen; im 300 Pixel breiten Seitenpanel desselben Tablets kehrt sie zur vertikalen zurück. Mit MediaQuery geschrieben, würde der dritte Fall brechen. Im horizontalen Layout lässt Expanded den Text die vom Bild übrig gelassene Breite einnehmen; warum das nötig ist, erkläre ich im Beitrag zu Expanded.
Häufig gestellte Fragen
Was ist der Unterschied zwischen LayoutBuilder und MediaQuery?
MediaQuery liefert die Größe des Bildschirms (genauer: des App-Fensters), LayoutBuilder den Platz, den der Parent diesem Widget zugeteilt hat. Füllt das Widget nicht den ganzen Bildschirm, unterscheiden sich die Werte; entscheidet eine Komponente anhand ihres eigenen Platzes, ist LayoutBuilder das richtige Werkzeug.
Baut LayoutBuilder in jedem Frame neu?
Nein. Der Builder läuft nur, wenn sich die Constraints des Parents ändern oder der LayoutBuilder selbst neu gebaut wird. Vermeiden Sie trotzdem schwere Berechnungen im Builder; beim Skalieren eines Fensters kommen Constraint-Änderungen in schneller Folge.
Warum liefert constraints.maxHeight Infinity?
Weil der Parent auf dieser Achse keine Grenze gesetzt hat; Column, ListView und SingleChildScrollView geben entlang ihrer Hauptachse unbegrenzte Constraints weiter. Prüfen Sie hasBoundedHeight oder verschieben Sie den LayoutBuilder unter einen Parent mit bekannter Höhe.
Kann ich mit LayoutBuilder die Größe eines Kindes ermitteln?
Nein. LayoutBuilder sagt, wie viel Platz das Kind einnehmen darf, nicht, wie viel es am Ende belegt hat. Um die tatsächliche Größe eines Widgets zu lesen, braucht es andere Techniken, etwa den Zugriff auf seine RenderBox über einen GlobalKey, nachdem das Layout abgeschlossen ist.
Welche Breakpoint-Werte sollte ich verwenden?
Wenn Sie bei null anfangen, sind die Material-3-Fenstergrößenklassen eine gute Basis: unter 600 dp compact, 600 bis 839 medium, ab 840 expanded. Halten Sie die Werte in einer einzigen Funktion und entscheiden Sie anhand der Breite, die von LayoutBuilder kommt, nicht anhand des Bildschirms; so verhält sich dieselbe Komponente auch in einem schmalen Panel richtig.
Verwandte Artikel
Flutter: Bildschirmabhängige Skalierung mit MediaQuery
Größen nach Bildschirm mit MediaQuery.sizeOf, paddingOf und textScalerOf; wann LayoutBuilder oder FractionallySizedBox besser passt und häufige Fehler.
Flutter: BottomSheet und showModalBottomSheet
showModalBottomSheet in Flutter: Werte zurückgeben, isScrollControlled, useSafeArea, showDragHandle, Tastatur, DraggableScrollableSheet und persistente Sheets.
Flutter: Expanded, Flexible und Spacer im Vergleich
Der Unterschied zwischen Expanded, Flexible und Spacer, loose und tight fit, dazu vier Rezepte: Werkzeugleiste, Kartenfußzeile, Formular, geteilter Screen.