Appearance
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:
- Netzwerkrequest: Dio verwenden
- JSON-Parsing:
News.fromJson() - ListView.builder: Effiziente Liste
- Routing:
Navigator.push() - 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:
- Provider: Zustandsverwaltung
- shared_preferences: Einstellungen speichern
- ThemeData: Theme umschalten
- 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:
- Erweitern Sie die News-App um eine Suchfunktion
- Fügen Sie den News-App Favoriten-Funktion hinzu (mit shared_preferences)
- Erweitern Sie die Profil-Seite um Profilbild-Änderung
- Fügen Sie der Profil-Seite mehr Einstellungen hinzu
Häufige Fehler:
- ❌ Provider nicht korrekt bereitstellen →
Provider.of()wirft Fehler - ❌ shared_preferences ohne
awaitverwenden → Daten werden nicht gespeichert - ❌ Netzwerkrequest ohne Fehlerbehandlung → App stürzt ab
- ❌ Navigator.push() ohne Context verwenden → Fehler
