Skip to content

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:

  1. StatefulWidget: Zustand muss sich ändern können
  2. setState(): UI nach Zustandsänderung aktualisieren
  3. Bedingte Farbe: _getZaehlerFarbe() gibt je nach Wert unterschiedliche Farbe zurück

Optimierung (Zustandsverwaltung mit Provider):

  • Für größere Apps Zustand mit Provider verwalten
  • 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:

  1. Form & GlobalKey: Für Formularvalidierung
  2. TextFormField: Eingabefeld mit Validierung
  3. obscureText: Passwort verbergen
  4. 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:

  1. Netzwerkrequest: Dio verwenden
  2. JSON-Parsing: Manuell mit Post.fromJson()
  3. Zustandsverwaltung: _isLoading, _errorMessage, _posts
  4. RefreshIndicator: Pull-to-Refresh
  5. 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:

  1. Erweitern Sie den Zähler um einen Schrittweiten-Wähler (1, 5, 10)
  2. Fügen Sie der Login-Seite eine "Passwort vergessen"-Funktion hinzu
  3. Erweitern Sie die Liste-Anzeige um Suchfunktion und Paginierung
  4. 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
  • ListView ohne Expanded/SizedBox in Column verwenden → Überlauf

Frei für alle Anfänger