İçeriğe geç / Skip to content / Zum Inhalt

Flutter: GridView-Widget verwenden

Ahmet Balaman

Zuletzt aktualisiert:

7 Min. Lesezeit

FlutterGridViewWidgetGridLayoutResponsive
Flutter: GridView-Widget verwenden

GridView ordnet Elemente in einem Raster aus Zeilen und Spalten an und macht scrollbar, was nicht auf den Bildschirm passt. Sie begegnen ihm überall dort, wo gleich große Kacheln nebeneinanderstehen: Fotogalerie, Produktkatalog, Kategorieauswahl. Man kann es als Raster-Variante der ListView sehen: dieselbe Konstruktor-Logik, dasselbe Lazy Loading, dazu ein Delegate, das die Größe jeder Zelle festlegt. Dieses Delegate und das Zellenverhältnis sind das eigentliche Thema dieses Beitrags, denn die meisten GridView-Fehler entstehen genau dort.

Live-Demo

Sie können dieses 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.

Grundlegende Verwendung: GridView.count

Der kürzeste Weg ist, die Spaltenanzahl anzugeben. Mit crossAxisCount: 2 teilt Flutter die verfügbare Breite in zwei Hälften und macht jede Zelle quadratisch:

GridView.count(
  crossAxisCount: 2, // 2 Spalten
  children: [
    Container(color: Colors.red, child: const Center(child: Text('1'))),
    Container(color: Colors.green, child: const Center(child: Text('2'))),
    Container(color: Colors.blue, child: const Center(child: Text('3'))),
    Container(color: Colors.orange, child: const Center(child: Text('4'))),
  ],
)

Wie bei ListView erzeugt der children-Konstruktor alle Zellen im Voraus. Für ein von Hand geschriebenes Menü mit zehn oder fünfzehn Zellen ist das in Ordnung; kommen die Daten aus einer Liste, wechseln Sie zum builder-Konstruktor weiter unten.

Wichtige Eigenschaften

Eigenschaft Beschreibung
crossAxisCount Spaltenanzahl (count-Konstruktor und FixedCrossAxisCount-Delegate)
maxCrossAxisExtent Maximale Breite einer Zelle (extent-Konstruktor und MaxCrossAxisExtent-Delegate)
mainAxisSpacing Abstand zwischen den Zeilen
crossAxisSpacing Abstand zwischen den Spalten
childAspectRatio Zellenbreite ÷ Höhe, Standard 1
mainAxisExtent Legt die Zellenhöhe in Pixeln fest statt über ein Verhältnis
padding Abstand rund um das Raster
shrinkWrap / physics Für verschachteltes Scrollen; die Kosten erkläre ich unten

GridView.extent mit maximaler Breite

Statt einer Spaltenanzahl sagen Sie „eine Zelle darf höchstens so breit sein“, und Flutter berechnet, wie viele Spalten passen. Drehen Sie das Telefon oder öffnen Sie die App auf einem Tablet, erscheinen von selbst mehr Spalten:

GridView.extent(
  maxCrossAxisExtent: 150, // Jede Zelle ist höchstens 150 Pixel breit
  mainAxisSpacing: 8,
  crossAxisSpacing: 8,
  padding: const EdgeInsets.all(8),
  children: [
    Container(color: Colors.purple, child: const Center(child: Text('A'))),
    Container(color: Colors.pink, child: const Center(child: Text('B'))),
    Container(color: Colors.cyan, child: const Center(child: Text('C'))),
    Container(color: Colors.amber, child: const Center(child: Text('D'))),
  ],
)

Lazy Loading mit GridView.builder

Kommen die Daten aus einer Liste, verwenden Sie GridView.builder. itemBuilder wird nur für sichtbare Zellen aufgerufen; ob 20 oder 20.000 Elemente, gleichzeitig existieren ungefähr so viele Zellen, wie auf den Bildschirm passen. Bei diesem Konstruktor bestimmt ein gridDelegate das Spaltenlayout:

GridView.builder(
  gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
    crossAxisCount: 3,
    mainAxisSpacing: 8,
    crossAxisSpacing: 8,
  ),
  itemCount: 20,
  padding: const EdgeInsets.all(8),
  itemBuilder: (context, index) {
    return Container(
      decoration: BoxDecoration(
        color: Colors.primaries[index % Colors.primaries.length],
        borderRadius: BorderRadius.circular(8),
      ),
      child: Center(
        child: Text(
          '${index + 1}',
          style: const TextStyle(color: Colors.white, fontSize: 24),
        ),
      ),
    );
  },
)

Zwei Delegates: FixedCrossAxisCount und MaxCrossAxisExtent

Für gridDelegate gibt es zwei fertige Klassen, und der Unterschied ist genau der zwischen count und extent:

  • SliverGridDelegateWithFixedCrossAxisCount: Die Spaltenanzahl ist fest, die Zellenbreite folgt dem Bildschirm. Wählen Sie das, wenn das Design etwas Eindeutiges wie „immer zwei Spalten“ vorgibt.
  • SliverGridDelegateWithMaxCrossAxisExtent: Sie begrenzen die Zellenbreite, die Spaltenanzahl folgt dem Bildschirm. Das ist die bequeme Wahl, wenn ein Code auf Telefon, Tablet und Web gut aussehen soll; Sie bekommen ein vernünftiges responsives Verhalten, ohne einen eigenen LayoutBuilder-Breakpoint zu schreiben.

FixedCrossAxisCount behält bei jeder Breite zwei Spalten, MaxCrossAxisExtent zeigt schmal zwei und breit drei Spalten

GridView.builder(
  gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
    maxCrossAxisExtent: 200,
    mainAxisSpacing: 10,
    crossAxisSpacing: 10,
    childAspectRatio: 1.5,
  ),
  itemCount: 12,
  itemBuilder: (context, index) {
    return Card(
      color: Colors.teal.shade100,
      child: Center(child: Text('Element $index')),
    );
  },
)

Wie childAspectRatio berechnet wird

In einer GridView ergibt sich die Zellenhöhe nicht aus dem Inhalt, sondern aus einem Verhältnis. Flutter bestimmt zuerst die Zellenbreite: verfügbare Breite minus Padding und Spaltenabstände, geteilt durch die Spaltenanzahl. Dann berechnet es die Höhe als Breite ÷ childAspectRatio. childAspectRatio: 16 / 9 ergibt also breite, flache Karten, 0.8 hochkant stehende:

GridView.count(
  crossAxisCount: 2,
  childAspectRatio: 16 / 9, // Breite Karten
  mainAxisSpacing: 8,
  crossAxisSpacing: 8,
  children: [
    Container(color: Colors.red),
    Container(color: Colors.blue),
    Container(color: Colors.green),
    Container(color: Colors.orange),
  ],
)

Was in der Zelle steht, spielt bei dieser Rechnung keine Rolle. Das ist der am häufigsten missverstandene Punkt an GridView und die Ursache des unten beschriebenen Overflow-Fehlers. Kennen Sie die Höhe in Pixeln, sparen Sie sich das Verhältnis und übergeben mainAxisExtent: 180; das Delegate ignoriert dann das Verhältnis und macht jede Zelle 180 Pixel hoch.

Individuell gestaltete Rasterkarten

Zellen mit Icon, Titel, Preis und Button, wie eine Produktkarte, brauchen ein Hochformat. Das Beispiel unten verwendet childAspectRatio: 0.8, damit die Zelle etwas höher als breit ist; den Titel mit maxLines zu begrenzen ist eine gute Gewohnheit, damit lange Namen nicht überlaufen:

final List<Map<String, dynamic>> products = [
  {'name': 'Produkt 1', 'price': '99 €', 'icon': Icons.phone_android},
  {'name': 'Produkt 2', 'price': '149 €', 'icon': Icons.laptop},
  {'name': 'Produkt 3', 'price': '299 €', 'icon': Icons.watch},
  {'name': 'Produkt 4', 'price': '79 €', 'icon': Icons.headphones},
];

GridView.builder(
  gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
    crossAxisCount: 2,
    mainAxisSpacing: 12,
    crossAxisSpacing: 12,
    childAspectRatio: 0.8,
  ),
  padding: const EdgeInsets.all(16),
  itemCount: products.length,
  itemBuilder: (context, index) {
    final product = products[index];
    return Card(
      elevation: 4,
      shape: RoundedRectangleBorder(
        borderRadius: BorderRadius.circular(12),
      ),
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          Icon(product['icon'], size: 48, color: Colors.blue),
          const SizedBox(height: 12),
          Text(
            product['name'],
            maxLines: 1,
            overflow: TextOverflow.ellipsis,
            style: const TextStyle(fontWeight: FontWeight.bold),
          ),
          const SizedBox(height: 4),
          Text(
            product['price'],
            style: const TextStyle(color: Colors.green, fontSize: 18),
          ),
          const SizedBox(height: 8),
          ElevatedButton(
            onPressed: () {},
            child: const Text('Kaufen'),
          ),
        ],
      ),
    );
  },
)

Für einen Galerie-Screen reichen quadratische Zellen und BoxFit.cover:

GridView.builder(
  gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
    crossAxisCount: 3,
    mainAxisSpacing: 4,
    crossAxisSpacing: 4,
  ),
  itemCount: 30,
  itemBuilder: (context, index) {
    return Image.network(
      'https://picsum.photos/200?random=$index',
      fit: BoxFit.cover,
    );
  },
)

Wann verwenden – und wann nicht?

Greifen Sie zu GridView, wenn Sie gleich große, gleichartige Zellen in einer Anzahl haben, die nicht auf den Bildschirm passt: Galerie, Katalog, Icon-Palette. Kommen die Daten aus einer Collection, ist builder der richtige Einstieg; soll die Spaltenanzahl dem Bildschirm folgen, das MaxCrossAxisExtent-Delegate.

In diesen Fällen passt ein anderes Widget besser:

  • Sind die Elemente unterschiedlich groß und nur wenige (Tag-Wolke, Filter-Chips), nehmen Sie Wrap; nichts scrollt, und jedes Element bekommt seine eigene Größe.
  • Stehen die Elemente einfach zeilenweise untereinander, braucht es kein Raster; eine ListView ist einfacher.
  • Muss das Raster zusammen mit einer Überschrift, einer horizontalen Liste oder einer einklappenden AppBar auf demselben Screen scrollen, verwenden Sie SliverGrid in einer CustomScrollView. Diese Entscheidungen zum verschachtelten Scrollen behandle ich im Beitrag zu SingleChildScrollView, ListView und NestedScrollView.
  • Sollen Zellen je nach Inhalt unterschiedlich hoch sein (Masonry-Layout im Pinterest-Stil), unterstützt GridView das nicht; dafür brauchen Sie ein Paket wie flutter_staggered_grid_view.

Häufige Fehler

1. childAspectRatio nicht an den Inhalt anpassen

Symptom: gelb-schwarze Streifen am unteren Rand der Zellen und diese Meldung in der Konsole:

A RenderFlex overflowed by 18 pixels on the bottom.

Die Zellenhöhe kommt aus dem Verhältnis, und der Inhalt ist höher. Es gibt drei Lösungen: das Verhältnis verkleinern (etwa childAspectRatio: 0.75, die Zelle wird höher), die Höhe mit mainAxisExtent in Pixeln festlegen oder die Column in der Zelle flexibel machen. Für die dritte Variante umschließen Sie den Text mit Expanded oder Flexible und geben ihm maxLines:

Column(
  children: [
    const Icon(Icons.image, size: 40),
    Expanded(
      child: Text(
        product['name'],
        maxLines: 2,
        overflow: TextOverflow.ellipsis,
      ),
    ),
    Text(product['price']),
  ],
)

Derselbe Overflow taucht auf, wenn der Nutzer die Systemschriftgröße vergrößert. Planen Sie die Zelle deshalb mit etwas Luft statt auf Passgenauigkeit.

2. Eine GridView direkt in eine Column setzen

Symptom: Der Screen bleibt leer, und die Konsole zeigt Vertical viewport was given unbounded height. Genau wie ListView möchte eine GridView alle Höhe nehmen, die sie findet, während eine Column unbegrenzte Höhe anbietet. Die Lösung ist, die GridView mit Expanded zu umschließen:

Column(
  children: [
    const Padding(
      padding: EdgeInsets.all(16),
      child: Text('Kategorien'),
    ),
    Expanded(
      child: GridView.count(
        crossAxisCount: 3,
        children: const [/* ... */],
      ),
    ),
  ],
)

3. shrinkWrap und NeverScrollableScrollPhysics aus Gewohnheit

Steht die GridView in einer SingleChildScrollView, hilft Expanded nicht; dann brauchen Sie dieses Paar:

GridView.builder(
  shrinkWrap: true,
  physics: const NeverScrollableScrollPhysics(),
  // ...
)

shrinkWrap: true zwingt das Raster, jede Zelle zu messen, um seine eigene Höhe zu kennen, und NeverScrollableScrollPhysics überlässt das Scrollen dem äußeren Widget. Die Folge: Alle Zellen werden auf einmal gebaut, und builder verliert sein Lazy Loading. Für einen Kategorieblock mit zehn oder fünfzehn Zellen ist das akzeptabel. Für Hunderte Produkte nicht; dort kombinieren Sie den Inhalt darüber und das Raster in einer CustomScrollView als SliverToBoxAdapter plus SliverGrid:

CustomScrollView(
  slivers: [
    const SliverToBoxAdapter(
      child: Padding(
        padding: EdgeInsets.all(16),
        child: Text('Empfohlene Produkte'),
      ),
    ),
    SliverGrid(
      gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
        crossAxisCount: 2,
      ),
      delegate: SliverChildBuilderDelegate(
        (context, index) => Card(child: Center(child: Text('Produkt $index'))),
        childCount: 200,
      ),
    ),
  ],
)

4. Eine feste Spaltenanzahl auf jedem Bildschirm

Symptom: Das crossAxisCount: 2, das auf dem Telefon gut aussieht, erzeugt auf dem Tablet riesige Karten über die halbe Bildschirmbreite; umgekehrt liefern die für das Tablet gewählten 4 Spalten auf dem Telefon unlesbar kleine Zellen. Wechseln Sie entweder zum MaxCrossAxisExtent-Delegate oder leiten Sie die Spaltenanzahl aus der Breite ab, etwa (constraints.maxWidth / 180).floor().clamp(2, 6).

Mini-Szenario: Ein Screen zur Kategorieauswahl

Stellen Sie sich den Screen einer Shopping-App vor, auf dem Nutzer ihre Interessen wählen: Karten mit Icon und Name, ein Tipp schaltet die Auswahl um, und der Titel zeigt, wie viele gewählt sind. Die Karten sind gleich groß und ihre Anzahl ist nicht fest, also GridView.builder; der Screen soll auch auf dem Tablet stimmen, also MaxCrossAxisExtent; und die Höhe ist aus dem Inhalt bekannt, also mainAxisExtent.

class CategoryPickerPage extends StatefulWidget {
  const CategoryPickerPage({super.key});

  @override
  State<CategoryPickerPage> createState() => _CategoryPickerPageState();
}

class _CategoryPickerPageState extends State<CategoryPickerPage> {
  static const _categories = [
    ('Elektronik', Icons.devices),
    ('Bücher', Icons.menu_book),
    ('Sport', Icons.sports_soccer),
    ('Mode', Icons.checkroom),
    ('Wohnen', Icons.chair),
    ('Gaming', Icons.sports_esports),
  ];

  final Set<int> _selected = {};

  @override
  Widget build(BuildContext context) {
    final colorScheme = Theme.of(context).colorScheme;

    return Scaffold(
      appBar: AppBar(title: Text('${_selected.length} Kategorien gewählt')),
      body: GridView.builder(
        padding: const EdgeInsets.all(16),
        gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
          maxCrossAxisExtent: 160,
          mainAxisExtent: 120,
          mainAxisSpacing: 12,
          crossAxisSpacing: 12,
        ),
        itemCount: _categories.length,
        itemBuilder: (context, index) {
          final (name, icon) = _categories[index];
          final isSelected = _selected.contains(index);

          return Card(
            color: isSelected ? colorScheme.primaryContainer : null,
            child: InkWell(
              borderRadius: BorderRadius.circular(12),
              onTap: () {
                setState(() {
                  isSelected ? _selected.remove(index) : _selected.add(index);
                });
              },
              child: Column(
                mainAxisAlignment: MainAxisAlignment.center,
                children: [
                  Icon(icon, size: 36),
                  const SizedBox(height: 8),
                  Text(name, maxLines: 1, overflow: TextOverflow.ellipsis),
                ],
              ),
            ),
          );
        },
      ),
    );
  }
}

Hier stecken drei Entscheidungen. Dank maxCrossAxisExtent: 160 zeigt ein Telefon zwei Spalten und ein Tablet vier oder fünf, ganz ohne Zusatzcode. mainAxisExtent: 120 macht die Zellenhöhe unabhängig von der Bildschirmbreite; mit einem Verhältnis würden die Karten auf einem breiten Bildschirm unnötig hoch. Die Auswahl liegt in einem Set<int>, und setState aktualisiert nur diesen Screen; brauchen andere Screens die gewählten Kategorien ebenfalls, verschieben Sie diesen State in etwas wie Provider. Und kommen die Kategorien aus einer API, wandert dasselbe Raster einfach in einen FutureBuilder.

Häufig gestellte Fragen

Was ist der Unterschied zwischen GridView.count und GridView.builder?

GridView.count nimmt eine fertige children-Liste und baut alle Zellen im Voraus; GridView.builder ruft itemBuilder nur für sichtbare Zellen auf. Das erste eignet sich für kurze, handgeschriebene Menüs, das zweite für alles, was aus einer Collection kommt.

Warum bekomme ich am unteren Rand der Rasterzellen einen Overflow-Fehler?

Die Zellenhöhe wird nicht aus dem Inhalt abgeleitet, sondern aus der Zellenbreite und childAspectRatio berechnet. Ist der Inhalt höher, erscheint „A RenderFlex overflowed“. Verkleinern Sie das Verhältnis, legen Sie die Höhe mit mainAxisExtent fest oder machen Sie den Text in der Zelle mit Expanded und maxLines flexibel.

Wie passe ich die Spaltenanzahl an die Bildschirmgröße an?

Verwenden Sie SliverGridDelegateWithMaxCrossAxisExtent: Sie geben eine maximale Zellenbreite an, und Flutter berechnet die Spaltenanzahl für den Bildschirm. Wollen Sie volle Kontrolle, leiten Sie in einem LayoutBuilder aus der Breite eine Spaltenanzahl ab und übergeben sie dem FixedCrossAxisCount-Delegate.

Wie setze ich eine GridView in eine SingleChildScrollView?

Übergeben Sie shrinkWrap: true und physics: const NeverScrollableScrollPhysics(), aber bedenken Sie, dass dann alle Zellen auf einmal gebaut werden. Bei langen Rastern ist ein SliverGrid in einer CustomScrollView die bessere Wahl.

Kommentare