Appearance
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.builderfür effiziente Listen gelernt - ✅
ListTile,Divider,RefreshIndicatorverwendet - ✅ Paginierung (Load More) implementiert
- ✅
GridViewfür Grid-Layouts gelernt - ✅
SingleChildScrollViewfür scrollbare Inhalte verwendet - ✅
AlertDialogfür Dialoge erstellt - ✅
showModalBottomSheetfür Bottom-Sheets verwendet - ✅ Ein Praxisbeispiel (Neuigkeiten-Liste) implementiert
Nächstes Kapitel: Wir werden Basis-Praxis lernen (Counter, Login, Liste).
Übungsaufgaben:
- Erstellen Sie eine To-Do-Liste mit
ListView.builderund Löschfunktion - Erstellen Sie eine Galerie mit
GridView.count(Bilder in Grid anzeigen) - Erstellen Sie ein Formular mit
SingleChildScrollView(viele Eingabefelder) - Implementieren Sie einen Bestätigungs-Dialog vor dem Löschen eines Elements
- Erstellen Sie ein Bottom-Sheet mit drei Optionen (Kamera, Galerie, Löschen)
Häufige Fehler:
- ❌
ListViewinColumnohneshrinkWrap: trueverwenden → Überlauf! - ❌
GridViewinColumnohneshrinkWrap: trueverwenden → Überlauf! - ❌
RefreshIndicatorohneListView/GridViewverwenden → Funktioniert nicht! - ❌ Controller in Dialogen nicht disposen → Speicherleck!
