Skip to content

Kapitel 10: Lokale Speicherung

10.1 Anwendungsfälle der lokalen Speicherung

Warum lokale Speicherung?

Szenarien:

  • ✅ Benutzereinstellungen speichern (Theme, Sprache)
  • ✅ Login-Status speichern (Eingeloggt bleiben)
  • ✅ Offline-Daten speichern (Neuigkeiten-Cache)
  • ✅ Benutzerdaten speichern (Profilinformationen)

Verschiedene Speicherlösungen

LösungAnwendungsfallVorteileNachteile
shared_preferencesEinfache Key-Value-DatenEinfach, leichtgewichtigNur primitive Typen
sqfliteStrukturierte Daten (Datenbank)Leistungsstark, SQLKomplexer
HiveNoSQL, schnelle LeistungSehr schnell, einfachBenötigt Code-Generierung
path_providerDateisystem-ZugriffZugriff auf Dokumente/CacheManuelles Dateihandling

Für Anfänger: Beginnen Sie mit shared_preferences, dann sqflite.

10.2 shared_preferences (Key-Value-Speicherung)

Installation

pubspec.yaml:

yaml
dependencies:
  flutter:
    sdk: flutter
  shared_preferences: ^2.2.0
bash
flutter pub get

Grundlegende Verwendung

Speichern (SET):

dart
import 'package:shared_preferences/shared_preferences.dart';

Future<void> speichereDaten() async {
  final prefs = await SharedPreferences.getInstance();
  
  // Verschiedene Datentypen speichern
  await prefs.setString('name', 'Max Mustermann');  // String
  await prefs.setInt('alter', 25);                  // Integer
  await prefs.setBool('istEingeloggt', true);       // Boolean
  await prefs.setDouble('gewicht', 75.5);           // Double
  await prefs.setStringList('hobbys', ['Lesen', 'Sport']);  // Liste<String>
}

Lesen (GET):

dart
Future<void> ladeDaten() async {
  final prefs = await SharedPreferences.getInstance();
  
  final name = prefs.getString('name') ?? 'Gast';  // Standardwert
  final alter = prefs.getInt('alter') ?? 0;
  final istEingeloggt = prefs.getBool('istEingeloggt') ?? false;
  final hobbys = prefs.getStringList('hobbys') ?? [];
  
  print('Name: $name, Alter: $alter');
}

Löschen (REMOVE):

dart
Future<void> loescheDaten() async {
  final prefs = await SharedPreferences.getInstance();
  
  await prefs.remove('name');     // Einzelnen Schlüssel löschen
  await prefs.clear();            // Alles löschen
}

Praxisbeispiel: Login-Status speichern

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

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

  @override
  State<LoginPage> createState() => _LoginPageState();
}

class _LoginPageState extends State<LoginPage> {
  final TextEditingController _usernameController = TextEditingController();
  final TextEditingController _passwordController = TextEditingController();
  bool _istEingeloggt = false;
  
  @override
  void initState() {
    super.initState();
    _pruefeLoginStatus();  // Login-Status beim Start prüfen
  }
  
  // Login-Status prüfen
  Future<void> _pruefeLoginStatus() async {
    final prefs = await SharedPreferences.getInstance();
    setState(() {
      _istEingeloggt = prefs.getBool('istEingeloggt') ?? false;
    });
  }
  
  // Login durchführen
  Future<void> _login() async {
    final username = _usernameController.text;
    final password = _passwordController.text;
    
    if (username == 'admin' && password == '123456') {
      final prefs = await SharedPreferences.getInstance();
      await prefs.setString('username', username);
      await prefs.setBool('istEingeloggt', true);
      
      setState(() {
        _istEingeloggt = true;
      });
      
      ScaffoldMessenger.of(context).showSnackBar(
        const SnackBar(content: Text('Login erfolgreich!')),
      );
    } else {
      ScaffoldMessenger.of(context).showSnackBar(
        const SnackBar(content: Text('Login fehlgeschlagen!')),
      );
    }
  }
  
  // Logout durchführen
  Future<void> _logout() async {
    final prefs = await SharedPreferences.getInstance();
    await prefs.clear();  // Alle Daten löschen
    
    setState(() {
      _istEingeloggt = false;
      _usernameController.clear();
      _passwordController.clear();
    });
  }
  
  @override
  Widget build(BuildContext context) {
    if (_istEingeloggt) {
      // Bereits eingeloggt → Profilseite anzeigen
      return _buildProfilSeite();
    } else {
      // Nicht eingeloggt → Login-Formular anzeigen
      return _buildLoginFormular();
    }
  }
  
  Widget _buildLoginFormular() {
    return Scaffold(
      appBar: AppBar(title: const Text('Login')),
      body: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            TextField(
              controller: _usernameController,
              decoration: const InputDecoration(labelText: 'Benutzername'),
            ),
            const SizedBox(height: 16),
            TextField(
              controller: _passwordController,
              obscureText: true,
              decoration: const InputDecoration(labelText: 'Passwort'),
            ),
            const SizedBox(height: 24),
            ElevatedButton(
              onPressed: _login,
              child: const Text('Login'),
            ),
          ],
        ),
      ),
    );
  }
  
  Widget _buildProfilSeite() {
    return Scaffold(
      appBar: AppBar(title: const Text('Profil')),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            const Icon(Icons.person, size: 100, color: Colors.blue),
            const SizedBox(height: 20),
            FutureBuilder<SharedPreferences>(
              future: SharedPreferences.getInstance(),
              builder: (context, snapshot) {
                if (snapshot.hasData) {
                  final username = snapshot.data!.getString('username');
                  return Text(
                    'Willkommen, $username!',
                    style: const TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
                  );
                }
                return const Text('Laden...');
              },
            ),
            const SizedBox(height: 30),
            ElevatedButton(
              onPressed: _logout,
              child: const Text('Logout'),
            ),
          ],
        ),
      ),
    );
  }
  
  @override
  void dispose() {
    _usernameController.dispose();
    _passwordController.dispose();
    super.dispose();
  }
}

10.3 sqflite (Lokale Datenbank)

Installation

pubspec.yaml:

yaml
dependencies:
  sqflite: ^2.3.0
  path_provider: ^2.1.0  # Um Datenbankpfad zu erhalten
bash
flutter pub get

Datenbank initialisieren

dart
import 'package:sqflite/sqflite.dart';
import 'package:path/path.dart';
import 'package:path_provider/path_provider.dart';

class DatabaseHelper {
  static Database? _database;
  
  // Singleton-Muster (nur eine Datenbank-Instanz)
  Future<Database> get database async {
    if (_database != null) return _database!;
    _database = await _initDatabase();
    return _database!;
  }
  
  // Datenbank initialisieren
  Future<Database> _initDatabase() async {
    // Datenbankpfad erhalten
    final documentsDirectory = await getApplicationDocumentsDirectory();
    final path = join(documentsDirectory.path, 'app_database.db');
    
    return await openDatabase(
      path,
      version: 1,
      onCreate: _onCreate,
    );
  }
  
  // Tabellen erstellen
  Future<void> _onCreate(Database db, int version) async {
    await db.execute('''
      CREATE TABLE benutzer(
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        name TEXT NOT NULL,
        alter INTEGER,
        email TEXT UNIQUE
      )
    ''');
  }
}

CRUD-Operationen (Create, Read, Update, Delete)

1. Erstellen (INSERT):

dart
Future<int> fuegeBenutzerHinzu(String name, int alter, String email) async {
  final db = await database;
  return await db.insert(
    'benutzer',
    {
      'name': name,
      'alter': alter,
      'email': email,
    },
    conflictAlgorithm: ConflictAlgorithm.replace,  // Bei Konflikt ersetzen
  );
}

2. Lesen (SELECT):

dart
Future<List<Map<String, dynamic>>> holeAlleBenutzer() async {
  final db = await database;
  return await db.query('benutzer');  // Alle Benutzer abrufen
}

Future<Map<String, dynamic>?> holeBenutzerPerId(int id) async {
  final db = await database;
  final result = await db.query(
    'benutzer',
    where: 'id = ?',
    whereArgs: [id],
  );
  return result.isNotEmpty ? result.first : null;
}

3. Aktualisieren (UPDATE):

dart
Future<int> aktualisiereBenutzer(int id, String name, int alter) async {
  final db = await database;
  return await db.update(
    'benutzer',
    {
      'name': name,
      'alter': alter,
    },
    where: 'id = ?',
    whereArgs: [id],
  );
}

4. Löschen (DELETE):

dart
Future<int> loescheBenutzer(int id) async {
  final db = await database;
  return await db.delete(
    'benutzer',
    where: 'id = ?',
    whereArgs: [id],
  );
}

Praxisbeispiel: Einfache Kontaktverwaltung

dart
import 'package:flutter/material.dart';
import 'package:sqflite/sqflite.dart';

class Kontakt {
  final int? id;
  final String name;
  final String telefon;
  
  Kontakt({this.id, required this.name, required this.telefon});
  
  Map<String, dynamic> toMap() {
    return {
      'id': id,
      'name': name,
      'telefon': telefon,
    };
  }
  
  factory Kontakt.fromMap(Map<String, dynamic> map) {
    return Kontakt(
      id: map['id'],
      name: map['name'],
      telefon: map['telefon'],
    );
  }
}

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

  @override
  State<KontaktPage> createState() => _KontaktPageState();
}

class _KontaktPageState extends State<KontaktPage> {
  List<Kontakt> _kontakte = [];
  final _nameController = TextEditingController();
  final _telefonController = TextEditingController();
  Database? _database;
  
  @override
  void initState() {
    super.initState();
    _initDatabase();
  }
  
  Future<void> _initDatabase() async {
    _database = await openDatabase(
      join(await getDatabasesPath(), 'kontakte.db'),
      onCreate: (db, version) {
        return db.execute(
          'CREATE TABLE kontakte(id INTEGER PRIMARY KEY, name TEXT, telefon TEXT)',
        );
      },
      version: 1,
    );
    _ladeKontakte();
  }
  
  Future<void> _ladeKontakte() async {
    final kontakte = await _database?.query('kontakte');
    setState(() {
      _kontakte = kontakte?.map((map) => Kontakt.fromMap(map)).toList() ?? [];
    });
  }
  
  Future<void> _hinzufuegenKontakt() async {
    await _database?.insert(
      'kontakte',
      Kontakt(name: _nameController.text, telefon: _telefonController.text).toMap(),
    );
    _nameController.clear();
    _telefonController.clear();
    _ladeKontakte();
  }
  
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Kontakte')),
      body: Column(
        children: <Widget>[
          Padding(
            padding: const EdgeInsets.all(8.0),
            child: TextField(controller: _nameController, decoration: const InputDecoration(labelText: 'Name')),
          ),
          Padding(
            padding: const EdgeInsets.all(8.0),
            child: TextField(controller: _telefonController, decoration: const InputDecoration(labelText: 'Telefon')),
          ),
          ElevatedButton(onPressed: _hinzufuegenKontakt, child: const Text('Hinzufügen')),
          Expanded(
            child: ListView.builder(
              itemCount: _kontakte.length,
              itemBuilder: (context, index) {
                final kontakt = _kontakte[index];
                return ListTile(
                  title: Text(kontakt.name),
                  subtitle: Text(kontakt.telefon),
                );
              },
            ),
          ),
        ],
      ),
    );
  }
}

10.4 Lokale Speicherung mit Zustandsverwaltung kombinieren

Ziel: Zustand speichern, App-Neustart überleben

Beispiel: Theme-Einstellung speichern

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() {
    _ladeTheme();  // Beim Start Theme laden
  }
  
  Future<void> _ladeTheme() 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();
  }
}

10.5 Häufige Fehler

Fehler 1: SharedPreferences.getInstance() nicht awaiten

Fehlerhaft:

dart
void ladeDaten() {
  final prefs = SharedPreferences.getInstance();  // Fehler: Future nicht awaited!
  final name = prefs.getString('name');
}

Richtig:

dart
Future<void> ladeDaten() async {
  final prefs = await SharedPreferences.getInstance();  // await verwenden!
  final name = prefs.getString('name');
}

Fehler 2: Datenbank nicht initialisiert

Fehler: App stürzt ab, weil Datenbank noch nicht erstellt wurde.

Lösung: Immer sicherstellen, dass Datenbank initialisiert ist:

dart
Future<void> _initDatabase() async {
  _database = await openDatabase(...);  // Initialisieren
}

Future<void> fuegeDatenHinzu() async {
  if (_database == null) {
    await _initDatabase();  // Falls noch nicht initialisiert
  }
  // Jetzt Datenbank verwenden
}

Zusammenfassung

In diesem Kapitel haben Sie:

  • ✅ Anwendungsfälle für lokale Speicherung verstanden
  • shared_preferences für Key-Value-Speicherung gemeistert
  • ✅ Ein Praxisbeispiel (Login-Status speichern) implementiert
  • sqflite für lokale Datenbank-Operationen gelernt
  • ✅ CRUD-Operationen (Create, Read, Update, Delete) implementiert
  • ✅ Lokale Speicherung mit Zustandsverwaltung kombiniert
  • ✅ Häufige Fehler behoben

Nächstes Kapitel: Wir werden Styling & Themes lernen (TextStyle, BoxDecoration, ThemeData).


Übungsaufgaben:

  1. Erstellen Sie eine App, die Benutzereinstellungen (Sprache, Theme) mit shared_preferences speichert
  2. Erstellen Sie eine einfache Notiz-App mit sqflite (Notizen hinzufügen, anzeigen, löschen)
  3. Kombinieren Sie Provider mit shared_preferences, um Theme-Einstellungen dauerhaft zu speichern
  4. Erstellen Sie eine App, die Kontaktdaten in einer sqflite-Datenbank verwaltet

Häufige Fehler:

  • SharedPreferences.getInstance() ohne await verwenden → App stürzt ab
  • ❌ Datenbankoperationen ohne await verwenden → Daten werden nicht gespeichert
  • setState() nach asynchronen Operationen vergessen → UI aktualisiert sich nicht
  • ❌ Datenbank nicht vor Verwendung initialisieren → NullPointer-Ausnahme

Frei für alle Anfänger