Skip to content

Kapitel 14: Fortgeschrittene Praxis

14.1 Praxis 4: Einfache News-App (komplexe Funktionalität)

14.1 Anforderungsanalyse

Funktionalität:

  • ✅ Startseite mit News-Liste
  • ✅ News-Detailseite
  • ✅ Routing (Seitenwechsel)
  • ✅ Netzwerkrequest (Daten abrufen)
  • ✅ ListView (dynamische Liste)
  • ✅ StatefulWidget (Zustandsverwaltung)
  • ✅ shared_preferences (Favoriten speichern)

14.2 Kernimplementierung

Schritt 1: Modellklasse (News-Model)

dart
// news_model.dart
class News {
  final int id;
  final String title;
  final String content;
  final String imageUrl;
  final String date;
  
  News({
    required this.id,
    required this.title,
    required this.content,
    required this.imageUrl,
    required this.date,
  });
  
  factory News.fromJson(Map<String, dynamic> json) {
    return News(
      id: json['id'],
      title: json['title'],
      content: json['body'],
      imageUrl: 'https://picsum.photos/300/200?random=${json['id']}',
      date: DateTime.now().toString().substring(0, 10),
    );
  }
}

Schritt 2: API-Service (Daten abrufen)

dart
// api_service.dart
import 'package:dio/dio.dart';
import 'news_model.dart';

class ApiService {
  final Dio _dio = Dio();
  
  Future<List<News>> ladeNews() async {
    try {
      final response = await _dio.get(
        'https://jsonplaceholder.typicode.com/posts?_limit=10',
      );
      
      final List<dynamic> data = response.data;
      return data.map((json) => News.fromJson(json)).toList();
    } catch (e) {
      throw Exception('Fehler beim Laden der News: $e');
    }
  }
}

Schritt 3: Startseite (News-Liste)

dart
// home_page.dart
import 'package:flutter/material.dart';
import 'api_service.dart';
import 'news_model.dart';
import 'detail_page.dart';

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

  @override
  State<HomePage> createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  final ApiService _apiService = ApiService();
  List<News> _newsList = [];
  bool _isLoading = false;
  String _errorMessage = '';
  
  @override
  void initState() {
    super.initState();
    _ladeNews();
  }
  
  Future<void> _ladeNews() async {
    setState(() {
      _isLoading = true;
      _errorMessage = '';
    });
    
    try {
      final news = await _apiService.ladeNews();
      setState(() {
        _newsList = news;
        _isLoading = false;
      });
    } catch (e) {
      setState(() {
        _errorMessage = e.toString();
        _isLoading = false;
      });
    }
  }
  
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('News App'),
      ),
      body: _buildBody(),
      floatingActionButton: FloatingActionButton(
        onPressed: _ladeNews,
        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: _ladeNews,
              child: const Text('Erneut versuchen'),
            ),
          ],
        ),
      );
    }
    
    return ListView.builder(
      itemCount: _newsList.length,
      itemBuilder: (context, index) {
        final news = _newsList[index];
        return Card(
          margin: const EdgeInsets.all(8),
          child: ListTile(
            leading: CircleAvatar(
              backgroundImage: NetworkImage(news.imageUrl),
            ),
            title: Text(
              news.title,
              maxLines: 1,
              overflow: TextOverflow.ellipsis,
            ),
            subtitle: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: <Widget>[
                Text(
                  news.content,
                  maxLines: 2,
                  overflow: TextOverflow.ellipsis,
                ),
                Text(
                  news.date,
                  style: const TextStyle(color: Colors.grey, fontSize: 12),
                ),
              ],
            ),
            onTap: () {
              Navigator.push(
                context,
                MaterialPageRoute(
                  builder: (context) => DetailPage(news: news),
                ),
              );
            },
          ),
        );
      },
    );
  }
}

Schritt 4: Detailseite (News-Details)

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

class DetailPage extends StatelessWidget {
  final News news;
  
  const DetailPage({super.key, required this.news});
  
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Details'),
      ),
      body: SingleChildScrollView(
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: <Widget>[
            Image.network(
              news.imageUrl,
              width: double.infinity,
              height: 200,
              fit: BoxFit.cover,
            ),
            Padding(
              padding: const EdgeInsets.all(16),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: <Widget>[
                  Text(
                    news.title,
                    style: const TextStyle(
                      fontSize: 24,
                      fontWeight: FontWeight.bold,
                    ),
                  ),
                  const SizedBox(height: 8),
                  Text(
                    news.date,
                    style: const TextStyle(color: Colors.grey),
                  ),
                  const SizedBox(height: 16),
                  Text(
                    news.content,
                    style: const TextStyle(fontSize: 16),
                  ),
                ],
              ),
            ),
          ],
        ),
      ),
    );
  }
}

14.3 Code-Erklärung & Optimierung

Wichtigste Konzepte:

  1. Netzwerkrequest: Dio verwenden
  2. JSON-Parsing: News.fromJson()
  3. ListView.builder: Effiziente Liste
  4. Routing: Navigator.push()
  5. Fehlerbehandlung: try-catch

Optimierung:

  • ✅ Pull-to-Refresh hinzufügen (RefreshIndicator)
  • ✅ Paginierung (Load More)
  • ✅ Favoriten-Funktion mit shared_preferences
  • ✅ Bilder cachen (cached_network_image)

14.4 Praxis 5: Profil-Seite (Styling + lokale Speicherung + Zustandsverwaltung)

14.4 Anforderungsanalyse

Funktionalität:

  • ✅ Benutzerinformationen anzeigen
  • ✅ Theme-Umschaltung (hell/dunkel)
  • ✅ Benutzereinstellungen speichern (shared_preferences)
  • ✅ Zustandsverwaltung mit Provider
  • ✅ Logout-Funktion

14.5 Kernimplementierung

Schritt 1: Theme-Provider (Zustandsverwaltung)

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

class ThemeProvider extends ChangeNotifier {
  bool _isDarkMode = false;
  
  bool get isDarkMode => _isDarkMode;
  
  ThemeData get themeData => _isDarkMode ? ThemeData.dark() : ThemeData.light();
  
  ThemeProvider() {
    _loadTheme();
  }
  
  Future<void> _loadTheme() async {
    final prefs = await SharedPreferences.getInstance();
    _isDarkMode = prefs.getBool('isDarkMode') ?? false;
    notifyListeners();
  }
  
  Future<void> toggleTheme() async {
    _isDarkMode = !_isDarkMode;
    final prefs = await SharedPreferences.getInstance();
    await prefs.setBool('isDarkMode', _isDarkMode);
    notifyListeners();
  }
}

Schritt 2: Profil-Seite (UI & Logik)

dart
// profile_page.dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'theme_provider.dart';

class ProfilePage extends StatelessWidget {
  const ProfilePage({super.key});

  @override
  Widget build(BuildContext context) {
    final themeProvider = Provider.of<ThemeProvider>(context);
    
    return Scaffold(
      appBar: AppBar(
        title: const Text('Profil'),
        actions: <Widget>[
          IconButton(
            icon: Icon(
              themeProvider.isDarkMode ? Icons.light_mode : Icons.dark_mode,
            ),
            onPressed: () {
              themeProvider.toggleTheme();
            },
          ),
        ],
      ),
      body: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: <Widget>[
            const Center(
              child: CircleAvatar(
                radius: 50,
                backgroundImage: NetworkImage('https://picsum.photos/200'),
              ),
            ),
            const SizedBox(height: 20),
            const Center(
              child: Text(
                'Max Mustermann',
                style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
              ),
            ),
            const SizedBox(height: 8),
            const Center(
              child: Text(
                'max.mustermann@example.com',
                style: TextStyle(color: Colors.grey),
              ),
            ),
            const SizedBox(height: 30),
            const Text(
              'Einstellungen',
              style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
            ),
            const SizedBox(height: 10),
            SwitchListTile(
              title: const Text('Dunkles Theme'),
              value: themeProvider.isDarkMode,
              onChanged: (value) {
                themeProvider.toggleTheme();
              },
            ),
            ListTile(
              leading: const Icon(Icons.person),
              title: const Text('Profil bearbeiten'),
              trailing: const Icon(Icons.chevron_right),
              onTap: () {
                // Zur Bearbeitungsseite navigieren
              },
            ),
            ListTile(
              leading: const Icon(Icons.settings),
              title: const Text('Einstellungen'),
              trailing: const Icon(Icons.chevron_right),
              onTap: () {
                // Zur Einstellungsseite navigieren
              },
            ),
            const Spacer(),
            SizedBox(
              width: double.infinity,
              child: ElevatedButton(
                onPressed: () {
                  // Logout-Logik
                  ScaffoldMessenger.of(context).showSnackBar(
                    const SnackBar(content: Text('Erfolgreich abgemeldet')),
                  );
                },
                style: ElevatedButton.styleFrom(
                  backgroundColor: Colors.red,
                  foregroundColor: Colors.white,
                ),
                child: const Text('Abmelden'),
              ),
            ),
          ],
        ),
      ),
    );
  }
}

Schritt 3: In main.dart Provider bereitstellen

dart
// main.dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'theme_provider.dart';
import 'profile_page.dart';

void main() {
  runApp(
    ChangeNotifierProvider(
      create: (context) => ThemeProvider(),
      child: const MyApp(),
    ),
  );
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    final themeProvider = Provider.of<ThemeProvider>(context);
    
    return MaterialApp(
      theme: themeProvider.themeData,
      home: const ProfilePage(),
    );
  }
}

14.6 Code-Erklärung & Optimierung

Wichtigste Konzepte:

  1. Provider: Zustandsverwaltung
  2. shared_preferences: Einstellungen speichern
  3. ThemeData: Theme umschalten
  4. SwitchListTile: Einstellungsoption

Optimierung:

  • ✅ Benutzerdaten vom Server abrufen
  • ✅ Profilbild ändern (Kamera/Galerie)
  • ✅ Mehr Einstellungen hinzufügen (Sprache, Benachrichtigungen)

Zusammenfassung

In diesem Kapitel haben Sie:

  • ✅ Eine einfache News-App mit Netzwerkrequest erstellt
  • ✅ Routing (Seitenwechsel) implementiert
  • ✅ Eine Profil-Seite mit Theme-Umschaltung erstellt
  • ✅ Zustandsverwaltung mit Provider verwendet
  • ✅ Lokale Speicherung mit shared_preferences implementiert
  • ✅ Praxisnahe Projekte umgesetzt

Nächstes Kapitel: Wir werden App-Packaging lernen (Android/iOS).


Übungsaufgaben:

  1. Erweitern Sie die News-App um eine Suchfunktion
  2. Fügen Sie den News-App Favoriten-Funktion hinzu (mit shared_preferences)
  3. Erweitern Sie die Profil-Seite um Profilbild-Änderung
  4. Fügen Sie der Profil-Seite mehr Einstellungen hinzu

Häufige Fehler:

  • ❌ Provider nicht korrekt bereitstellen → Provider.of() wirft Fehler
  • ❌ shared_preferences ohne await verwenden → Daten werden nicht gespeichert
  • ❌ Netzwerkrequest ohne Fehlerbehandlung → App stürzt ab
  • ❌ Navigator.push() ohne Context verwenden → Fehler

Frei für alle Anfänger