Android Coden
Android 7 min lesen

Konfliktlösung bei der Android-Synchronisierung

Konfliktlösung entscheidet, welche Daten bei parallelen Änderungen gelten. Du lernst Regeln für Sync, Zeitstempel und Merging.

Konfliktlösung ist ein Kernproblem, sobald deine App Daten lokal speichert und später mit einem Server abgleicht. Du musst entscheiden, was passieren soll, wenn dieselbe Aufgabe, Notiz, Einstellung oder Bestellung auf dem Gerät und im Backend verändert wurde, bevor beide Seiten wieder synchron sind.

Was ist das?

Conflict Resolution bedeutet: Du definierst Regeln, wie deine App mit Sync-Konflikten umgeht. Ein Konflikt entsteht, wenn zwei Datenstände nicht mehr automatisch eindeutig zusammenpassen. Typisch ist eine Offline-First-App: Der Nutzer bearbeitet lokal einen Datensatz, während derselbe Datensatz auf einem anderen Gerät oder durch einen Serverprozess ebenfalls geändert wird. Beim nächsten Sync reicht ein einfaches Speichern nicht mehr, weil eine Seite sonst die andere überschreibt.

Das mentale Modell ist: Deine App verwaltet nicht nur Werte, sondern auch deren Herkunft und Änderungsverlauf. Ein Datensatz hat eine lokale Version, eine entfernte Version und oft Metadaten wie updatedAt, version, serverRevision oder eine Liste lokal ausstehender Änderungen. Konfliktlösung beantwortet dann die Frage: Welche Änderung gewinnt, welche wird zusammengeführt und wann muss der Nutzer entscheiden?

Im modernen Android-Projekt gehört diese Logik in die Datenschicht. Repositorys, lokale Datenquellen und Remote-Datenquellen sind dafür besser geeignet als ein Compose-Screen. Die UI soll anzeigen, dass ein Datensatz synchronisiert, zusammengeführt oder blockiert ist. Sie sollte aber nicht selbst entscheiden, ob ein Serverwert einen lokalen Wert überschreibt. So bleibt dein Code testbar, wiederverwendbar und näher an der Architektur, die Android für saubere Datenflüsse empfiehlt.

Wichtig ist auch: Konfliktlösung ist keine reine Technikfrage. Bei einem Chat kann „neueste Nachricht gewinnt“ passend sein. Bei einer Lieferadresse kann ein späterer Zeitstempel sinnvoll wirken, aber fachlich falsch sein, wenn eine Änderung vom Kundensupport Vorrang hat. Bei einem gemeinsamen Dokument willst du vielleicht Felder einzeln zusammenführen. Deshalb beginnt gute Konfliktlösung mit einer Produktregel, nicht mit einem cleveren Algorithmus.

Wie funktioniert es?

Die einfachste Strategie heißt oft „Last Write Wins“. Dabei gewinnt die Änderung mit dem neuesten Zeitstempel. Das ist leicht zu implementieren und für manche Daten ausreichend, etwa für einen Anzeigenamen oder eine lokale Sortierreihenfolge. Du vergleichst updatedAt von lokaler und entfernter Version und speicherst die neuere Variante. Der Nachteil: Du verlierst die ältere Änderung, selbst wenn sie fachlich wichtig war. Außerdem sind Zeitstempel nur so gut wie ihre Quelle. Geräteuhren können falsch sein, Zeitzonen können verwirren, und Millisekunden allein erklären nicht, warum etwas geändert wurde.

Stabiler ist eine serverseitige Revision oder Versionsnummer. Der Server vergibt zum Beispiel revision = 18. Wenn die App eine Änderung sendet, schickt sie mit: „Ich habe Revision 18 bearbeitet.“ Hat der Server inzwischen Revision 19, lehnt er das Update ab oder liefert beide Stände zurück. Deine App kann dann eine definierte Merge-Regel anwenden. Dieses Modell ist für Lernende gut verständlich: Du bearbeitest nicht „irgendeinen Datensatz“, sondern eine konkrete Version dieses Datensatzes.

Merging bedeutet, dass nicht zwangsläufig ein kompletter Datensatz gewinnt. Du kannst Felder getrennt betrachten. Beispiel: Lokal wurde der Titel einer Aufgabe geändert, auf dem Server wurde nur completed gesetzt. Dann kann die App beide Änderungen übernehmen. Schwieriger wird es, wenn beide Seiten denselben Titel verändert haben. Dann brauchst du eine Regel: lokale Änderung gewinnt, Server gewinnt, Nutzer entscheidet, oder du speicherst beide Varianten als Konfliktzustand.

Im Android-Alltag läuft das meist über einen Sync-Prozess: Eine lokale Datenbank, häufig Room, enthält den aktuellen UI-Stand und zusätzlich Sync-Metadaten. Ein Repository stellt der UI Flow-Daten bereit. Ein Worker, eine explizite Aktualisierung oder ein anderer Sync-Mechanismus lädt Remote-Änderungen, sendet lokale Änderungen und verarbeitet Konflikte. Compose beobachtet nur den Zustand. Wenn ein Konflikt nicht automatisch lösbar ist, zeigt die UI eine klare Auswahl an.

Eine gute Faustregel lautet: Automatisiere nur Konflikte, bei denen Datenverlust akzeptabel oder fachlich ausgeschlossen ist. Sobald beide Seiten dasselbe fachlich relevante Feld geändert haben, solltest du den Konflikt sichtbar machen oder eine sehr klare Produktregel besitzen. „Neuester Zeitstempel gewinnt“ ist eine technische Standardantwort, aber keine universelle Fachentscheidung.

Du solltest außerdem zwischen temporären Sync-Fehlern und echten Konflikten unterscheiden. Kein Netzwerk, ein Timeout oder ein Serverfehler ist noch kein Konflikt. Die App kann später erneut versuchen zu synchronisieren. Ein Konflikt liegt vor, wenn zwei gültige Änderungen nicht ohne Regel zusammenpassen. Diese Trennung hilft dir, Fehlerzustände in UI, Logs und Tests sauber zu benennen.

In der Praxis

Stell dir eine Aufgaben-App vor. Eine Aufgabe hat id, title, completed, updatedAt und revision. Der Nutzer ändert offline den Titel auf dem Smartphone. Gleichzeitig markiert er dieselbe Aufgabe auf dem Tablet als erledigt. Beim nächsten Sync bekommt das Smartphone die Serverversion zurück. Wenn beide Änderungen unterschiedliche Felder betreffen, kannst du sie zusammenführen.

Ein vereinfachtes Modell kann so aussehen:

data class Task(
    val id: String,
    val title: String,
    val completed: Boolean,
    val updatedAtMillis: Long,
    val revision: Long
)

data class TaskChange(
    val baseRevision: Long,
    val newTitle: String?,
    val newCompleted: Boolean?,
    val changedAtMillis: Long
)

sealed interface MergeResult {
    data class Merged(val task: Task) : MergeResult
    data class Conflict(
        val local: TaskChange,
        val remote: Task
    ) : MergeResult
}

fun mergeTask(
    localChange: TaskChange,
    remoteTask: Task
): MergeResult {
    if (localChange.baseRevision == remoteTask.revision) {
        return MergeResult.Merged(
            remoteTask.copy(
                title = localChange.newTitle ?: remoteTask.title,
                completed = localChange.newCompleted ?: remoteTask.completed,
                updatedAtMillis = maxOf(localChange.changedAtMillis, remoteTask.updatedAtMillis),
                revision = remoteTask.revision + 1
            )
        )
    }

    val changesTitle = localChange.newTitle != null
    val changesCompleted = localChange.newCompleted != null

    return if (changesTitle && changesCompleted) {
        MergeResult.Conflict(localChange, remoteTask)
    } else {
        MergeResult.Merged(
            remoteTask.copy(
                title = localChange.newTitle ?: remoteTask.title,
                completed = localChange.newCompleted ?: remoteTask.completed,
                updatedAtMillis = maxOf(localChange.changedAtMillis, remoteTask.updatedAtMillis),
                revision = remoteTask.revision + 1
            )
        )
    }
}

Der Code ist bewusst klein gehalten. In einer echten App würdest du die Revision wahrscheinlich vom Server vergeben lassen und nicht lokal erhöhen. Trotzdem zeigt das Beispiel den entscheidenden Gedanken: Du prüfst zuerst, auf welcher Basis die lokale Änderung entstanden ist. Danach unterscheidest du, ob die Änderungen gemeinsam angewendet werden können oder ob ein Konfliktzustand entsteht.

In deinem Repository könnte der Ablauf so aussehen: Die UI schreibt eine lokale Änderung in die Datenbank und markiert sie als „pending“. Der Sync lädt die aktuelle Serverversion. Dann ruft er eine Merge-Funktion auf. Bei Merged wird der zusammengeführte Stand gespeichert und später an den Server gesendet oder mit dessen Antwort bestätigt. Bei Conflict bleibt die lokale Änderung erhalten, und die UI zeigt dem Nutzer beide Varianten. Dieser Zustand ist Teil deines Datenmodells, nicht nur ein Toast oder ein kurzer Dialog.

Eine typische Stolperfalle ist das stille Überschreiben. Du sendest ein PUT /tasks/{id} mit dem kompletten lokalen Objekt, ohne Revision oder Änderungsbasis. Der Server speichert es, und eine andere Änderung ist weg. In Tests fällt das oft nicht auf, weil du nur den einfachen Online-Fall prüfst. Prüfe deshalb mindestens diese Fälle: lokal geändert und Server unverändert, Server geändert und lokal unverändert, beide Seiten ändern unterschiedliche Felder, beide Seiten ändern dasselbe Feld.

Auch Zeitstempel brauchen Disziplin. Wenn du sie nutzt, entscheide, ob sie vom Server oder vom Gerät kommen. Für Konfliktregeln sind Serverzeitstempel meist robuster, weil alle Clients dieselbe Quelle verwenden. Gerätezeitstempel sind für UI-Hinweise nützlich, etwa „vor 2 Minuten bearbeitet“, aber als alleinige Konfliktentscheidung riskant. Falls du dennoch lokale Zeit nutzt, dokumentiere die Grenze klar im Code-Review.

Für Tests kannst du die Merge-Logik als reine Kotlin-Funktion halten. Das ist ein Vorteil: Du brauchst keinen Emulator, keine Datenbank und kein Netzwerk, um die wichtigste Regel zu prüfen. Unit-Tests können konkrete lokale und entfernte Stände aufbauen und das Ergebnis vergleichen. Ergänzend helfen Integrationstests für den Repository-Ablauf: Wird ein Konflikt korrekt gespeichert? Zeigt die UI einen lösbaren Zustand? Wird ein erneuter Sync nach der Nutzerentscheidung sauber verarbeitet?

Code-Reviews sind bei Konfliktlösung besonders wertvoll. Bitte andere Entwickler nicht nur zu prüfen, ob der Code kompiliert, sondern ob die Regel fachlich verständlich ist. Eine gute Review-Frage lautet: „Welche Änderung geht hier verloren, und ist das beabsichtigt?“ Wenn niemand diese Frage klar beantworten kann, ist die Strategie noch nicht reif genug.

Fazit

Konfliktlösung sorgt dafür, dass Offline-First-Apps nicht nur Daten speichern, sondern Änderungen verlässlich versöhnen. Für dich als Android-Entwickler heißt das: Lege die Regeln in der Datenschicht ab, arbeite mit Revisionen oder klar definierten Zeitstempeln, und unterscheide automatische Merges von echten Konflikten. Übe das Thema mit einer kleinen Repository-Funktion, schreibe Tests für mehrere Sync-Szenarien und prüfe im Debugger, welcher lokale und entfernte Stand in deine Merge-Regel hineingeht. So erkennst du früh, ob deine App Daten schützt oder unbeabsichtigt überschreibt.

Quellen (5)
Redaktion

Geschrieben von

Redaktion

Das Redaktionsteam recherchiert und schreibt Artikel zu aktuellen Themen rund um Tech, Lifestyle und Ratgeber.