Skip to content

Kapitel 12: Erweiterte Widgets

12.1 Listen-Widget (ListView - am häufigsten verwendet)

Grundlegende Verwendung (ListView.builder - empfohlen)

ListView.builder ist leistungsfähiger als ListView.children, da es nur sichtbare Elemente baut.

dart
ListView.builder(
  itemCount: 20,  // Anzahl der Elemente
  itemBuilder: (context, index) {
    return ListTile(
      leading: Icon(Icons.person),
      title: Text('Element $index'),
      subtitle: Text('Beschreibung $index'),
      trailing: Icon(Icons.arrow_forward_ios),
      onTap: () {
        print('Element $index geklickt');
      },
    );
  },
)

ListTile (Listenelement)

dart
ListTile(
  leading: CircleAvatar(  // Vorne (Icon/Avatar)
    backgroundImage: NetworkImage('https://example.com/avatar.jpg'),
  ),
  title: const Text('Max Mustermann'),  // Titel
  subtitle: const Text('max@example.com'),  // Untertitel
  trailing: const Icon(Icons.chevron_right),  // Hinten (Icon)
  onTap: () {  // Klick-Event
    print('ListTile geklickt');
  },
  onLongPress: () {  // Langdrück-Event
    print('Langgedrückt');
  },
)

Listenuberschuss (Divider)

dart
ListView.builder(
  itemCount: 10,
  itemBuilder: (context, index) {
    return Column(
      children: <Widget>[
        ListTile(title: Text('Element $index')),
        const Divider(),  // Trennlinie
      ],
    );
  },
)

Pull-to-Refresh (RefreshIndicator)

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

  @override
  State<RefreshPage> createState() => _RefreshPageState();
}

class _RefreshPageState extends State<RefreshPage> {
  List<String> _items = [];
  
  @override
  void initState() {
    super.initState();
    _ladeDaten();
  }
  
  Future<void> _ladeDaten() async {
    await Future.delayed(const Duration(seconds: 2));  // Simuliere Laden
    setState(() {
      _items = List.generate(20, (index) => 'Element $index');
    });
  }
  
  Future<void> _aktualisieren() async {
    await _ladeDaten();
  }
  
  @override
  Widget build(BuildContext context) {
    return RefreshIndicator(
      onRefresh: _aktualisieren,  // Wird bei Ziehen aufgerufen
      child: ListView.builder(
        itemCount: _items.length,
        itemBuilder: (context, index) {
          return ListTile(title: Text(_items[index]));
        },
      ),
    );
  }
}

Load More (Paginierung)

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

  @override
  State<LoadMorePage> createState() => _LoadMorePageState();
}

class _LoadMorePageState extends State<LoadMorePage> {
  List<String> _items = [];
  bool _isLoading = false;
  int _page = 1;
  
  @override
  void initState() {
    super.initState();
    _ladeMehr();
  }
  
  Future<void> _ladeMehr() async {
    if (_isLoading) return;
    
    setState(() {
      _isLoading = true;
    });
    
    await Future.delayed(const Duration(seconds: 2));  // Simuliere API
    
    setState(() {
      _items.addAll(
        List.generate(10, (index) => 'Element ${_items.length + index}'),
      );
      _isLoading = false;
      _page++;
    });
  }
  
  @override
  Widget build(BuildContext context) {
    return ListView.builder(
      itemCount: _items.length + 1,  // +1 für "Mehr laden"-Button
      itemBuilder: (context, index) {
        if (index == _items.length) {
          return _isLoading
              ? const Center(child: CircularProgressIndicator())
              : ElevatedButton(
                  onPressed: _ladeMehr,
                  child: const Text('Mehr laden'),
                );
        }
        return ListTile(title: Text(_items[index]));
      },
    );
  }
}

12.2 Grid-Widget (GridView)

Grundlegende Verwendung

dart
GridView.count(
  crossAxisCount: 2,  // 2 Spalten
  crossAxisSpacing: 10,  // Horizontaler Abstand
  mainAxisSpacing: 10,   // Vertikaler Abstand
  childAspectRatio: 0.75,  // Breite/Höhe Verhältnis
  padding: const EdgeInsets.all(10),
  children: <Widget>[
    _buildGridItem('Artikel 1', Icons.shopping_bag),
    _buildGridItem('Artikel 2', Icons.phone_iphone),
    _buildGridItem('Artikel 3', Icons.laptop),
    _buildGridItem('Artikel 4', Icons.headset),
  ],
)

GridView.builder (effizienter)

dart
GridView.builder(
  gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
    crossAxisCount: 2,
    crossAxisSpacing: 10,
    mainAxisSpacing: 10,
    childAspectRatio: 0.75,
  ),
  itemCount: 20,
  itemBuilder: (context, index) {
    return _buildGridItem('Artikel $index', Icons.shopping_bag);
  },
)

Hilfsfunktion: Grid-Element erstellen

dart
Widget _buildGridItem(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: 10),
          Text(title, style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
          const SizedBox(height: 5),
          const Text('Kurze Beschreibung...', textAlign: TextAlign.center),
        ],
      ),
    ),
  );
}

12.3 Scroll-Widget (SingleChildScrollView)

Wann verwenden?

  • ✅ Wenn der Inhalt möglicherweise über den Bildschirm hinausgeht
  • ✅ Für Formulare mit vielen Eingabefeldern
  • ✅ Für scrolbare Spalten/Zeilen

Grundlegende Verwendung

dart
SingleChildScrollView(
  child: Column(
    children: <Widget>[
      // Viele Widgets...
      Container(height: 200, color: Colors.red),
      Container(height: 200, color: Colors.blue),
      Container(height: 200, color: Colors.green),
      Container(height: 200, color: Colors.yellow),
      Container(height: 200, color: Colors.purple),
    ],
  ),
)

Horizontales Scrollen

dart
SingleChildScrollView(
  scrollDirection: Axis.horizontal,  // Horizontales Scrollen
  child: Row(
    children: <Widget>[
      Container(width: 200, height: 200, color: Colors.red),
      Container(width: 200, height: 200, color: Colors.blue),
      Container(width: 200, height: 200, color: Colors.green),
      Container(width: 200, height: 200, color: Colors.yellow),
    ],
  ),
)

12.4 Dialog-Widget (AlertDialog)

Einfacher Dialog (AlertDialog)

dart
void _zeigeDialog(BuildContext context) {
  showDialog(
    context: context,
    builder: (context) {
      return AlertDialog(
        title: const Text('Bestätigung'),
        content: const Text('Möchten Sie diesen Vorgang wirklich ausführen?'),
        actions: <Widget>[
          TextButton(
            onPressed: () {
              Navigator.pop(context);  // Dialog schließen
            },
            child: const Text('Abbrechen'),
          ),
          ElevatedButton(
            onPressed: () {
              Navigator.pop(context);
              print('Bestätigt!');
            },
            child: const Text('OK'),
          ),
        ],
      );
    },
  );
}

Eingabe-Dialog (mit TextField)

dart
void _zeigeEingabeDialog(BuildContext context) {
  final controller = TextEditingController();
  
  showDialog(
    context: context,
    builder: (context) {
      return AlertDialog(
        title: const Text('Name eingeben'),
        content: TextField(
          controller: controller,
          decoration: const InputDecoration(
            hintText: 'Geben Sie Ihren Namen ein',
          ),
        ),
        actions: <Widget>[
          TextButton(
            onPressed: () => Navigator.pop(context),
            child: const Text('Abbrechen'),
          ),
          ElevatedButton(
            onPressed: () {
              final name = controller.text;
              print('Eingegebener Name: $name');
              Navigator.pop(context);
            },
            child: const Text('OK'),
          ),
        ],
      );
    },
  );
}

12.5 Bottom-Sheet (showModalBottomSheet)

Einfaches Bottom-Sheet

dart
void _zeigeBottomSheet(BuildContext context) {
  showModalBottomSheet(
    context: context,
    shape: const RoundedRectangleBorder(
      borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
    ),
    builder: (context) {
      return Container(
        padding: const EdgeInsets.all(16),
        child: Column(
          mainAxisSize: MainAxisSize.min,  // Nur so groß wie nötig
          children: <Widget>[
            const Text('Optionen', style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold)),
            const SizedBox(height: 16),
            ListTile(
              leading: const Icon(Icons.camera_alt),
              title: const Text('Kamera'),
              onTap: () {
                Navigator.pop(context);
                print('Kamera ausgewählt');
              },
            ),
            ListTile(
              leading: const Icon(Icons.photo),
              title: const Text('Galerie'),
              onTap: () {
                Navigator.pop(context);
                print('Galerie ausgewählt');
              },
            ),
            ListTile(
              leading: const Icon(Icons.delete),
              title: const Text('Löschen', style: TextStyle(color: Colors.red)),
              onTap: () {
                Navigator.pop(context);
                print('Löschen ausgewählt');
              },
            ),
          ],
        ),
      );
    },
  );
}

12.6 Praxisbeispiel: Dynamische Liste mit Pull-to-Refresh & Load More

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

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

  @override
  State<NewsPage> createState() => _NewsPageState();
}

class _NewsPageState extends State<NewsPage> {
  List<Map<String, dynamic>> _news = [];
  bool _isLoading = false;
  int _page = 1;
  
  @override
  void initState() {
    super.initState();
    _ladeNews();
  }
  
  Future<void> _ladeNews() async {
    if (_isLoading) return;
    
    setState(() {
      _isLoading = true;
    });
    
    // Simuliere API-Aufruf
    await Future.delayed(const Duration(seconds: 2));
    
    setState(() {
      _news.addAll(
        List.generate(10, (index) {
          final id = _news.length + index + 1;
          return {
            'id': id,
            'title': 'Neuigkeit $id',
            'content': 'Dies ist der Inhalt der Neuigkeit $id...',
            'date': DateTime.now().toString().substring(0, 10),
          };
        }),
      );
      _isLoading = false;
      _page++;
    });
  }
  
  Future<void> _aktualisieren() async {
    setState(() {
      _news.clear();
      _page = 1;
    });
    await _ladeNews();
  }
  
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Neuigkeiten')),
      body: RefreshIndicator(
        onRefresh: _aktualisieren,
        child: ListView.builder(
          itemCount: _news.length + 1,
          itemBuilder: (context, index) {
            if (index == _news.length) {
              return _isLoading
                  ? const Center(child: CircularProgressIndicator())
                  : Padding(
                      padding: const EdgeInsets.all(16.0),
                      child: ElevatedButton(
                        onPressed: _ladeNews,
                        child: const Text('Mehr laden'),
                      ),
                    );
            }
            
            final item = _news[index];
            return Card(
              margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
              child: ListTile(
                contentPadding: const EdgeInsets.all(16),
                title: Text(item['title'], style: const TextStyle(fontWeight: FontWeight.bold)),
                subtitle: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: <Widget>[
                    const SizedBox(height: 8),
                    Text(item['content']),
                    const SizedBox(height: 8),
                    Text(item['date'], style: const TextStyle(color: Colors.grey, fontSize: 12)),
                  ],
                ),
                onTap: () {
                  print('Neuigkeit ${item['id']} geklickt');
                },
              ),
            );
          },
        ),
      ),
    );
  }
}

Zusammenfassung

In diesem Kapitel haben Sie:

  • ListView.builder für effiziente Listen gelernt
  • ListTile, Divider, RefreshIndicator verwendet
  • ✅ Paginierung (Load More) implementiert
  • GridView für Grid-Layouts gelernt
  • SingleChildScrollView für scrollbare Inhalte verwendet
  • AlertDialog für Dialoge erstellt
  • showModalBottomSheet für Bottom-Sheets verwendet
  • ✅ Ein Praxisbeispiel (Neuigkeiten-Liste) implementiert

Nächstes Kapitel: Wir werden Basis-Praxis lernen (Counter, Login, Liste).


Übungsaufgaben:

  1. Erstellen Sie eine To-Do-Liste mit ListView.builder und Löschfunktion
  2. Erstellen Sie eine Galerie mit GridView.count (Bilder in Grid anzeigen)
  3. Erstellen Sie ein Formular mit SingleChildScrollView (viele Eingabefelder)
  4. Implementieren Sie einen Bestätigungs-Dialog vor dem Löschen eines Elements
  5. Erstellen Sie ein Bottom-Sheet mit drei Optionen (Kamera, Galerie, Löschen)

Häufige Fehler:

  • ListView in Column ohne shrinkWrap: true verwenden → Überlauf!
  • GridView in Column ohne shrinkWrap: true verwenden → Überlauf!
  • RefreshIndicator ohne ListView/GridView verwenden → Funktioniert nicht!
  • ❌ Controller in Dialogen nicht disposen → Speicherleck!

Frei für alle Anfänger