Technologie & Architektur 21. Juli 2025 16 min Lesezeit

Offline-Apps entwickeln 2025: Funktioniert auch ohne Internet

Offline-First App-Entwicklung: SQLite/SQLCipher, Sync-Strategien, Conflict Resolution. Real Cases: Wächterkontrolle, Inventur, Mensa-NFC. Kosten +15-25%.

Carola Schulte, App-Entwicklerin
Carola Schulte, App-Entwicklerin
Zurück zum Blog

Offline-Apps entwickeln: Funktioniert auch ohne Internet

Ihre App soll im Außendienst, im Keller, im Funkloch funktionieren? Offline-First ist kein Nice-to-Have - es ist kritisch für Business-Apps, die in der Realität funktionieren müssen.

Als Software-Architektin mit 25+ Jahren Erfahrung zeige ich Ihnen, wie Sie robuste Offline-Apps bauen: SQLite/SQLCipher, Sync-Strategien, Conflict Resolution - mit konkreten Beispielen aus Wächterkontrollsystemen, Inventur-Apps und NFC-Check-in-Systemen.

🔔 WICHTIG: Alle Preise verstehen sich netto (zzgl. 19% USt.). DE-Sätze; Offshore/NE-EU abweichend.


Die Wahrheit über Offline-Apps

Offline-First ist nicht teurer - wenn Sie von Anfang an richtig planen.

  • ✅ Offline-First kostet +15-25% mehr als Online-Only
  • ✅ SQLite/SQLCipher Setup kostet 2-3 Entwicklertage
  • ✅ Sync-Logik kostet 3-5 Entwicklertage
  • ✅ Nachträgliches Offline kostet +150-200% (komplette Architektur-Umstellung)

Fazit: Entscheiden Sie in der Discovery-Phase, ob Ihre App offline funktionieren muss - nicht nach 6 Monaten Entwicklung.


Was bedeutet Offline-First?

Offline-First bedeutet: Ihre App funktioniert komplett ohne Internet - und synchronisiert Daten, sobald Netz verfügbar ist.

WICHTIG: Offline-First ≠ Offline-Capable!

Offline-First vs. Offline-Capable vs. Online-Only

AnsatzFunktioniert ohne Netz?Daten schreiben offline?Sync-LogikUse Case
Online-Only❌ Nein (keine Daten)❌ NeinKeineSocial Media, News-Apps
Offline-Capable⚠️ Teilweise (cached data, read-only)❌ NeinEinfach (Cache)E-Commerce, Blogs
Offline-First✅ Voll funktionsfähig (read + write)✅ Ja (SQLite)Komplex (Sync + Conflict Resolution)Außendienst, Inventur, Field-Apps

Der entscheidende Unterschied:

  • Offline-Capable: App zeigt gecachte Daten (z.B. News-Artikel von gestern), aber User kann nichts hinzufügen/bearbeiten.
  • Offline-First: App funktioniert voll offline - User kann Daten erfassen, bearbeiten, löschen. Alles wird lokal gespeichert und später synchronisiert.

Beispiel Wächterkontrolle:

  • Online-Only: Wächter kann im Funkloch keine Daten erfassen ❌
  • Offline-Capable: Wächter kann alte Check-Ins sehen, aber keine neuen erstellen ⚠️
  • Offline-First: Wächter erfasst GPS, Fotos, NFC komplett offline - alles lokal in SQLite gespeichert, später synchronisiert ✅

Fazit: Wenn Ihre App im Außendienst funktionieren muss → Offline-First ist Pflicht (nicht Offline-Capable).


Wann brauchen Sie Offline-First?

✅ Offline-First ist PFLICHT:

  • Außendienst: Techniker, Wächter, Lieferanten (kein Netz garantiert)
  • Lagerhallen/Keller: Inventur, Barcode-Scanning (schlechter Empfang)
  • Feldforschung: Wissenschaftler, Geologen, Archäologen (remote Locations)
  • Healthcare: Notfall-Daten müssen immer abrufbar sein
  • B2B-Apps: Kunden erwarten 100% Verfügbarkeit

❌ Offline-First ist OVERKILL:

  • Social Media: User posten nur mit Netz
  • Streaming: Spotify/Netflix (Offline-Modus = Download, nicht Sync)
  • Booking/E-Commerce: Realtime-Verfügbarkeit wichtig (Race Conditions)

Decision Matrix

KriteriumOffline-FirstOnline-Only
User ist oft ohne Netz
Daten müssen sofort verfügbar sein
Write-Operations ohne Netz nötig
Realtime-Collaboration wichtig⚠️ Komplex
Budget +20% okay

Technische Grundlagen: Lokale Datenspeicherung

SQLite (Mobile: Android & iOS)

Was ist SQLite:

  • Embedded SQL-Datenbank (kein Server nötig)
  • 100% Offline, auf Gerät gespeichert
  • ACID-compliant (Transaktionen, Rollback)

Vorteile:

  • ✅ Schnell (native C-Library)
  • ✅ Kleine Footprint (500 KB)
  • ✅ Keine Netzwerk-Latenz

Nachteile:

  • ❌ Keine Verschlüsselung (Default)
  • ❌ Concurrent-Writes schwierig (1 Writer zur Zeit)

Use Case: Standard für Offline-Apps (90% aller Apps nutzen SQLite)


SQLCipher (Encrypted SQLite)

Was ist SQLCipher:

  • SQLite mit AES-256-GCM Verschlüsselung (authenticated encryption, seit SQLCipher 4+)
  • Drop-in Replacement (gleiche API wie SQLite)
  • Open Source (BSD-Lizenz)

Vorteile:

  • ✅ Verschlüsselte DB (DSGVO-konform)
  • ✅ Transparente Verschlüsselung (App merkt nichts)
  • ✅ Page-Level Encryption (jede Page einzeln verschlüsselt)

Nachteile:

  • ❌ 5-15% langsamer als SQLite (Encryption-Overhead)
  • ❌ Commercial Edition für Enterprise (kostenlos für Open Source)

Use Case: Banking, Health, B2B-Apps (sensible Daten)

Kosten: 0 € (Open Source), +1 Tag Setup


Hive (Flutter)

Was ist Hive:

  • NoSQL Key-Value Store (wie Redis, aber lokal)
  • Geschrieben in Dart (perfekt für Flutter)
  • Lazy-Loading, Type-Safe

Vorteile:

  • ✅ Schnell (keine SQL-Overhead)
  • ✅ Einfache API (weniger Boilerplate als SQLite)
  • ✅ Verschlüsselung inkludiert (HiveAES mit AES-256-CBC)

Nachteile:

  • ❌ Keine SQL-Queries (nur Key-Value Lookup)
  • ❌ Keine Relationen (wie MongoDB)

Use Case: Flutter-Apps mit einfachen Datenstrukturen (Settings, User-Profile)


IndexedDB (Web/PWA)

Was ist IndexedDB:

  • Browser-basierte NoSQL-DB (JavaScript API)
  • Asynchron (non-blocking)
  • Große Speicherkapazität (1-10 GB je nach Browser: Chrome/Edge 60%, Firefox 50%, Safari 1-10 GB)

Vorteile:

  • ✅ Standard in allen Browsern
  • ✅ Offline PWAs möglich
  • ✅ Keine Installation nötig

Nachteile:

  • ❌ Komplexe API (Callbacks, Promises, Transactions)
  • ❌ Keine Verschlüsselung (User kann lesen)
  • ❌ Storage-Limits (Browser-abhängig)

Use Case: Progressive Web Apps (Gmail, Google Docs Offline)


Realm (Alternative zu SQLite)

Was ist Realm:

  • Object-Oriented Database (kein SQL nötig)
  • Schneller bei Writes als SQLite
  • Realm Sync (Backend-as-a-Service für automatische Synchronisation)

Vorteile:

  • ✅ Schneller bei Writes (bis zu 10× schneller als SQLite)
  • ✅ Object-Oriented (kein SQL-Boilerplate)
  • ✅ Realm Sync (automatische Cloud-Synchronisation inkludiert)
  • ✅ Live Objects (Änderungen propagieren automatisch)

Nachteile:

  • ❌ Größerer Footprint (5 MB vs. 500 KB SQLite)
  • ❌ Kommerziell (Realm Sync ab 30 USD/Monat)
  • ❌ Vendor-Lock-in (MongoDB-Abhängigkeit)

Use Case: Apps mit komplexen Relationen, die Cloud-Sync brauchen (IoT-Apps, Healthcare)

Kosten: 0 € (lokale DB), +30-200 USD/Monat (Realm Sync Cloud)


Sync-Strategien: Wie kommen Daten ins Backend?

Problem: Conflict Resolution

Szenario:

  • User A bearbeitet Dokument offline
  • User B bearbeitet gleiches Dokument offline
  • Beide synchronisieren später → Konflikt!

Lösung: Sie brauchen eine Conflict Resolution Strategie.


Strategie 1: Last-Write-Wins (LWW)

Prinzip: Neuester Timestamp gewinnt.

WICHTIG: Serverzeit via NTP/Server-Zeit verwenden, nie Client-Zeit für Conflict Resolution! Client-Zeit nur für UI-Anzeige.

Beispiel:

// User A: Dokument bearbeitet um 10:00
{ id: 123, title: "Neuer Titel A", updated_at: "2025-01-15 10:00" }

// User B: Dokument bearbeitet um 10:05
{ id: 123, title: "Neuer Titel B", updated_at: "2025-01-15 10:05" }

// Sync: User B gewinnt (10:05 > 10:00)
// User A's Änderungen gehen verloren!

Vorteile:

  • ✅ Einfach zu implementieren (1 Entwicklertag)
  • ✅ Keine User-Interaktion nötig

Nachteile:

  • ❌ Datenverlust möglich (User A’s Änderungen weg)
  • ❌ Keine Merge-Logik

Use Case: Inventur-Apps (jedes Item hat nur 1 Owner), Fitness-Tracker (User bearbeitet nur eigene Daten)


Strategie 2: Operational Transformation (OT)

Prinzip: Jede Änderung wird als Operation gespeichert und transformiert.

Beispiel (Google Docs-Style):

// User A: "Hello" → "Hello World" (Insert "World" at Position 6)
Operation A: { type: "insert", pos: 6, text: " World" }

// User B: "Hello" → "Hi" (Delete 4 chars, Insert "i")
Operation B: { type: "delete", pos: 1, len: 4 }
             { type: "insert", pos: 1, text: "i" }

// Sync: Transform Operations → "Hi World"

Vorteile:

  • ✅ Kein Datenverlust (alle Änderungen werden merged)
  • ✅ Realtime-Collaboration möglich

Nachteile:

  • ❌ Komplex zu implementieren (5-10 Entwicklertage)
  • ❌ Fehleranfällig (Race Conditions)

Use Case: Collaborative Editing (Google Docs, Figma, Notion)


Strategie 3: CRDTs (Conflict-Free Replicated Data Types)

Prinzip: Datenstrukturen, die mathematisch garantiert konfliktfrei mergen.

Beispiel (Counter):

// User A: Counter = 5, +1 offline → 6
// User B: Counter = 5, +2 offline → 7

// Sync mit CRDT: 5 + 1 + 2 = 8 ✅
// (nicht Last-Write-Wins: 7 ❌)

Vorteile:

  • ✅ Garantiert konfliktfrei (mathematisch bewiesen)
  • ✅ Peer-to-Peer Sync möglich (kein Server nötig)

Nachteile:

  • ❌ Komplex: 5-8 Tage (mit Library wie Automerge/Yjs), 15-25 Tage (Custom-Implementierung)
  • ❌ Overhead (größere Payload)

Use Case: Distributed Systems (Redis, Riak), Collaborative Apps (Figma, Miro)


Strategie 4: Manual Conflict Resolution (User entscheidet)

Prinzip: Bei Konflikt → User-Dialog zeigen.

Beispiel:

Konflikt erkannt:
Version A (Sie): "Neuer Titel A"
Version B (Server): "Neuer Titel B"

[ ] Meine Version behalten
[ ] Server-Version übernehmen
[ ] Manuell mergen

Vorteile:

  • ✅ User hat Kontrolle (kein Datenverlust)
  • ✅ Einfach zu implementieren (2 Entwicklertage)

Nachteile:

  • ❌ User-Interaktion nötig (kann nerven)
  • ❌ Nicht für Realtime geeignet

Use Case: B2B-Apps (wichtige Dokumente), CRM-Apps (User will Kontrolle)


Welche Strategie wählen?

Use CaseEmpfohlene StrategieAufwand
Fitness-Tracker (User bearbeitet nur eigene Daten)Last-Write-Wins1 Tag
Inventur-App (1 User pro Item)Last-Write-Wins1 Tag
CRM (wichtige Daten)Manual Conflict Resolution2 Tage
Kollaboratives Dokument (Google Docs)Operational Transformation8-10 Tage
Distributed System (P2P Sync)CRDTs (mit Library)5-8 Tage

90% aller Apps: Last-Write-Wins + Timestamps reichen aus.


Sync-Performance optimieren: Delta-Sync

Problem: Volle Synchronisation bei großen Datenmengen dauert lange und verbraucht viel Traffic.

Lösung: Delta-Sync - Nur geänderte Daten synchronisieren.

Beispiel: Full-Sync vs. Delta-Sync

Full-Sync (ineffizient):

// ❌ Alle 10.000 Artikel herunterladen (50 MB)
final articles = await api.get('/articles');
await db.insertBatch('articles', articles);

Delta-Sync (effizient):

// ✅ Nur geänderte Artikel seit letztem Sync (250 KB)
final lastSync = await prefs.getString('last_sync');
final changes = await api.get('/articles?updated_since=$lastSync');

// Nur geänderte/neue Artikel einfügen
for (var article in changes) {
  await db.insertOrUpdate('articles', article);
}

// Timestamp speichern
await prefs.setString('last_sync', DateTime.now().toIso8601String());

Vorteile

  • 100× weniger Daten (50 MB → 250 KB bei 50 Änderungen von 10.000 Items)
  • 10× schneller (Full-Sync 30s → Delta-Sync 3s)
  • Weniger Traffic (wichtig für Mobile-Daten)
  • Geringerer Battery-Drain

Backend-Implementierung

// Backend-API mit updated_since Filter
GET /api/articles?updated_since=2025-01-15T10:00:00Z

// SQL-Query
SELECT * FROM articles
WHERE updated_at > '2025-01-15T10:00:00Z'
ORDER BY updated_at ASC

Kosten

  • Implementierung: +1-2 Entwicklertage (Backend + Client)
  • ROI: Rechnet sich ab 500+ Einträgen

SQLite-Performance optimieren

Problem: SQLite wird langsam bei 1000+ Einträgen ohne Optimierung.

Lösungen:

Indizes erstellen

// ✅ Index auf häufig gesuchte Spalten
await db.execute('CREATE INDEX idx_timestamp ON checkins(timestamp)');
await db.execute('CREATE INDEX idx_article_id ON scans(article_id)');

// Vor Index: Query 500ms
// Nach Index: Query 5ms (100× schneller)

WAL-Mode aktivieren

Was ist WAL (Write-Ahead Logging)?

  • Concurrent Reads während Writes möglich
  • Schnellere Writes (kein DB-Lock)
// WAL-Mode aktivieren (bei DB-Open)
await db.execute('PRAGMA journal_mode=WAL');

// Performance: +30-50% bei Concurrent Operations

VACUUM & ANALYZE

// DB komprimieren (nach vielen Deletes)
await db.execute('VACUUM');

// Query-Optimizer aktualisieren
await db.execute('ANALYZE');

// Tipp: 1× pro Woche im Background

Performance-Regeln

  • Indizes für WHERE/JOIN Spalten
  • WAL-Mode für Concurrent Access
  • Batch-Inserts (1000 einzelne Inserts = 10s, 1 Batch = 0.5s)
  • VACUUM nach 10.000+ Deletes
  • Keine Indizes auf kleine Tabellen (<100 Einträge = Overhead)

Kosten: +0.5-1 Tag (Indizes + WAL-Setup)


Offline-Testing: Tools & Best Practices

Problem: Offline-Szenarien sind schwer zu testen (Netz ein/aus, Sync-Konflikte simulieren).

Lösungen:

Airplane-Mode Simulator

// Mock für Netzwerk-Status (Testing)
class NetworkService {
  bool _isOffline = false;

  void setOfflineMode(bool offline) {
    _isOffline = offline;
  }

  Future<Response> get(String url) async {
    if (_isOffline) throw NoInternetException();
    return http.get(url);
  }
}

// Test: Offline-Modus simulieren
test('checkin works offline', () async {
  networkService.setOfflineMode(true);

  final checkin = await checkinService.create(data);

  expect(checkin.synced, false);
  expect(await db.query('checkins'), hasLength(1));
});

Conflict-Szenarien testen

// Test: 2 User bearbeiten gleiches Dokument
test('conflict resolution works', () async {
  // User A offline bearbeitet
  await dbA.update('docs', {'title': 'Version A'}, where: 'id = 1');

  // User B offline bearbeitet
  await dbB.update('docs', {'title': 'Version B'}, where: 'id = 1');

  // Beide syncen
  await syncA();
  await syncB();

  // Last-Write-Wins: Neuester Timestamp gewinnt
  final doc = await db.query('docs', where: 'id = 1');
  expect(doc.title, 'Version B'); // B hat neueren Timestamp
});

Tools

ToolUse CasePlatform
Charles ProxyNetzwerk-Throttling (3G, LTE simulieren)iOS/Android
Chrome DevToolsOffline-Mode + Storage-Limits testenPWA
Android Studio EmulatorAirplane-Mode ToggleAndroid
Xcode Network Link ConditionerLangsames Netz simuliereniOS

Test-Checkliste

  • App startet offline (keine Crash, gecachte Daten sichtbar)
  • Daten können offline erstellt werden (synced = false Flag)
  • Sync funktioniert nach Reconnect (alle unsyncten Daten hochgeladen)
  • Sync-Konflikte werden erkannt (Last-Write-Wins oder Manual Resolution)
  • Retry-Logic funktioniert (Exponential Backoff bei Fehlern)
  • Large-Dataset Performance (1000+ Einträge ohne UI-Freeze)

Kosten: +2-3 Entwicklertage (Offline-Tests schreiben)


Real Cases: Offline-First in der Praxis

Case 1: Wächterkontrollsystem

Herausforderung:

  • Wächter arbeitet in Gebäuden ohne Netz (Keller, Tiefgaragen)
  • GPS-Tracking, QR/NFC-Check-Ins, Fotos müssen erfasst werden
  • Totmann-Feature (Neigungssensor) muss offline funktionieren

Technische Lösung:

  • SQLCipher: GPS-Logs, Check-Ins, Fotos lokal verschlüsselt (nur notwendige Spalten, Fotos separat on-device)
  • Sync-Strategie: Last-Write-Wins (jeder Check-In hat ULID als Merge-Key, updated_at serverseitig)
  • Sync-Trigger: Auto bei WLAN + Ladezustand >30%; Fallback mit Exponential Backoff + Jitter (verhindert Battery-Drain & Thundering Herd)
  • Conflict-Free: Jeder Wächter bearbeitet nur eigene Daten

Implementierung:

// Check-In lokal speichern (SQLCipher)
await db.insert('checkins', {
  'id': uuid.v4(),
  'timestamp': DateTime.now().toIso8601String(),
  'latitude': gps.latitude,
  'longitude': gps.longitude,
  'nfc_tag_id': nfcTag,
  'synced': false, // Flag für Sync
});

// Sync-Logik (wenn Netz verfügbar)
Future<void> syncCheckins() async {
  final unsyncedCheckins = await db.query(
    'checkins',
    where: 'synced = ?',
    whereArgs: [false]
  );

  for (var checkin in unsyncedCheckins) {
    try {
      await api.post('/checkins', body: checkin);
      await db.update('checkins',
        {'synced': true},
        where: 'id = ?',
        whereArgs: [checkin['id']]
      );
    } catch (e) {
      // Retry später (Exponential Backoff)
    }
  }
}

Kosten Gesamt: 85.000 € (davon Offline-First: +13.000 € = +18%)

Ergebnis: Wächter können 100% offline arbeiten, Daten werden nachts im WLAN synchronisiert.


Case 2: Inventur-Scanner (Barcode/RFID)

Herausforderung:

  • Lagerhallen haben oft kein Netz (Beton, Metall)
  • 1000+ Artikel müssen gescannt werden (Barcode/RFID)
  • Mehrere Scanner parallel (Team-Inventur)

Technische Lösung:

  • SQLite: Artikel-Datenbank lokal (5-10 MB)
  • Sync-Strategie: Last-Write-Wins + Conflict-Detection
  • Batch-Sync: Alle 500 Scans → Backend (weniger API-Calls)
  • Conflict: Wenn 2 Scanner gleiches Item scannen → Flag “Überprüfung nötig”

Implementierung:

// Artikel-Stammdaten beim App-Start herunterladen
Future<void> downloadArticles() async {
  final articles = await api.get('/articles');
  await db.delete('articles'); // Clear old data
  await db.insertBatch('articles', articles);
}

// Scan lokal speichern
await db.insert('scans', {
  'article_id': barcode,
  'quantity': qty,
  'timestamp': DateTime.now().toIso8601String(),
  'scanner_id': deviceId,
  'synced': false,
});

// Batch-Sync (alle 500 Scans)
if (scanCount % 500 == 0) {
  await syncScans();
}

Conflict-Detection:

// Backend prüft: Wurde Item schon von anderem Scanner erfasst?
if (existingScan && existingScan.scanner_id !== scan.scanner_id) {
  // Flag für manuelle Überprüfung
  scan.needs_review = true;
}

Kosten Gesamt: 72.000 € (davon Offline-First: +12.000 € = +20%)

Ergebnis: Team kann parallel scannen, Konflikte werden markiert, keine Duplikate.


Case 3: Mensa-NFC-App (Check-in System)

Herausforderung:

  • 500+ Check-Ins pro Tag (Stoßzeit: 11:30-13:00)
  • NFC-Reader funktioniert auch ohne Internet
  • Master-Slave-Setup (Ampel-Status wird über Sockets verteilt)
  • Offline-Suche (Student-DB: 2000+ Einträge)

Technische Lösung:

  • SQLite: Student-DB lokal (Name, Matrikelnummer, Foto)
  • Sync-Strategie: Uni-directional (Check-Ins → Backend, Student-DB ← Backend)
  • Master-Slave: WebSockets für Realtime-Ampel (fallback: HTTP-Polling)
  • Offline-Suche: FTS5 (Full-Text-Search in SQLite)

Implementierung:

// Student-DB mit FTS5 (Full-Text-Search)
await db.execute('''
  CREATE VIRTUAL TABLE students_fts
  USING fts5(name, matrikel_nr, email)
''');

// Offline-Suche
final results = await db.query(
  'students_fts',
  where: 'students_fts MATCH ?',
  whereArgs: ['$query*'], // Prefix-Search
);

// NFC-Check-In (offline speichern)
await db.insert('checkins', {
  'student_id': student.id,
  'timestamp': DateTime.now().toIso8601String(),
  'status': ampelStatus, // grün/gelb/rot
  'synced': false,
});

// Sync im Hintergrund (non-blocking)
syncCheckins(); // Fire-and-forget

Kosten Gesamt: 45.000 € (davon Offline-First: +6.500 € = +17%)

Ergebnis: Check-In dauert <500ms (auch offline), Suche funktioniert instant, Sync läuft im Hintergrund.


Was kostet Offline-First wirklich?

Kosten-Übersicht

KomponenteAufwandKosten
SQLite-Setup1 Tag+1.200 €
SQLCipher-Setup (verschlüsselt)2 Tage+2.500 €
DAO/Repository-Layer2-3 Tage+3.000-4.000 €
Sync-Logik (Last-Write-Wins)3-4 Tage+4.000-5.000 €
Sync-Logik (Conflict Resolution)5-7 Tage+6.500-9.000 €
Batch-Sync + Retry-Logic2 Tage+2.500 €
Offline-Suche (FTS5)1-2 Tage+1.500-2.500 €
Testing (Sync-Szenarien)2-3 Tage+2.500-4.000 €

Gesamt (Standard-App mit Offline): +12.000-22.000 € (= +15-25% bei 60k Budget)

Nachträglich: +50.000-100.000 € (komplette Architektur-Umstellung)

Offline-First-Kosten nach App-Größe

App-TypBasis-BudgetOffline-First-AufschlagGesamt mit Offline
Simple App (MVP, 1-2 Features)20.000-30.000 €+3.000-5.000 € (+15-18%)23.000-35.000 €
Standard Business-App (3-5 Features)40.000-70.000 €+7.000-15.000 € (+17-22%)47.000-85.000 €
Komplexe App (Hardware, Multi-User)80.000-150.000 €+15.000-35.000 € (+18-25%)95.000-185.000 €

Faustformel: Offline-First kostet +15-25% des Basis-Budgets.


Top 5 Fehler bei Offline-Apps

Fehler: Timestamps nicht synchronisiert

Problem: Client-Timestamps sind oft falsch (User stellt Uhr um).

Lösung:

// ❌ Falsch: Client-Timestamp
final timestamp = DateTime.now(); // User-Gerät kann falsch sein

// ✅ Richtig: Server-Timestamp für Conflict Resolution
final serverTime = await api.getServerTime(); // NTP-synchronisiert
await db.insert('data', {
  'client_timestamp': DateTime.now(), // Für UI
  'server_timestamp': serverTime,     // Für Conflict Resolution
});

Fehler: Keine Sync-Status-UI

Problem: User weiß nicht, ob Daten synchronisiert sind.

Lösung:

// Sync-Status anzeigen
final unsyncedCount = await db.query('data', where: 'synced = false');

// UI: "5 Änderungen nicht synchronisiert"
if (unsyncedCount > 0) {
  showSyncBanner("$unsyncedCount Änderungen warten auf Sync");
}

Best Practice: Sync-Icon in der Navbar (grün = synced, gelb = pending, rot = error)

Offline-Indicator: UI-Best Practices

Pflicht-Elemente für Offline-Apps:

1. Netzwerk-Status-Banner

// Connectivity-Check (mit connectivity_plus Package)
StreamBuilder<ConnectivityResult>(
  stream: Connectivity().onConnectivityChanged,
  builder: (context, snapshot) {
    final isOffline = snapshot.data == ConnectivityResult.none;

    return isOffline
      ? Container(
          color: Colors.orange,
          padding: EdgeInsets.all(8),
          child: Row(
            children: [
              Icon(Icons.wifi_off, color: Colors.white),
              SizedBox(width: 8),
              Text('Offline-Modus - Daten werden später synchronisiert',
                   style: TextStyle(color: Colors.white)),
            ],
          ),
        )
      : SizedBox.shrink();
  },
)

2. Sync-Status-Indicator

// In AppBar/Navbar
IconButton(
  icon: Icon(
    syncStatus == SyncStatus.synced ? Icons.cloud_done :
    syncStatus == SyncStatus.pending ? Icons.cloud_upload :
    Icons.cloud_off,
    color: syncStatus == SyncStatus.synced ? Colors.green :
           syncStatus == SyncStatus.pending ? Colors.orange :
           Colors.red,
  ),
  onPressed: () => showSyncDetails(),
)

3. Item-Level Sync-Indicator

// Bei Listen: Zeige Status pro Item
ListTile(
  title: Text(checkin.title),
  trailing: checkin.synced
    ? Icon(Icons.check_circle, color: Colors.green, size: 16)
    : Icon(Icons.sync, color: Colors.orange, size: 16),
)

4. Manuelle Sync-Aktion

// Pull-to-Refresh für manuellen Sync
RefreshIndicator(
  onRefresh: () async {
    await syncService.syncNow();
  },
  child: ListView(...),
)

UI-Regeln:

  • Offline-Banner persistent (solange kein Netz)
  • Sync-Status immer sichtbar (Navbar-Icon)
  • Unsync-Count anzeigen (“5 Änderungen warten”)
  • Letzte Sync-Zeit anzeigen (“Zuletzt vor 5 Min.”)
  • Nicht: User blockieren (App muss offline funktionieren)
  • Nicht: Permanent-Notifications (nerven)

Kosten: +1 Entwicklertag (UI-Komponenten)


Fehler: Alle Daten herunterladen (Speicherplatz!)

Problem: 10.000 Artikel = 50 MB → App wird langsam.

Lösung:

// ❌ Falsch: Alle Artikel herunterladen
final articles = await api.get('/articles'); // 10k Artikel

// ✅ Richtig: Pagination + Lazy-Loading
final articles = await api.get('/articles?page=1&limit=100');

// Oder: Nur relevante Daten (z.B. User's Region)
final articles = await api.get('/articles?region=DE-BY');

Fehler: SQLite-Datenbank nicht migrieren

Problem: App-Update → neue Spalte → Crash bei alten Daten.

Lösung:

// Migration-System (sqflite-Migration)
await db.execute('ALTER TABLE users ADD COLUMN last_sync INTEGER');

// Oder: Schema-Versionierung
final version = await db.getVersion();
if (version < 2) {
  await db.execute('ALTER TABLE ...');
  await db.setVersion(2);
}

Fehler: Keine Retry-Logic bei Sync-Fehlern

Problem: Sync schlägt fehl (Netz weg) → Daten bleiben unsynced.

Lösung:

// Exponential Backoff
int retryCount = 0;
while (retryCount < 5) {
  try {
    await api.post('/sync', data);
    break; // Success
  } catch (e) {
    retryCount++;
    await Future.delayed(Duration(seconds: 2 ** retryCount)); // 2s, 4s, 8s, 16s, 32s
  }
}

Checkliste: Ist Ihre App bereit für Offline?

Discovery-Phase (vor Dev)

  • Use Case analysiert: Arbeitet User oft ohne Netz?
  • Datenvolumen geschätzt: Wie viele Daten lokal speichern? (<50 MB empfohlen)
  • Conflict-Strategie definiert: Last-Write-Wins oder Manual Resolution?
  • Sync-Trigger definiert: Manuell, Auto (WLAN), oder Realtime?

Dev-Phase

  • SQLite/SQLCipher Setup: Datenbank läuft lokal
  • DAO/Repository-Layer: Clean Architecture (Testbar)
  • Sync-Logik implementiert: Backend-API für Sync
  • Sync-Status-UI: User sieht, ob Daten synced sind
  • Retry-Logic: Exponential Backoff bei Fehlern
  • Migrations-System: Schema-Updates funktionieren

Testing

  • Offline-Szenarien getestet: App startet ohne Netz
  • Sync-Szenarien getestet: User A + B bearbeiten gleiches Dokument
  • Large-Dataset getestet: 1000+ Einträge in DB (Performance?)
  • Speicherplatz getestet: DB-Size auf Low-End-Geräten okay?

FAQs: Offline-Apps

SQLite vs. SQLCipher - was brauche ich?

Kurze Antwort: SQLCipher bei sensiblen Daten (Banking, Health, B2B).

Lange Antwort:

KriteriumSQLiteSQLCipher
Verschlüsselung❌ Keine✅ AES-256-CBC
PerformanceSchneller5-15% langsamer
DSGVO-Compliance⚠️ Klartext✅ Verschlüsselt
Kosten0 €0 € (Open Source)
Setup-Aufwand1 Tag2 Tage

Empfehlung: Bei B2B-Apps immer SQLCipher (auch wenn “nur” E-Mails gespeichert werden).


Wie groß darf die lokale Datenbank sein?

Kurze Antwort: <50 MB für Standard-Apps, <200 MB für komplexe Apps.

Lange Antwort:

GerätSQLite-LimitPraktisches Limit
iPhone (64 GB)~5 GB200 MB
Android (Low-End)~2 GB100 MB
iPad/Tablet~10 GB500 MB

Tipp: Artikel-Stammdaten (5-10 MB) okay, aber nicht alle Bilder herunterladen (lazy-load stattdessen).


Was ist die beste Sync-Strategie?

Kurze Antwort: 90% aller Apps: Last-Write-Wins reicht.

Lange Antwort:

  • Last-Write-Wins: Fitness-Tracker, Inventur, Field-Apps (User bearbeitet nur eigene Daten)
  • Manual Conflict Resolution: CRM, B2B-Apps (wichtige Daten, User will Kontrolle)
  • Operational Transformation: Google Docs, Figma (Realtime-Collaboration)
  • CRDTs: Distributed Systems, P2P-Apps (sehr selten nötig)

Entscheidung: Wenn User nur eigene Daten bearbeitet → Last-Write-Wins (einfach + günstig).


Kann ich PWA mit Offline-First bauen?

Kurze Antwort: Ja, mit IndexedDB + Service Workers.

Lange Antwort:

Vorteile:

  • ✅ Keine App-Store-Submission
  • ✅ Cross-Platform (iOS, Android, Desktop)
  • ✅ Offline-Capable (Service Workers)

Nachteile:

  • ❌ Keine Hardware-Zugriff (Barcode, NFC eingeschränkt; GPS funktioniert)
  • ❌ Storage-Limits (Browser-abhängig)
  • iOS PWAs: Kein Background-Sync/Push, strengere Storage-Limits, Barcode/NFC nur eingeschränkt

Empfehlung: PWA okay für Business-Tools (CRM, Tickets), aber nicht für Hardware-intensive Apps.

Service Workers für Offline-PWAs

Was sind Service Workers?

  • JavaScript-Proxy zwischen App und Server
  • Cachen von Assets (HTML, CSS, JS, Bilder)
  • Offline-Requests abfangen und aus Cache bedienen

Beispiel:

// service-worker.js
self.addEventListener('install', (event) => {
  event.waitUntil(
    caches.open('v1').then((cache) => {
      return cache.addAll([
        '/',
        '/index.html',
        '/app.js',
        '/styles.css',
        '/logo.png'
      ]);
    })
  );
});

// Requests abfangen: Cache-First-Strategie
self.addEventListener('fetch', (event) => {
  event.respondWith(
    caches.match(event.request).then((response) => {
      // Cache-Hit → aus Cache bedienen
      if (response) return response;

      // Cache-Miss → vom Server laden
      return fetch(event.request);
    })
  );
});

Cache-Strategien:

  • Cache-First: Offline-Priorität (App lädt auch ohne Netz)
  • Network-First: Aktualität-Priorität (Fallback auf Cache bei Netzfehler)
  • Stale-While-Revalidate: Cache sofort zeigen, im Hintergrund aktualisieren

Background Sync API (PWA)

Was ist Background Sync?

  • Automatische Synchronisation im Hintergrund (auch wenn Browser geschlossen)
  • ACHTUNG: Funktioniert NICHT auf iOS Safari (nur Chrome/Edge/Android)

Beispiel:

// Registrieren eines Sync-Events
navigator.serviceWorker.ready.then((registration) => {
  registration.sync.register('sync-checkins');
});

// Service Worker hört auf Sync-Event
self.addEventListener('sync', (event) => {
  if (event.tag === 'sync-checkins') {
    event.waitUntil(syncCheckinsToBackend());
  }
});

async function syncCheckinsToBackend() {
  const db = await openIndexedDB();
  const unsyncedCheckins = await db.getAll('checkins', 'synced', false);

  for (const checkin of unsyncedCheckins) {
    await fetch('/api/checkins', {
      method: 'POST',
      body: JSON.stringify(checkin)
    });
    await db.update('checkins', { ...checkin, synced: true });
  }
}

Limitation iOS:

  • Kein Background Sync API (User muss App öffnen zum Syncen)
  • Kein Background Fetch (keine Auto-Updates)
  • Alternative: Periodischer Sync beim App-Open (nicht ideal)

Was passiert bei App-Updates mit lokaler DB?

Kurze Antwort: Migrations-System nutzen (Schema-Versionierung).

Lange Antwort:

// Migration-System (sqflite)
onUpgrade: (db, oldVersion, newVersion) async {
  if (oldVersion < 2) {
    await db.execute('ALTER TABLE users ADD COLUMN last_sync INTEGER');
  }
  if (oldVersion < 3) {
    await db.execute('CREATE INDEX idx_timestamp ON checkins(timestamp)');
  }
}

Best Practice:

  • Migrations idempotent schreiben (mehrfach ausführbar ohne Fehler)
  • Versionen tracken: Eigene schema_migrations-Tabelle
  • Backup vor Upgrade: Lokaler Export/Backup erstellen
  • Migrations-Tests schreiben (alte DB-Version → neue Version)

Fazit: Offline-First ist kein Luxus

Die wichtigsten Learnings:

  • Offline-First kostet +15-25% (aber nachträglich +150-200%)
  • SQLite/SQLCipher reicht für 90% der Apps (einfach + robust)
  • Last-Write-Wins reicht meist (kein CRDT-Overkill nötig)
  • Entscheidung in Discovery-Phase (nicht nach 6 Monaten Dev)
  • Sync-Status-UI ist Pflicht (User muss sehen, was synced ist)
  • Migrations-System von Anfang an (spätere Schema-Änderungen)

Offline-First ist Foundation für robuste Business-Apps. Planen Sie es von Tag 1 ein, und Ihre App funktioniert - egal wo, egal wann.


Lassen Sie uns über Ihr Offline-Projekt sprechen

Sie brauchen eine App, die auch ohne Internet funktioniert? In einem kostenlosen Erstgespräch (30 Min):

  • ✅ Analysieren wir Ihren Use Case (brauchen Sie wirklich Offline-First?)
  • ✅ Definieren wir Sync-Strategie (Last-Write-Wins vs. Conflict Resolution)
  • ✅ Schätzen wir Datenvolumen (wie viel lokal speichern?)
  • ✅ Klären wir Kosten (+15-25% für Offline-First, transparent)

Keine Buzzwords, keine Angstmacherei - nur ehrliche Einschätzung aus 25+ Jahren Praxis.

Termin vereinbaren →


Weitere hilfreiche Artikel:

Ihr App-Projekt besprechen?

Lassen Sie uns in einem kostenlosen Erstgespräch über Ihre Anforderungen sprechen.

Jetzt Kontakt aufnehmen