Flutter: Flexible Abstände mit dem Spacer-Widget
Zuletzt aktualisiert:
8 Min. Lesezeit

Spacer öffnet in einer Row oder Column einen flexiblen, unsichtbaren Abstand zwischen Widgets. Anders als eine feste SizedBox wächst der Abstand mit breiterem Bildschirm und schrumpft mit schmalerem. Dieser Beitrag konzentriert sich nur auf Spacer: was in ihm steckt, wie er Abstände mit flex gewichtet, wann mainAxisAlignment oder SizedBox das bessere Werkzeug ist und warum er in scrollbaren Bereichen einen Fehler wirft. Wenn Sie zwischen Spacer, Expanded und Flexible abwägen, lesen Sie den Vergleich von Expanded, Flexible und Spacer; die Details der flex-Rechnung stehen im Expanded-Beitrag.
Was ist Spacer?
Der Quellcode von Spacer ist eine einzige Zeile: Die Methode build gibt Expanded(flex: flex, child: const SizedBox.shrink()) zurück. Ein Spacer ist also ein Expanded um eine Box der Größe null. Daraus folgen drei Dinge:
- Er muss ein direktes Kind einer Row, Column oder Flex sein, weil Expanded das sein muss.
- Er füllt seinen gesamten Anteil; der Anteil ist der Platz, den die Geschwister mit fester Größe übrig lassen.
- Er ist unsichtbar und hat keinen Parameter
child; Sie können ihm weder Farbe noch Rahmen noch eine Tippfläche geben.
Grundlegende Verwendung
Spacer in einer Row
Row(
children: [
Text('Links'),
Spacer(),
Text('Rechts'),
],
)Der Spacer füllt den gesamten freien Raum zwischen „Links“ und „Rechts“, sodass die beiden Texte an den gegenüberliegenden Enden sitzen.
Spacer in einer Column
Column(
children: [
Text('Oben'),
Spacer(),
Text('Unten'),
],
)Der gesamte vertikale Platz zwischen „Oben“ und „Unten“ wird zum Abstand. Dafür muss die Höhe der Column begrenzt sein; im Body eines Scaffold ist das kein Problem, in einem scrollbaren Bereich gibt es einen Fehler (siehe unten).
Ein einzelnes Element ans Ende schieben
Ein Spacer muss nicht zwischen zwei Widgets stehen. Setzen Sie ihn an den Anfang, schiebt er das folgende Element ans Ende der Zeile:
Row(
children: [
Spacer(),
Text('Rechts'),
],
)Dieser Code ist gültig und liefert dasselbe Ergebnis wie mainAxisAlignment: MainAxisAlignment.end. Der Vorteil von Spacer: Das Layout bleibt korrekt, wenn Sie später links ein Element ergänzen.
Mehrere Spacer
Mit mehreren Spacern verteilen Sie die Abstände gleichmäßig:
Row(
children: [
Text('Links'),
Spacer(),
Text('Mitte'),
Spacer(),
Text('Rechts'),
],
)Beide Spacer haben denselben flex-Wert (Standard 1) und teilen sich den freien Platz daher zur Hälfte. „Links“ und „Rechts“ liegen an den Rändern, „Mitte“ sitzt zwischen den beiden Lücken. „Mitte“ steht nicht unbedingt exakt mittig: Die Lücken sind gleich groß, aber wenn „Links“ und „Rechts“ unterschiedlich breit sind, verschiebt sich der mittlere Text leicht.
Der Parameter flex
Mit dem Parameter flex des Spacer stellen Sie das Verhältnis der Abstände ein; er funktioniert genauso wie das flex aus der offiziellen API-Dokumentation des Expanded-Widgets:
Row(
children: [
Text('Links'),
Spacer(flex: 1), // 1 Anteil
Text('Mitte'),
Spacer(flex: 2), // 2 Anteile
Text('Rechts'),
],
)Der zweite Spacer nimmt doppelt so viel Platz ein wie der erste: Der Abstand zwischen „Mitte“ und „Rechts“ ist doppelt so groß wie der zwischen „Links“ und „Mitte“. flex muss eine positive ganze Zahl sein; Spacer(flex: 0) löst im Debug-Modus einen Assertion-Fehler aus.
Flex-Verhältnisse
Column(
children: [
Container(height: 50, color: Colors.red),
Spacer(flex: 1),
Container(height: 50, color: Colors.blue),
Spacer(flex: 3),
Container(height: 50, color: Colors.green),
],
)Zuerst werden die 150 Pixel Gesamthöhe der drei Boxen abgezogen, dann wird der Restplatz in 1+3 = 4 Anteile geteilt: Der erste Abstand bekommt 25 % des Restplatzes, der zweite 75 %. Die Verhältnisse beziehen sich immer auf den Restplatz, nie auf die Gesamthöhe.
Praktische Beispiele
Beispiel 1: Ein Layout im Stil einer App Bar
Für eine echte obere Leiste gibt es natürlich das AppBar-Widget; hier geht es darum zu sehen, wie Spacer die Icons nach rechts schiebt, während der Titel links bleibt:
Row(
children: [
IconButton(icon: Icon(Icons.menu), onPressed: () {}),
Text('Titel', style: TextStyle(fontSize: 20)),
Spacer(),
IconButton(icon: Icon(Icons.search), onPressed: () {}),
IconButton(icon: Icon(Icons.more_vert), onPressed: () {}),
],
)Beispiel 2: Kartenfußzeile
Row(
children: [
Text('Preis: 9,99 €'),
Spacer(),
ElevatedButton(
onPressed: () {},
child: Text('Kaufen'),
),
],
)Beispiel 3: Einen Button nach unten schieben
Column(
children: [
Padding(
padding: EdgeInsets.all(16),
child: Text('Oberer Inhalt'),
),
Spacer(),
Padding(
padding: EdgeInsets.all(16),
child: ElevatedButton(
onPressed: () {},
child: Text('Unterer Button'),
),
),
],
)Ist diese Column der Body eines Scaffold, klebt der Button am unteren Bildschirmrand. Kann der Inhalt höher als der Bildschirm werden, reicht diese Struktur nicht; die scrollbare Variante folgt weiter unten.
Beispiel 4: Dreiteiliges Layout
Row(
children: [
Icon(Icons.arrow_back),
Spacer(),
Text('Titel'),
Spacer(),
Icon(Icons.settings),
],
)Die beiden Icons sind gleich breit, deshalb landet der Titel genau in der Mitte. Fügen Sie rechts ein zweites Icon hinzu, rutscht der Titel nach links; sind die Elemente an den Rändern unterschiedlich breit, zentrieren ein Stack oder die Eigenschaft centerTitle von AppBar zuverlässiger.
Spacer im Vergleich zu SizedBox
SizedBox: fester Abstand
Row(
children: [
Text('Links'),
SizedBox(width: 20), // Immer 20 Pixel
Text('Rechts'),
],
)- Abstand mit fester Größe
- Bleibt gleich, auch wenn sich die Bildschirmgröße ändert
- Funktioniert auch in scrollbaren Bereichen
- Details: Größen und Abstände mit SizedBox
Spacer: flexibler Abstand
Row(
children: [
Text('Links'),
Spacer(), // Füllt den gesamten Restplatz
Text('Rechts'),
],
)- Wächst und schrumpft mit dem Restplatz
- Funktioniert nur in einer Row oder Column mit begrenzter Größe
- Nützlich in responsiven Layouts
Faustregel: Gibt das Design einen Abstand zwischen Elementen vor (8, 12, 16 Pixel), nehmen Sie SizedBox; brauchen Sie einen Abstand nach dem Prinzip „was übrig bleibt“, nehmen Sie Spacer.
Spacer im Vergleich zu Expanded
Weil ein Spacer ein leeres Expanded ist, liefern die beiden folgenden Varianten dasselbe Ergebnis:
Mit Spacer
Row(
children: [
Text('Links'),
Spacer(),
Text('Rechts'),
],
)Das Gegenstück mit Expanded
Row(
children: [
Text('Links'),
Expanded(child: SizedBox.shrink()),
Text('Rechts'),
],
)Unterschied: Spacer ist lesbarer und zeigt die Absicht klar. Nehmen Sie Spacer nur für Abstände und Expanded für Inhalte.
Spacer und Expanded zusammen
Row(
children: [
Text('Fest'),
Spacer(flex: 1),
Expanded(
flex: 2,
child: Container(
color: Colors.blue,
child: Text('Wachsend'),
),
),
Spacer(flex: 1),
Text('Fest'),
],
)Nachdem die Breiten der beiden „Fest“-Texte abgezogen sind, wird der Restplatz in 1+2+1 = 4 Anteile geteilt: Die blaue Fläche bekommt die Hälfte, jeder Abstand ein Viertel. Weil Spacer und Expanded an derselben Verteilung teilnehmen, skalieren Abstände und Inhalt gemeinsam.
Vergleich mit mainAxisAlignment
In einfachen Fällen können Sie statt eines Spacer mainAxisAlignment verwenden:
mainAxisAlignment.spaceBetween
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text('Links'),
Text('Rechts'),
],
)Das Gegenstück mit Spacer
Row(
children: [
Text('Links'),
Spacer(),
Text('Rechts'),
],
)Unterschied: spaceBetween macht alle Lücken gleich groß; Spacer setzt den Abstand genau dorthin, wo Sie ihn haben wollen, und lässt sich mit flex gewichten. Kombinieren Sie beides nicht in einer Zeile: Laut offizieller Dokumentation nimmt ein Spacer den gesamten freien Platz ein, sodass spaceBetween, spaceAround und spaceEvenly keine sichtbare Wirkung haben, weil nichts mehr zu verteilen ist. Wie eine Row ihre eigene Breite wählt und wie das mit der Ausrichtung zusammenhängt, erklärt der Beitrag zu mainAxisSize.
Wo wirft Spacer einen Fehler?
1. Als direktes Kind einer ListView
// ❌ FALSCH: wirft einen Fehler
ListView(
children: [
Text('Oben'),
Spacer(),
Text('Unten'),
],
)Symptom: ein Fehler, der mit Incorrect use of ParentDataWidget. beginnt. Weil ein Spacer ein Expanded enthält, darf er nur direktes Kind einer Row, Column oder Flex sein; eine ListView ordnet ihre Kinder als scrollbare Liste an, nicht als Flex. Für Abstände zwischen Listeneinträgen nehmen Sie SizedBox(height: 16) oder ListView.separated.
2. In einer Column innerhalb einer SingleChildScrollView
// ❌ FALSCH: wirft einen Fehler
SingleChildScrollView(
child: Column(
children: [
Text('Oben'),
Spacer(),
Text('Unten'),
],
),
)Symptom: RenderFlex children have non-zero flex but incoming height constraints are unbounded. Hier steht der Spacer in einer Column, also am richtigen Ort; das Problem ist, dass der Scroll-Bereich der Column unendliche Höhe gibt und sich aus unendlich kein „Restplatz“ berechnen lässt. Reicht ein fester Abstand, nehmen Sie eine SizedBox. Warum scrollbare Strukturen unbegrenzte Höhe anbieten, erkläre ich im Beitrag zu ScrollView und Listen-Widgets:
// ✅ RICHTIG
SingleChildScrollView(
child: Column(
children: [
Text('Oben'),
SizedBox(height: 100), // Fester Abstand
Text('Unten'),
],
),
)3. In einem begrenzten Bereich kein Problem
// ✅ RICHTIG
SizedBox(
height: 300, // Begrenzte Höhe
child: Column(
children: [
Text('Oben'),
Spacer(),
Text('Unten'),
],
),
)Sobald die Höhe der Column feststeht, funktioniert der Spacer wieder.
Einen Button auf einer scrollbaren Seite unten halten
Ein häufiger Wunsch bei Registrierungs- und Login-Formularen: Ist der Inhalt kurz, soll der Button am unteren Bildschirmrand stehen; öffnet sich die Tastatur oder wächst der Inhalt, soll die Seite scrollen. Die Lösung aus der Dokumentation zu SingleChildScrollView gibt der Column die Regel „mindestens so hoch wie der sichtbare Bereich“:
LayoutBuilder(
builder: (context, constraints) {
return SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: ConstrainedBox(
constraints: BoxConstraints(minHeight: constraints.maxHeight - 32),
child: IntrinsicHeight(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const Text('Willkommen', style: TextStyle(fontSize: 24)),
const SizedBox(height: 12),
const Text('Tippen Sie unten auf den Button, um fortzufahren.'),
const Spacer(),
FilledButton(onPressed: () {}, child: const Text('Weiter')),
],
),
),
),
);
},
)LayoutBuilder liefert die Höhe des sichtbaren Bereichs, ConstrainedBox macht die Column mindestens so hoch (32 ist das obere und untere Padding zusammen), und IntrinsicHeight gibt der Column eine exakte Höhe und schafft damit einen „Restplatz“, den der Spacer berechnen kann. Wächst der Inhalt, wächst die Column mit, und das Scrollen funktioniert weiter. Die Dokumentation weist darauf hin, dass IntrinsicHeight teuer ist, weil der Inhalt zweimal angeordnet wird; nutzen Sie diese Struktur daher für kurze Formulare, nicht für lange Listen. Bei Elementen mit fester Höhe gibt es einen günstigeren Weg: IntrinsicHeight und Spacer weglassen und der Column mainAxisAlignment: MainAxisAlignment.spaceBetween geben.
Wann verwendet man es?
Spacer verwenden:
- Wenn der Abstand zwischen Elementen mit dem Bildschirm wachsen und schrumpfen soll
- Wenn Abstände in unterschiedlichen Verhältnissen verteilt werden sollen (
flex) - Wenn Sie eine Gruppe ans Ende einer Zeile oder Spalte schieben möchten
- Wenn Sie in einer Row oder Column mit begrenzter Größe arbeiten
Spacer nicht verwenden:
- Als direktes Kind einer ListView oder GridView
- In einer Column innerhalb einer
SingleChildScrollView(außer mit dem Muster oben) - In einer Zeile, die sich mit
mainAxisSize: MainAxisSize.minan ihren Inhalt legen soll, weil ein Spacer die Zeile auf volle Breite zieht - Wenn ein fester Abstand genügt (dann
SizedBox)
Zusammenfassung
Spacer:
- Ist ein leeres
Expanded:Expanded(flex: flex, child: const SizedBox.shrink()) - Erzeugt in Row und Column flexiblen Abstand, indem er einen Anteil am Restplatz nimmt
- Gewichtet Abstände über den Parameter
flex - Kann auch allein ein einzelnes Element ans Ende schieben
- Wirft Fehler in scrollbaren Bereichen mit unbegrenzter Höhe und in Eltern-Widgets ohne Flex
- Ist für Abstände gedacht; für Inhalte nehmen Sie Expanded
Häufig gestellte Fragen
Gibt es einen Performance-Unterschied zwischen Spacer und Expanded(child: SizedBox())?
Nein. Spacer enthält intern genau diese Struktur; der einzige Unterschied ist die Lesbarkeit. Nehmen Sie Spacer, um im Code klar zu zeigen, dass hier nur Abstand gemeint ist.
Warum bewirkt spaceBetween nichts, wenn ein Spacer da ist?
Der Spacer nimmt den gesamten freien Platz der Zeile als seinen Anteil. mainAxisAlignment verteilt nur den Platz, der nach dem Anordnen der Kinder übrig bleibt, und mit einem Spacer ist dieser Rest null. Entscheiden Sie sich: nur spaceBetween für gleiche Lücken, nur Spacer für einen Abstand an einer bestimmten Stelle oder mit Verhältnis.
Kann ich einem Spacer eine Farbe oder ein Padding geben?
Nein, Spacer hat keinen Parameter child und ist unsichtbar. Wollen Sie eine farbige oder dekorierte flexible Fläche, verwenden Sie Expanded(child: Container(color: ...)).
Verwandte Artikel
Flutter: Platz ausfüllen mit dem Expanded-Widget
Expanded im Detail: Wie der flex-Anteil berechnet wird, Verschachtelung, warum es nur in Row und Column funktioniert und die drei häufigsten Fehler.
Flutter: Platz und Ausrichtung mit mainAxisSize steuern
max und min bei mainAxisSize, der Einfluss der Constraints, warum mainAxisAlignment scheinbar nichts tut, und Rezepte für Dialoge, Sheets und Button-Gruppen.
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.