Flutter: Expanded, Flexible und Spacer im Vergleich
Zuletzt aktualisiert:
8 Min. Lesezeit

Für die Platzaufteilung in einer Row oder Column gibt es drei Werkzeuge: Expanded füllt den Restplatz, Flexible lässt ein Kind klein bleiben, Spacer öffnet nur eine Lücke. Dieser Beitrag zeigt zuerst an einer einzigen Zeile, wie sich die drei unterscheiden, und dann vier Alltagsszenarien, die sie in einem Layout kombinieren (Werkzeugleiste, Kartenfußzeile, Formularzeilen und ein responsiver geteilter Bildschirm), mit Code und Begründung. Die Details der einzelnen Widgets stehen in eigenen Beiträgen: flex-Rechnung, Verschachtelung und Expanded-Fehler im Expanded-Beitrag im Detail, Abstandsverhältnisse und die Fallen in scrollbaren Bereichen im Spacer-Beitrag.
Live-Demo
Sie können das Expanded-Widget im interaktiven Beispiel unten ausprobieren:
💡 Falls das Beispiel oben nicht lädt, klicken Sie auf DartPad, um es in einem neuen Tab auszuführen.
Der wesentliche Unterschied
Alle drei gehören zum selben Mechanismus: Eine Row oder Column misst zuerst ihre Kinder mit fester Größe und verteilt den Restplatz dann auf die Kinder mit flex-Wert. Der Unterschied liegt darin, was das Kind mit seinem Anteil macht.
Expanded: Das Kind muss seinen Anteil vollständig ausfüllen. Es ist die Kurzform vonFlexible(fit: FlexFit.tight).Flexible: Der Standard-fitistFlexFit.loose, das Kind darf also „höchstens“ seinen Anteil einnehmen. Ist es selbst kleiner, bleibt es klein; passt es nicht, wird es in den Anteil gezwängt.Spacer: EinExpandedohne Inhalt; er nutzt seinen Anteil als unsichtbaren Abstand und trägt keinen Inhalt.
Um den Unterschied zwischen Flexible und Expanded zu sehen, braucht das Kind eine eigene Größe. Ein kurzes Label genügt:
Row(
children: [
Flexible(
child: Container(
color: Colors.green,
padding: const EdgeInsets.all(8),
child: const Text('Kurzes Label'),
),
),
const SizedBox(width: 8),
const Icon(Icons.info_outline),
],
)In dieser Zeile ist die grüne Fläche so breit wie der Text, und das Icon sitzt direkt daneben. Machen Sie den Text zu lang für die Zeile, begrenzt Flexible ihn auf die Restbreite, und der Text bricht in eine zweite Zeile um, statt überzulaufen. Schreiben Sie im selben Code Expanded statt Flexible, reicht die grüne Fläche auch beim kurzen Text bis zum Icon, und das Icon wird an den rechten Rand geschoben.
Entscheidungstabelle
Vor den Rezepten der kurze Weg zur Entscheidung, welches Widget an welches Kind kommt:
| Bedarf | Werkzeug |
|---|---|
| Das Kind soll den gesamten Restplatz füllen | Expanded |
| Das Kind behält seine Größe, schrumpft aber, wenn es nicht passt | Flexible |
| Flexible Lücke zwischen zwei Elementen | Spacer |
| Feste Lücke zwischen zwei Elementen | SizedBox |
| Elemente sollen in die nächste Zeile umbrechen | Wrap |
Rezept 1: Werkzeugleiste
Stellen Sie sich die untere Leiste eines Editor-Bildschirms vor: links zwei Icons, rechts ein „Speichern“-Button. Icons und Button behalten ihre Größe, die Lücke dazwischen soll flexibel sein:
Row(
children: [
IconButton(onPressed: () {}, icon: const Icon(Icons.undo)),
IconButton(onPressed: () {}, icon: const Icon(Icons.redo)),
const Spacer(),
FilledButton(onPressed: () {}, child: const Text('Speichern')),
],
)Spacer ließe sich hier durch mainAxisAlignment.spaceBetween ersetzen, aber spaceBetween macht jede Lücke gleich groß; sollen die beiden Icons nebeneinander bleiben, ist Spacer das richtige Werkzeug. Für einen Titel in der Mitte setzen Sie statt des Spacer ein Expanded(child: Text(title, textAlign: TextAlign.center, overflow: TextOverflow.ellipsis)): Der Titel nimmt den Restplatz und wird bei Überlänge mit Auslassungspunkten gekürzt.
Rezept 2: Kartenfußzeile
Die untere Zeile einer Produktkarte: links der Preis, rechts ein Favoriten-Icon und ein „In den Warenkorb“-Button:
Row(
children: [
Text('24,90 €', style: Theme.of(context).textTheme.titleMedium),
const Spacer(),
IconButton(onPressed: () {}, icon: const Icon(Icons.favorite_border)),
FilledButton.tonal(onPressed: () {}, child: const Text('In den Warenkorb')),
],
)Diese Zeile kann auf schmalen Bildschirmen überlaufen, wenn Preis und Button-Text lang sind. Weil Spacer ein flex-Kind ist, kann er bis auf null schrumpfen; der Overflow beginnt, sobald die festen Kinder zusammen breiter sind als die Row. Dann packen Sie den Preis in Flexible mit overflow: TextOverflow.ellipsis oder ersetzen die Zeile durch Wrap. Die Kartenkomponente selbst behandelt der Beitrag zur Verwendung von Card.
Rezept 3: Formularzeilen
Im Adressformular stehen „Stadt“ und „Postleitzahl“ in einer Zeile, das Stadtfeld doppelt so breit:
Row(
children: [
Expanded(
flex: 2,
child: TextFormField(
decoration: const InputDecoration(labelText: 'Stadt'),
),
),
const SizedBox(width: 12),
Expanded(
child: TextFormField(
keyboardType: TextInputType.number,
decoration: const InputDecoration(labelText: 'Postleitzahl'),
),
),
],
)Textfelder in einer Row müssen Expanded sein; lassen Sie sie nackt, wirft die App einen Fehler (siehe unten). Die Lücke dazwischen ist fest, also SizedBox, nicht Spacer. Validierung und das Form-Widget erklärt der Beitrag zur Verwendung von Form.
Rezept 4: Responsiver geteilter Bildschirm
Auf Tablet und Desktop links eine Liste, rechts die Detailansicht; auf dem Telefon nur die Liste. Die Entscheidung sollte von der Breite abhängen, die dieses Widget bekommt, nicht von der Bildschirmbreite; dafür gibt es LayoutBuilder:
LayoutBuilder(
builder: (context, constraints) {
if (constraints.maxWidth < 720) {
return const NoteList();
}
return const Row(
children: [
SizedBox(width: 320, child: NoteList()),
VerticalDivider(width: 1),
Expanded(child: NoteDetail()),
],
);
},
)Die Liste ist fest 320 Pixel breit, die Detailansicht bekommt alles Übrige. Soll die Liste ebenfalls anteilig wachsen, ersetzen Sie die SizedBox durch Expanded(flex: 1) und geben der Detailansicht flex: 2. Die Wahl zwischen LayoutBuilder und MediaQuery behandle ich im Beitrag zu MediaQuery, ein vollständiges responsives Layout mit Breakpoints im Beitrag zu LayoutBuilder.
Wann verwenden – und wann nicht?
Die drei sind in jeder Row und Column, in der Platz zwischen Geschwistern aufgeteilt wird, das richtige Werkzeug. In drei Situationen brauchen Sie aber etwas anderes:
- Steckt die Row oder Column in einer
SingleChildScrollView, hat die Hauptachse keine Grenze;ExpandedundSpacerfunktionieren dort nicht. Geben Sie eine feste Größe oder verlagern Sie das Scrollen aus dem äußeren Widget heraus. - Bei Chips und Tags mit unbekannter Anzahl reicht selbst
Flexiblenicht; die Elemente müssen in die nächste Zeile. Nehmen SieWrap. - Soll sich die Row eng an ihren Inhalt legen (
mainAxisSize: MainAxisSize.min), machenExpandedundSpacerdiese Absicht zunichte, weil beide den gesamten Restplatz fordern. In solchen Zeilen nurFlexibleundSizedBoxverwenden. Wie eine Row ihre eigene Breite wählt, erklärt der Beitrag zu mainAxisSize.
Häufige Fehler
1. Ein nacktes TextField in einer Row
Symptom: An InputDecorator, which is typically created by a TextField, cannot have an unbounded width. Eine Row gibt ihren Kindern keine Breitengrenze, und das Textfeld weiß nicht, wie breit es sein soll. Die Lösung: das TextField in Expanded oder Flexible packen. Wollen Sie eine feste Breite, geht auch SizedBox(width: 120, child: TextField(...)).
2. Flexible schreiben und erwarten, dass es füllt
Symptom: Sie haben Flexible(child: Container(color: Colors.blue, child: Text('A'))) geschrieben, und die blaue Fläche ist nur so breit wie der Text. Der Standard-fit von Flexible ist FlexFit.loose: Das Kind muss nicht wachsen, und der Container schmiegt sich an sein Kind. Soll es füllen, nehmen Sie Expanded oder setzen fit: FlexFit.tight.
3. Flexible mit einem Container ohne Kind testen
Symptom: Sie haben Flexible(child: Container(color: Colors.green)) geschrieben, das Ergebnis sieht genauso aus wie mit Expanded, und Sie schließen daraus, dass es keinen Unterschied gibt. Es gibt einen, aber dieses Beispiel kann ihn nicht zeigen. Ein Container ohne Kind und ohne Größe wird so groß wie möglich, wenn er begrenzte Constraints bekommt. Ein loose-Constraint sagt „höchstens so viel“, also wählt der Container einfach die größte erlaubte Größe und füllt den ganzen Anteil. Um den Unterschied zu sehen, nehmen Sie ein Kind mit eigener Größe, wie im Label-Beispiel oben.
4. Spacer in einem scrollbaren Bereich
SingleChildScrollView(
child: Column(
children: const [
Text('Oben'),
Spacer(), // Fehler
Text('Unten'),
],
),
)Symptom: RenderFlex children have non-zero flex but incoming height constraints are unbounded. Im Scroll-Bereich ist die Höhe unendlich, „Restplatz“ ist undefiniert. Verwenden Sie eine feste SizedBox(height: 24); wollen Sie das untere Element wirklich am unteren Bildschirmrand festhalten, nehmen Sie die Column aus dem Scroll-Bereich und machen den Inhaltsteil zu Expanded(child: SingleChildScrollView(...)).
5. Einsatz in einem Eltern-Widget ohne Flex
Alle drei dürfen nur direkte Kinder einer Row, Column oder Flex sein. In einem Stack, Container oder Padding erhalten Sie Incorrect use of ParentDataWidget. Am häufigsten rutscht ein Padding zwischen Expanded und seine Row; die Lösung (Reihenfolge umdrehen) beschreibe ich ausführlich im Expanded-Beitrag.
Mini-Szenario: Zeile einer Produktliste
Eine Produktzeile, die die vier Rezepte zusammenführt. Links ein Vorschaubild, in der Mitte Titel und Beschreibung, rechts der Preis; darunter ein Favoriten-Icon und ein Warenkorb-Button:
class ProductRow extends StatelessWidget {
const ProductRow({
super.key,
required this.title,
required this.subtitle,
required this.price,
});
final String title;
final String subtitle;
final String price;
@override
Widget build(BuildContext context) {
final text = Theme.of(context).textTheme;
return Card(
child: Padding(
padding: const EdgeInsets.all(12),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(width: 64, height: 64, child: Placeholder()),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(title, style: text.titleMedium, maxLines: 1, overflow: TextOverflow.ellipsis),
Text(subtitle, style: text.bodySmall, maxLines: 2, overflow: TextOverflow.ellipsis),
],
),
),
const SizedBox(width: 12),
Text(price, style: text.titleMedium),
],
),
const SizedBox(height: 8),
Row(
children: [
const Spacer(),
IconButton(onPressed: () {}, icon: const Icon(Icons.favorite_border)),
FilledButton.tonal(onPressed: () {}, child: const Text('In den Warenkorb')),
],
),
],
),
),
);
}
}In der oberen Zeile sind Bild und Preis fest, die mittlere Column nimmt über Expanded die gesamte Breite dazwischen; ein langer Titel wird mit Auslassungspunkten gekürzt statt überzulaufen, weil Expanded dem Text eine begrenzte Breite gibt. Die Lücken sind fest, also SizedBox. In der unteren Zeile schiebt der Spacer Icon und Button nach rechts; da links nichts steht, ergäbe mainAxisAlignment.end dasselbe Ergebnis, aber Spacer funktioniert ohne Änderung weiter, wenn Sie später links ein „Auf Lager“-Label ergänzen. Die äußere Column der Karte ist mainAxisSize.min, deshalb enthält sie kein Expanded; sonst würde die Karte in einer Listenzeile einen Fehler wegen unendlicher Höhe werfen.
Häufig gestellte Fragen
Warum wirft ein TextField in einer Row einen Fehler?
Eine Row gibt ihren Kindern keine Breitengrenze, und ein Textfeld kann keine unbegrenzte Breite haben. Packen Sie das TextField in Expanded oder Flexible oder geben Sie ihm mit SizedBox eine feste Breite.
Spacer oder mainAxisAlignment?
Sollen alle Lücken gleich groß sein, reicht spaceBetween oder spaceEvenly. Muss die Lücke an einer bestimmten Stelle sitzen oder in unterschiedlichen Verhältnissen verteilt werden, nehmen Sie Spacer(flex:). Kombinieren Sie beides nicht in einer Zeile: Der Spacer nimmt den gesamten freien Platz, deshalb hat spaceBetween keine sichtbare Wirkung.
Wann ist Flexible gegenüber Expanded vorzuziehen?
Wenn das Kind seine eigene Größe behalten, aber bei Platzmangel schrumpfen soll. In Layouts, die sich an den Inhalt legen, etwa Chat-Blasen und Chips, ist Flexible richtig; wo der gesamte Restplatz gefüllt werden muss, ist Expanded die Wahl.
Warum funktionieren diese Widgets nicht in einer ScrollView?
Ein Scroll-Bereich bietet auf der Hauptachse unendlichen Platz, und „Restplatz“ lässt sich nicht berechnen. Geben Sie eine feste Größe oder verlagern Sie das Scrollen in den inneren, mit Expanded umschlossenen Teil statt um die Column herum.
Verwandte Artikel
Flutter: GridView-Widget verwenden
Flutter-GridView-Leitfaden: count, extent und builder, die beiden Grid-Delegates, der childAspectRatio-Overflow-Fehler und die wahren Kosten von shrinkWrap.
Flutter: Flexible Abstände mit dem Spacer-Widget
Was in Spacer steckt, Abstände mit flex gewichten, der Unterschied zu SizedBox und mainAxisAlignment und warum Spacer in scrollbaren Bereichen scheitert.
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.