Appearance
Kapitel 13: Basis-Praxis
13.1 Praxis 1: Einfacher Zähler (Zustandsverwaltung-Basis)
13.1 Anforderungsanalyse
Funktionalität:
- ✅ Zählerwert anzeigen
- ✅ "+"-Button: Zähler erhöhen
- ✅ "-"-Button: Zähler verringern
- ✅ "Zurücksetzen"-Button: Zähler auf 0 setzen
- ✅ Zählerstand farbig anzeigen (grün für positiv, rot für negativ)
13.2 Kernimplementierung
Vollständiger Code:
dart
import 'package:flutter/material.dart';
class CounterPage extends StatefulWidget {
const CounterPage({super.key});
@override
State<CounterPage> createState() => _CounterPageState();
}
class _CounterPageState extends State<CounterPage> {
int _zaehler = 0;
void _erhoehen() {
setState(() {
_zaehler++;
});
}
void _verringern() {
setState(() {
_zaehler--;
});
}
void _zuruecksetzen() {
setState(() {
_zaehler = 0;
});
}
Color _getZaehlerFarbe() {
if (_zaehler > 0) {
return Colors.green;
} else if (_zaehler < 0) {
return Colors.red;
} else {
return Colors.grey;
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Einfacher Zähler')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
// Titel
const Text(
'Aktueller Zählerstand:',
style: TextStyle(fontSize: 20),
),
const SizedBox(height: 20),
// Zähleranzeige (farbig)
Text(
'$_zaehler',
style: TextStyle(
fontSize: 64,
fontWeight: FontWeight.bold,
color: _getZaehlerFarbe(),
),
),
const SizedBox(height: 40),
// Buttons
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
FloatingActionButton(
onPressed: _verringern,
backgroundColor: Colors.red,
child: const Icon(Icons.remove),
),
const SizedBox(width: 20),
ElevatedButton(
onPressed: _zuruecksetzen,
style: ElevatedButton.styleFrom(
backgroundColor: Colors.grey,
),
child: const Text('Zurücksetzen'),
),
const SizedBox(width: 20),
FloatingActionButton(
onPressed: _erhoehen,
backgroundColor: Colors.green,
child: const Icon(Icons.add),
),
],
),
],
),
),
);
}
}13.3 Code-Erklärung & Optimierung
Wichtigste Konzepte:
- StatefulWidget: Zustand muss sich ändern können
- setState(): UI nach Zustandsänderung aktualisieren
- Bedingte Farbe:
_getZaehlerFarbe()gibt je nach Wert unterschiedliche Farbe zurück
Optimierung (Zustandsverwaltung mit Provider):
- Für größere Apps Zustand mit
Providerverwalten - Trennung von Logik und UI
13.4 Praxis 2: Login-Seite (Basis-Widgets + Layout-Praxis)
13.4 Anforderungsanalyse
Funktionalität:
- ✅ Benutzernamen-Eingabefeld
- ✅ Passwort-Eingabefeld (verborgen)
- ✅ "Login"-Button
- ✅ "Abbrechen"-Button
- ✅ Formularvalidierung (Felder dürfen nicht leer sein)
- ✅ Bild-Anzeige (Logo)
- ✅ Fehlermeldung bei falschen Zugangsdaten
13.5 Kernimplementierung
Vollständiger Code:
dart
import 'package:flutter/material.dart';
class LoginPage extends StatefulWidget {
const LoginPage({super.key});
@override
State<LoginPage> createState() => _LoginPageState();
}
class _LoginPageState extends State<LoginPage> {
final _formKey = GlobalKey<FormState>(); // Formular-Schlüssel
final _usernameController = TextEditingController();
final _passwordController = TextEditingController();
void _login() {
if (_formKey.currentState!.validate()) {
// Formular ist gültig
final username = _usernameController.text;
final password = _passwordController.text;
if (username == 'admin' && password == '123456') {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Login erfolgreich!')),
);
// Hier zur nächsten Seite navigieren
} else {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Login fehlgeschlagen!'),
backgroundColor: Colors.red,
),
);
}
}
}
void _abbrechen() {
_usernameController.clear();
_passwordController.clear();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Login')),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Form(
key: _formKey, // Formular-Schlüssel
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
// Logo
const Icon(Icons.lock, size: 80, color: Colors.blue),
const SizedBox(height: 32),
// Benutzername
TextFormField(
controller: _usernameController,
decoration: const InputDecoration(
labelText: 'Benutzername',
hintText: 'Geben Sie Benutzernamen ein',
prefixIcon: Icon(Icons.person),
border: OutlineInputBorder(),
),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Bitte Benutzernamen eingeben';
}
return null;
},
),
const SizedBox(height: 16),
// Passwort
TextFormField(
controller: _passwordController,
obscureText: true, // Passwort verbergen
decoration: const InputDecoration(
labelText: 'Passwort',
hintText: 'Geben Sie Passwort ein',
prefixIcon: Icon(Icons.lock),
border: OutlineInputBorder(),
),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Bitte Passwort eingeben';
}
if (value.length < 6) {
return 'Passwort muss mindestens 6 Zeichen lang sein';
}
return null;
},
),
const SizedBox(height: 24),
// Login-Button
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: _login,
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 16),
),
child: const Text('Login', style: TextStyle(fontSize: 18)),
),
),
const SizedBox(height: 16),
// Abbrechen-Button
TextButton(
onPressed: _abbrechen,
child: const Text('Abbrechen'),
),
],
),
),
),
);
}
@override
void dispose() {
_usernameController.dispose();
_passwordController.dispose();
super.dispose();
}
}13.6 Code-Erklärung & Optimierung
Wichtigste Konzepte:
- Form & GlobalKey: Für Formularvalidierung
- TextFormField: Eingabefeld mit Validierung
- obscureText: Passwort verbergen
- SnackBar: kurze Benachrichtigung anzeigen
Optimierung:
- ✅ Passwort-Regeln verschärfen (Groß-/Kleinschreibung, Sonderzeichen)
- ✅ "Passwort vergessen"-Link hinzufügen
- ✅ Registrierungs-Button hinzufügen
13.7 Praxis 3: Liste-Anzeige-Seite (Netzwerkrequest + List-Widget)
13.7 Anforderungsanalyse
Funktionalität:
- ✅ Daten per Netzwerkrequest (GET) abrufen
- ✅ Daten in ListView dynamisch anzeigen
- ✅ Pull-to-Refresh (Daten neu laden)
- ✅ Ladeanzeige während des Ladens
- ✅ Fehlerbehandlung (Netzwerkfehler, Serverfehler)
- ✅ Klicken auf Listenelement → Detailseite (optional)
13.8 Kernimplementierung
Vollständiger Code:
dart
import 'package:flutter/material.dart';
import 'package:dio/dio.dart';
// Modellklasse
class Post {
final int id;
final String title;
final String body;
Post({required this.id, required this.title, required this.body});
factory Post.fromJson(Map<String, dynamic> json) {
return Post(
id: json['id'],
title: json['title'],
body: json['body'],
);
}
}
class PostsPage extends StatefulWidget {
const PostsPage({super.key});
@override
State<PostsPage> createState() => _PostsPageState();
}
class _PostsPageState extends State<PostsPage> {
final Dio _dio = Dio();
List<Post> _posts = [];
bool _isLoading = false;
String _errorMessage = '';
@override
void initState() {
super.initState();
_ladePosts();
}
Future<void> _ladePosts() async {
setState(() {
_isLoading = true;
_errorMessage = '';
});
try {
final response = await _dio.get('https://jsonplaceholder.typicode.com/posts?_limit=10');
// JSON zu Liste von Post-Objekten konvertieren
final List<dynamic> data = response.data;
final posts = data.map((json) => Post.fromJson(json)).toList();
setState(() {
_posts = posts;
_isLoading = false;
});
} catch (e) {
setState(() {
_errorMessage = 'Fehler beim Laden: $e';
_isLoading = false;
});
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Posts')),
body: _buildBody(),
floatingActionButton: FloatingActionButton(
onPressed: _ladePosts,
child: const Icon(Icons.refresh),
),
);
}
Widget _buildBody() {
if (_isLoading) {
return const Center(child: CircularProgressIndicator());
}
if (_errorMessage.isNotEmpty) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Icon(Icons.error, size: 64, color: Colors.red),
const SizedBox(height: 16),
Text(_errorMessage, textAlign: TextAlign.center),
const SizedBox(height: 16),
ElevatedButton(
onPressed: _ladePosts,
child: const Text('Erneut versuchen'),
),
],
),
);
}
return RefreshIndicator(
onRefresh: _ladePosts,
child: ListView.builder(
itemCount: _posts.length,
itemBuilder: (context, index) {
final post = _posts[index];
return Card(
margin: const EdgeInsets.all(8),
child: ListTile(
leading: CircleAvatar(
child: Text('${post.id}'),
),
title: Text(
post.title,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
subtitle: Text(
post.body,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
onTap: () {
// Zur Detailseite navigieren (optional)
print('Post ${post.id} geklickt');
},
),
);
},
),
);
}
@override
void dispose() {
_dio.close();
super.dispose();
}
}13.9 Code-Erklärung & Optimierung
Wichtigste Konzepte:
- Netzwerkrequest: Dio verwenden
- JSON-Parsing: Manuell mit
Post.fromJson() - Zustandsverwaltung:
_isLoading,_errorMessage,_posts - RefreshIndicator: Pull-to-Refresh
- ListView.builder: Effiziente Liste
Optimierung:
- ✅ JSON-Autoparsing mit
json_serializable - ✅ Bilder in Liste anzeigen
- ✅ Paginierung (Load More)
- ✅ Suchfunktion
Zusammenfassung
In diesem Kapitel haben Sie:
- ✅ Einen einfachen Zähler mit StatefulWidget & setState() erstellt
- ✅ Eine Login-Seite mit Formularvalidierung erstellt
- ✅ Eine Liste-Anzeige-Seite mit Netzwerkrequest erstellt
- ✅ Pull-to-Refresh und Fehlerbehandlung implementiert
- ✅ Praxisnahe Projekte umgesetzt
Nächstes Kapitel: Wir werden fortgeschrittene Praxis lernen (News-App, Profil-Seite).
Übungsaufgaben:
- Erweitern Sie den Zähler um einen Schrittweiten-Wähler (1, 5, 10)
- Fügen Sie der Login-Seite eine "Passwort vergessen"-Funktion hinzu
- Erweitern Sie die Liste-Anzeige um Suchfunktion und Paginierung
- Erstellen Sie eine Detailseite, die beim Klicken auf ein Listenelement geöffnet wird
Häufige Fehler:
- ❌
setState()vergessen → UI aktualisiert sich nicht - ❌ Controller nicht disposen → Speicherleck
- ❌ Netzwerkrequest ohne Fehlerbehandlung → App stürzt ab
- ❌
ListViewohneExpanded/SizedBoxinColumnverwenden → Überlauf
