Skip to content

Kapitel 6: Flutter Layouts

6.1 Grundlegende Layout-Prinzipien

Alles ist ein Widget

In Flutter werden Layouts durch Widgets erreicht. Es gibt keine separaten Layout-Sprachen (wie HTML/CSS).

Layout-Achsen

Hauptachse (Main Axis):

  • MainAxisAlignment: Ausrichtung entlang der Hauptachse
  • MainAxisSize: Größe der Hauptachse

Querachse (Cross Axis):

  • CrossAxisAlignment: Ausrichtung entlang der Querachse
Row (Horizontal):
Hauptachse: Horizontal (von links nach rechts)
Querachse: Vertikal (von oben nach unten)

Column (Vertikal):
Hauptachse: Vertikal (von oben nach unten)
Querachse: Horizontal (von links nach rechts)

6.2 Lineare Layouts (Row & Column)

Row (Horizontales Layout)

dart
Row(
  mainAxisAlignment: MainAxisAlignment.spaceEvenly,  // Hauptachse
  crossAxisAlignment: CrossAxisAlignment.center,      // Querachse
  children: <Widget>[
    Icon(Icons.home),
    Text('Startseite'),
    ElevatedButton(onPressed: () {}, child: Text('Go')),
  ],
)

MainAxisAlignment-Optionen:

  • MainAxisAlignment.start: Am Anfang (links/oben)
  • MainAxisAlignment.end: Am Ende (rechts/unten)
  • MainAxisAlignment.center: Zentriert
  • MainAxisAlignment.spaceBetween: Gleichmäßiger Abstand (ohne Rand)
  • MainAxisAlignment.spaceAround: Gleichmäßiger Abstand (mit halbem Rand)
  • MainAxisAlignment.spaceEvenly: Vollständig gleicher Abstand

CrossAxisAlignment-Optionen:

  • CrossAxisAlignment.start: Am Anfang der Querachse
  • CrossAxisAlignment.end: Am Ende der Querachse
  • CrossAxisAlignment.center: Zentriert (Standard)
  • CrossAxisAlignment.stretch: Dehnt Kinder auf volle Querachsen-Breite

Column (Vertikales Layout)

dart
Column(
  mainAxisAlignment: MainAxisAlignment.center,
  crossAxisAlignment: CrossAxisAlignment.stretch,
  children: <Widget>[
    Text('Zeile 1'),
    Text('Zeile 2'),
    ElevatedButton(onPressed: () {}, child: Text('Button')),
  ],
)

Expanded (Platz füllen)

dart
Row(
  children: <Widget>[
    Expanded(
      flex: 2,  // 2/3 des verfügbaren Platzes
      child: Container(color: Colors.red, height: 50),
    ),
    Expanded(
      flex: 1,  // 1/3 des verfügbaren Platzes
      child: Container(color: Colors.blue, height: 50),
    ),
  ],
)

Spacer (Abstand füllen)

dart
Row(
  children: <Widget>[
    Text('Links'),
    const Spacer(),  // Drückt nachfolgende Widgets nach rechts
    Text('Rechts'),
  ],
)

6.3 Flex-Layout (Flex & Expanded)

Flex-Widget

Flex ist die Basis von Row und Column.

dart
Flex(
  direction: Axis.horizontal,  // Oder Axis.vertical
  mainAxisAlignment: MainAxisAlignment.spaceAround,
  children: <Widget>[
    // ...
  ],
)

Flexible (Flexible Größe)

dart
Row(
  children: <Widget>[
    Flexible(
      flex: 1,
      fit: FlexFit.loose,  // Kann schrumpfen (Standard)
      child: Container(color: Colors.red, height: 50),
    ),
    Flexible(
      flex: 2,
      fit: FlexFit.tight,  // Füllt verfügbaren Platz (wie Expanded)
      child: Container(color: Colors.blue, height: 50),
    ),
  ],
)

Unterschied: Expanded vs. Flexible

  • Expanded = Flexible mit fit: FlexFit.tight
  • Expanded füllt immer verfügbaren Platz
  • Flexible kann (standardmäßig) schrumpfen

6.4 Fluss-Layout (Wrap & Flow)

Wrap (Fluss-Layout)

Wrap erlaubt Zeilenumbruch, wenn der Platz nicht ausreicht.

dart
Wrap(
  spacing: 8.0,           // Horizontaler Abstand
  runSpacing: 4.0,        // Vertikaler Abstand
  alignment: WrapAlignment.center,
  children: <Widget>[
    Chip(label: Text('Flutter')),
    Chip(label: Text('Dart')),
    Chip(label: Text('Android')),
    Chip(label: Text('iOS')),
    Chip(label: Text('Web')),
  ],
)

Anwendungsfälle:

  • Tags/Chips anzeigen
  • Buttons in einer flexiblen Zeile
  • Überlauf vermeiden

Flow (Benutzerdefiniertes Fluss-Layout)

Fortgeschritten – meist nicht notwendig für Anfänger.

6.5 Stapel-Layout (Stack & Positioned)

Stack (Stapel-Container)

Stack erlaubt Widgets übereinander zu stapeln.

dart
Stack(
  alignment: Alignment.center,  // Standardausrichtung
  children: <Widget>[
    Container(width: 200, height: 200, color: Colors.red),
    Container(width: 150, height: 150, color: Colors.blue),
    Container(width: 100, height: 100, color: Colors.green),
  ],
)

Positioned (Positionierung in Stack)

dart
Stack(
  children: <Widget>[
    Container(width: 300, height: 300, color: Colors.grey.shade200),
    
    // Oben links
    const Positioned(
      top: 10,
      left: 10,
      child: Icon(Icons.home, size: 30),
    ),
    
    // Unten rechts
    const Positioned(
      bottom: 10,
      right: 10,
      child: Icon(Icons.settings, size: 30),
    ),
    
    // Zentriert
    const Center(
      child: Text('Zentriert', style: TextStyle(fontSize: 20)),
    ),
  ],
)

Anwendungsfälle:

  • Bilder mit Overlay-Text
  • Badge auf Icon
  • Floating Action Button positionieren

6.6 Card-Layout (Karte)

dart
Card(
  elevation: 4,              // Schattenstärke
  shape: RoundedRectangleBorder(
    borderRadius: BorderRadius.circular(12),
  ),
  child: Padding(
    padding: const EdgeInsets.all(16.0),
    child: Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: <Widget>[
        const Text('Kartentitel', style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold)),
        const SizedBox(height: 8),
        const Text('Dies ist der Inhalt der Karte.'),
        const SizedBox(height: 16),
        Row(
          mainAxisAlignment: MainAxisAlignment.end,
          children: <Widget>[
            TextButton(onPressed: () {}, child: const Text('Abbrechen')),
            ElevatedButton(onPressed: () {}, child: const Text('OK')),
          ],
        ),
      ],
    ),
  ),
)

6.7 Layout-Anpassung (Responsive Design)

MediaQuery (Bildschirmgröße abrufen)

dart
class ResponsivePage extends StatelessWidget {
  const ResponsivePage({super.key});

  @override
  Widget build(BuildContext context) {
    final size = MediaQuery.of(context).size;
    final width = size.width;
    
    return Scaffold(
      body: width > 600  // Tablet/Desktop
          ? _buildWideLayout()
          : _buildNarrowLayout(),  // Smartphone
    );
  }
  
  Widget _buildNarrowLayout() {
    return const Center(child: Text('Schmal (Smartphone)'));
  }
  
  Widget _buildWideLayout() {
    return const Center(child: Text('Breit (Tablet/Desktop)'));
  }
}

flutter_screenutil (Plugin empfohlen)

Installation (pubspec.yaml):

yaml
dependencies:
  flutter_screenutil: ^5.9.0

Verwendung:

dart
import 'package:flutter_screenutil/flutter_screenutil.dart';

// In MaterialApp initialisieren
MaterialApp(
  home: Scaffold(
    body: Builder(
      builder: (context) {
        // Initialize ScreenUtil
        ScreenUtil.init(context, designSize: const Size(360, 690));
        
        return Container(
          width: 300.w,   // .w = breite relativ zur Designgröße
          height: 200.h,  // .h = höhe relativ zur Designgröße
          padding: EdgeInsets.all(20.r),  // .r = radius/size relativ
          child: Text(
            'Adaptiver Text',
            style: TextStyle(fontSize: 24.sp),  // .sp = schriftgröße relativ
          ),
        );
      },
    ),
  ),
)

6.8 Praxisbeispiel: Startseiten-Layout erstellen

dart
import 'package:flutter/material.dart';

class HomePage extends StatelessWidget {
  const HomePage({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Startseite')),
      body: SingleChildScrollView(  // Scrollbar bei Überlauf
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.stretch,
          children: <Widget>[
            // Banner-Bereich
            Container(
              height: 200,
              decoration: const BoxDecoration(
                gradient: LinearGradient(
                  colors: [Colors.blue, Colors.purple],
                  begin: Alignment.topLeft,
                  end: Alignment.bottomRight,
                ),
              ),
              child: const Center(
                child: Text(
                  'Willkommen!',
                  style: TextStyle(fontSize: 32, color: Colors.white, fontWeight: FontWeight.bold),
                ),
              ),
            ),
            
            const SizedBox(height: 20),
            
            // Kategorien (horizontal scrollbar)
            SizedBox(
              height: 100,
              child: ListView(
                scrollDirection: Axis.horizontal,
                children: <Widget>[
                  _buildCategoryCard(Icons.code, 'Flutter'),
                  _buildCategoryCard(Icons.android, 'Android'),
                  _buildCategoryCard(Icons.phone_iphone, 'iOS'),
                  _buildCategoryCard(Icons.web, 'Web'),
                  _buildCategoryCard(Icons.desktop_windows, 'Desktop'),
                ],
              ),
            ),
            
            const SizedBox(height: 20),
            
            // Neuigkeiten-Bereich (Grid)
            Padding(
              padding: const EdgeInsets.all(16.0),
              child: GridView.count(
                shrinkWrap: true,  // WICHTIG: In Column verwendbar
                physics: const NeverScrollableScrollPhysics(),  // Scroll deaktivieren
                crossAxisCount: 2,  // 2 Spalten
                crossAxisSpacing: 10,
                mainAxisSpacing: 10,
                childAspectRatio: 0.75,  // Breite/Höhe Verhältnis
                children: <Widget>[
                  _buildNewsCard('Neuigkeit 1', Icons.article),
                  _buildNewsCard('Neuigkeit 2', Icons.article),
                  _buildNewsCard('Neuigkeit 3', Icons.article),
                  _buildNewsCard('Neuigkeit 4', Icons.article),
                ],
              ),
            ),
          ],
        ),
      ),
    );
  }
  
  // Hilfsfunktion: Kategorie-Karte
  Widget _buildCategoryCard(IconData icon, String label) {
    return Padding(
      padding: const EdgeInsets.symmetric(horizontal: 8.0),
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: <Widget>[
          CircleAvatar(
            radius: 30,
            backgroundColor: Colors.blue.shade100,
            child: Icon(icon, size: 30, color: Colors.blue),
          ),
          const SizedBox(height: 8),
          Text(label),
        ],
      ),
    );
  }
  
  // Hilfsfunktion: Neuigkeiten-Karte
  Widget _buildNewsCard(String title, IconData icon) {
    return Card(
      elevation: 4,
      shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
      child: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Icon(icon, size: 50, color: Colors.blue),
            const SizedBox(height: 16),
            Text(title, style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
            const SizedBox(height: 8),
            const Text('Kurze Beschreibung der Neuigkeit...', textAlign: TextAlign.center),
          ],
        ),
      ),
    );
  }
}

6.9 Häufige Fehler

Fehler 1: Layout-Überlauf (Renderflex overflowed)

Fehler:

════════ Exception caught by rendering library ═══════
A RenderFlex overflowed by 100 pixels on the right.

Ursache: Widget passt nicht in verfügbaren Platz.

Lösungen:

  1. Expanded verwenden
dart
Row(
  children: <Widget>[
    Expanded(child: Text('Sehr langer Text der überläuft...')),
    Icon(Icons.more_vert),
  ],
)
  1. SingleChildScrollView verwenden
dart
SingleChildScrollView(
  scrollDirection: Axis.horizontal,
  child: Row(...),
)
  1. Flexible verwenden
dart
Row(
  children: <Widget>[
    Flexible(child: Text('Langer Text...')),
    Icon(Icons.more_vert),
  ],
)

Fehler 2: Ausrichtung falsch

Fehler: Widgets sind nicht dort, wo sie sein sollten.

Prüfung:

dart
// Debugging: Rote Linien für Layout-Grenzen anzeigen
import 'package:flutter/rendering.dart';

void main() {
  debugPaintSizeEnabled = true;  // Aktiviert Visual Debugging
  runApp(MyApp());
}

Fehler 3: Zu tiefe Verschachtelung

Schlecht (zu tief verschachtelt):

dart
Column(
  children: <Widget>[
    Row(
      children: <Widget>[
        Column(
          children: <Widget>[
            Row(
              children: <Widget>[
                Text('Zu tief!'),
              ],
            ),
          ],
        ),
      ],
    ),
  ],
)

Besser: Widgets extrahieren

dart
class MyComplexWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Column(...);  // Einfacher zu verstehen
  }
}

Zusammenfassung

In diesem Kapitel haben Sie:

  • ✅ Grundlegende Layout-Prinzipien verstanden
  • ✅ Lineare Layouts (Row, Column) gemeistert
  • Expanded, Flexible, Spacer verwendet
  • ✅ Fluss-Layout (Wrap) zur Überlaufvermeidung gelernt
  • ✅ Stapel-Layout (Stack, Positioned) für Overlays verwendet
  • Card-Layout für schöne Karten gelernt
  • ✅ Responsive Design mit MediaQuery & flutter_screenutil implementiert
  • ✅ Ein komplexes Startseiten-Layout erstellt
  • ✅ Häufige Layout-Fehler behoben

Nächstes Kapitel: Wir werden Zustandsverwaltung lernen (StatefulWidget, setState, Provider).


Übungsaufgaben:

  1. Erstellen Sie eine Produktliste mit ListView und Card
  2. Erstellen Sie ein Responsives Layout, das auf Smartphone und Tablet unterschiedlich aussieht
  3. Verwenden Sie Stack, um ein Bild mit Text-Overlay zu erstellen
  4. Erstellen Sie eine Tag-Liste mit Wrap, die bei Überlauf umbricht
  5. Beheben Sie einen Layout-Überlauf-Fehler in einer bestehenden App

Häufige Fehler:

  • Expanded außerhalb von Row/Column verwenden → Expanded muss in Row/Column sein
  • ListView in Column ohne shrinkWrap: true verwenden → Überlauf!
  • Stack ohne Positioned verwenden und sich über Überlappung wundern
  • ❌ Zu tiefe Verschachtelung → Performance-Probleme & schlechte Lesbarkeit

Frei für alle Anfänger