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
| Ansatz | Funktioniert ohne Netz? | Daten schreiben offline? | Sync-Logik | Use Case |
|---|---|---|---|---|
| Online-Only | ❌ Nein (keine Daten) | ❌ Nein | Keine | Social Media, News-Apps |
| Offline-Capable | ⚠️ Teilweise (cached data, read-only) | ❌ Nein | Einfach (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
| Kriterium | Offline-First | Online-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 Case | Empfohlene Strategie | Aufwand |
|---|---|---|
| Fitness-Tracker (User bearbeitet nur eigene Daten) | Last-Write-Wins | 1 Tag |
| Inventur-App (1 User pro Item) | Last-Write-Wins | 1 Tag |
| CRM (wichtige Daten) | Manual Conflict Resolution | 2 Tage |
| Kollaboratives Dokument (Google Docs) | Operational Transformation | 8-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
| Tool | Use Case | Platform |
|---|---|---|
| Charles Proxy | Netzwerk-Throttling (3G, LTE simulieren) | iOS/Android |
| Chrome DevTools | Offline-Mode + Storage-Limits testen | PWA |
| Android Studio Emulator | Airplane-Mode Toggle | Android |
| Xcode Network Link Conditioner | Langsames Netz simulieren | iOS |
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
| Komponente | Aufwand | Kosten |
|---|---|---|
| SQLite-Setup | 1 Tag | +1.200 € |
| SQLCipher-Setup (verschlüsselt) | 2 Tage | +2.500 € |
| DAO/Repository-Layer | 2-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-Logic | 2 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-Typ | Basis-Budget | Offline-First-Aufschlag | Gesamt 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:
| Kriterium | SQLite | SQLCipher |
|---|---|---|
| Verschlüsselung | ❌ Keine | ✅ AES-256-CBC |
| Performance | Schneller | 5-15% langsamer |
| DSGVO-Compliance | ⚠️ Klartext | ✅ Verschlüsselt |
| Kosten | 0 € | 0 € (Open Source) |
| Setup-Aufwand | 1 Tag | 2 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ät | SQLite-Limit | Praktisches Limit |
|---|---|---|
| iPhone (64 GB) | ~5 GB | 200 MB |
| Android (Low-End) | ~2 GB | 100 MB |
| iPad/Tablet | ~10 GB | 500 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.
Weitere hilfreiche Artikel:
- App-Sicherheit & DSGVO: SQLCipher für verschlüsselte DBs
- Business-App-Entwicklung: Offline-First für Außendienst
- MVP-Entwicklung: Offline-First von Anfang an
- Flutter App-Entwicklung: Hive vs. SQLite
- Native vs. Cross-Platform: Performance bei Offline-Apps
- App-Entwicklung Kosten: Was kostet Offline-First?
- App erstellen lassen: Discovery bis Sync-Strategie
- Android App-Entwicklung: Room vs. SQLite
- iOS App-Entwicklung: Core Data vs. SQLite
- App-Wartung & Support: DB-Migrations testen
- App-Entwickler finden: Offline-First-Expertise prüfen
- Agentur oder Freelancer: Wer kann Offline-First?
Ihr App-Projekt besprechen?
Lassen Sie uns in einem kostenlosen Erstgespräch über Ihre Anforderungen sprechen.
Jetzt Kontakt aufnehmen