Offline-first achtergrond-synchronisatie voor mobiele apps: conflicten, logica voor opnieuw proberen en UX
Plan offline-first achtergrond-synchronisatie voor mobiele apps met duidelijke conflicregels, retry-logica en een eenvoudige pending-changes UX voor native Kotlin- en SwiftUI-apps.

Het probleem: gebruikers bewerken offline en de werkelijkheid verandert
Iemand begint aan een taak met een goede verbinding en loopt daarna een lift, een hoek van een magazijn of een metrotunnel in. De app blijft draaien, dus ze blijven werken. Ze tikken op Opslaan, voegen een notitie toe, wijzigen een status of maken misschien zelfs een nieuw record. Alles lijkt goed omdat het scherm meteen bijwerkt.
Later komt de verbinding terug en probeert de app op de achtergrond bij te werken. Daar kan achtergrond-synchronisatie mensen verrassen.
Als de app niet zorgvuldig is, kan dezelfde actie twee keer worden verzonden (duplicaten), of een nieuwere wijziging vanaf de server kan overschrijven wat de gebruiker net deed (verloren bewerkingen). Soms toont de app verwarrende staten zoals “Opgeslagen” en “Niet opgeslagen” tegelijk, of verschijnt een record, verdwijnt en verschijnt weer na synchronisatie.
Een conflict is eenvoudig: twee verschillende wijzigingen zijn gemaakt aan hetzelfde ding voordat de app ze kon reconciliëren. Bijvoorbeeld: een supportmedewerker zet de prioriteit van een ticket op Hoog terwijl hij offline is, maar een collega online sluit het ticket. Als de offline telefoon opnieuw verbinding krijgt, kunnen beide wijzigingen niet netjes worden toegepast zonder een regel.
Het doel is niet om offline perfect te laten voelen. Het doel is voorspelbaarheid:
- Mensen kunnen blijven werken zonder bang te zijn hun werk te verliezen.
- Synchronisatie gebeurt later zonder mysterieuze duplicaten.
- Als iets aandacht nodig heeft, zegt de app duidelijk wat er gebeurde en wat te doen.
Dit geldt of je nu handmatig code schrijft in Kotlin/SwiftUI of native apps bouwt met een no-code platform zoals AppMaster. Het lastige is niet de UI-widgets. Het is beslissen hoe de app zich gedraagt wanneer de wereld verandert terwijl de gebruiker offline is.
Een simpel offline-first model (zonder jargon)
Een offline-first app gaat ervan uit dat de telefoon soms het netwerk verliest, maar de app nog steeds bruikbaar moet aanvoelen. Schermen moeten laden en knoppen werken, ook als de server niet bereikbaar is.
Vier termen dekken het meeste:
- Lokale cache: data opgeslagen op het apparaat zodat de app direct iets kan tonen.
- Sync-queue: een lijst met acties die de gebruiker uitvoerde terwijl hij offline was (of terwijl de verbinding onstabiel was).
- Server-truth: de versie op de backend die uiteindelijk door iedereen wordt gedeeld.
- Conflict: wanneer de geplaatste wijziging van de gebruiker niet meer netjes toepasbaar is omdat de server-versie veranderde.
Een handig mentaal model is lezen en schrijven te scheiden.
Lezen is meestal rechttoe rechtaan: toon de beste beschikbare data (vaak uit de lokale cache) en ververst stilletjes wanneer de verbinding terugkomt.
Schrijven is anders. Vertrouw niet op “het hele record opslaan” in één keer. Dat faalt zodra je offline bent.
Registreer in plaats daarvan wat de gebruiker deed als kleine items in een wijzigingslog. Bijvoorbeeld: “zet status op Goedgekeurd”, “voeg opmerking X toe”, “wijzig hoeveelheid van 2 naar 3.” Elk item gaat met tijdstempel en ID in de sync-queue. Achtergrond-synchronisatie probeert die vervolgens te leveren.
De gebruiker blijft werken terwijl wijzigingen van pending naar synced verschuiven.
Als je een no-code platform zoals AppMaster gebruikt, wil je nog steeds dezelfde bouwstenen: gecachte reads voor snelle schermen en een duidelijke queue van gebruikersacties die opnieuw geprobeerd, samengevoegd of gemarkeerd kan worden bij een conflict.
Bepaal wat echt offline ondersteuning nodig heeft
Offline-first kan klinken als “alles werkt zonder verbinding”, maar die belofte is waar veel apps in de problemen komen. Kies de onderdelen die echt profiteren van offline-ondersteuning en houd de rest duidelijk online-only.
Denk in termen van gebruikersintentie: wat moeten mensen kunnen doen in een kelder, in een vliegtuig of in een magazijn met slechte dekking? Een goed uitgangspunt is acties ondersteunen die dagelijks werk creëren of bijwerken, en acties blokkeren waarvoor “laatste waarheid” echt moet worden gecontroleerd.
Praktisch gezien omvatten offline-vriendelijke acties vaak het aanmaken en bewerken van kernrecords (notities, taken, inspecties, tickets), het opstellen van reacties en het bijvoegen van foto’s (lokaal opgeslagen en later geüpload). Verwijderen kan ook, maar veiliger als soft delete met een undo-venster tot de server het bevestigt.
Bepaal nu wat realtime moet blijven omdat het risico te groot is. Betalingen, permissiewijzigingen, goedkeuringen en alles wat gevoelige data betreft, moet meestal een verbinding vereisen. Als de server moet controleren of een actie valide is, sta die actie niet offline toe. Toon een duidelijke “verbinding vereist” melding, geen mysterieuze fout.
Stel verwachtingen voor frisheid vast. “Offline” is niet binair. Definieer hoe verouderd data mag zijn: minuten, uren of “de volgende keer dat de app opent.” Zet die regel in de UI in gewone taal, zoals “Laatst bijgewerkt 2 uur geleden” en “Synchroniseert bij verbinding.”
Markeer ten slotte data met hoge conflictkans vroeg. Voorraadtellingen, gedeelde taken en teamberichten trekken vaak conflicten omdat meerdere mensen snel bewerken. Overweeg voor die gevallen offline bewerkingen te beperken tot concepten, of wijzigingen als losse gebeurtenissen vast te leggen in plaats van één waarde te overschrijven.
Als je in AppMaster bouwt, helpt deze stap je data en businessregels te modelleren zodat de app veilige concepten offline kan bewaren en risicovolle acties online-only blijven.
Ontwerp de sync-queue: wat je voor elke wijziging opslaat
Als een gebruiker offline werkt, probeer dan niet de hele database te synchroniseren. Synchroniseer de acties van de gebruiker. Een duidelijke actielijst is de ruggengraat van achtergrond-sync en blijft te begrijpen als er iets misgaat.
Houd acties klein en menselijk, afgestemd op wat de gebruiker daadwerkelijk deed:
- Maak een record aan
- Werk specifieke veld(en) bij
- Wijzig status (indienen, goedkeuren, archiveren)
- Verwijderen (bij voorkeur soft delete tot bevestigd)
Kleine acties zijn makkelijker te debuggen. Als support iemand moet helpen, is “Wijzigde status Concept -> Ingediend” veel handiger dan een enorme JSON-verandering bekijken.
Voor elke geplaatste actie sla voldoende metadata op om het veilig te kunnen afspelen en conflicten te detecteren:
- Record-id (en een tijdelijke lokale ID voor gloednieuwe records)
- Actietijdstempel en apparaat-id
- Verwachte versie (of laatst bekende bijwerktijd) van het record
- Payload (de specifieke gewijzigde velden, plus oude waarde als je die kunt bewaren)
- Idempotency key (een unieke actie-ID zodat retries geen duplicaten maken)
Die verwachte versie is de sleutel voor eerlijke conflictafhandeling. Als de server-versie is opgeschoven, kun je pauzeren en om een beslissing vragen in plaats van iemand anders stilletjes te overschrijven.
Sommige acties moeten samen worden toegepast omdat de gebruiker ze als één stap ziet. Bijvoorbeeld: “Bestelling aanmaken” plus “drie regels toevoegen” moet slagen of falen als geheel. Sla een group ID (of transactie-ID) op zodat de sync-engine ze samen kan verzenden en alles commit of alles pending houdt.
Of je dit nu handmatig bouwt of in AppMaster, het doel is hetzelfde: elke wijziging wordt één keer vastgelegd, veilig afgespeeld en verklaarbaar wanneer iets niet klopt.
Conflictresolutieregels die je aan gebruikers kunt uitleggen
Conflicten zijn normaal. Het doel is ze zeldzaam, veilig en makkelijk uitlegbaar te maken wanneer ze gebeuren.
Noem het moment dat een conflict optreedt: de app stuurt een wijziging en de server zegt: “Dat record is gewijzigd sinds jij begon met bewerken.” Daarom is versiebeheer belangrijk.
Houd twee waarden bij elk record:
- Server-versie (de huidige versie op de server)
- Verwachte versie (de versie waarvan de telefoon dacht dat die bewerkt werd)
Als de verwachte versie overeenkomt, accepteer de update en verhoog de server-versie. Als dat niet het geval is, pas je je conflictregel toe.
Kies een regel per datatypes (niet één regel voor alles)
Verschillende data hebben verschillende regels. Een statusveld is niet hetzelfde als een lange notitie.
Regels die gebruikers doorgaans begrijpen:
- Last write wins: prima voor laag-risico velden zoals weergavevoorkeuren.
- Velden samenvoegen: het beste wanneer velden onafhankelijk zijn (status vs notities).
- Vraag de gebruiker: het beste voor hoog-risico bewerkingen zoals prijs, permissies of totalen.
- Server wint met een kopie: bewaar de serverwaarde, maar sla de gebruikerswijziging als concept op zodat die opnieuw toegepast kan worden.
In AppMaster mappen deze regels goed naar visuele logica: controleer versies, vergelijk velden en kies dan het pad.
Bepaal hoe deletes zich gedragen (anders verlies je data)
Verwijderen is het lastige geval. Gebruik een tombstone (een “verwijderd” marker) in plaats van het record direct te verwijderen. Bepaal vervolgens wat er gebeurt als iemand een record bewerkt dat elders is verwijderd.
Een duidelijke regel is: “Deletes winnen, maar je kunt herstellen.” Voorbeeld: een verkoper bewerkt offline een klantnotitie terwijl een admin die klant verwijdert. Bij sync toont de app “Klant is verwijderd. Herstellen om je notitie toe te passen?” Dit voorkomt stil verlies en houdt de controle bij de gebruiker.
Retries en foutstaten: houd het voorspelbaar
Als sync faalt, geven de meeste gebruikers niet om waarom. Ze willen weten of hun werk veilig is en wat er vervolgens gebeurt. Een voorspelbare set staten voorkomt paniek en supporttickets.
Begin met een kleine, zichtbare statusmodel en houd het consistent over schermen:
- Queued: opgeslagen op het apparaat, wachtend op netwerk
- Syncing: nu verzenden
- Sent: bevestigd door de server
- Failed: kon niet verzenden, zal opnieuw proberen of heeft aandacht nodig
- Needs review: verzonden, maar de server weigerde of markeerde het
Retries moeten spaarzaam zijn voor batterij en data. Gebruik eerst snelle retries (voor korte signaalverliesjes), en vertraag daarna. Een eenvoudige backoff zoals 1 min, 5 min, 15 min, dan elk uur is makkelijk te begrijpen. Herprobeer ook alleen wanneer het zin heeft (probeer niet steeds een wijziging die ongeldig is opnieuw te verzenden).
Behandel fouten verschillend, omdat de volgende stap anders is:
- Offline / geen netwerk: blijf queued, probeer opnieuw wanneer online
- Timeout / server onbereikbaar: markeer failed, auto-retry met backoff
- Auth verlopen: pauzeer sync en vraag de gebruiker opnieuw in te loggen
- Validatie mislukt (ongeldige invoer): needs review, toon wat te repareren
- Conflict (record gewijzigd): needs review, routeer naar je conflictoplossingsregels
Idempotentie is wat retries veilig maakt. Elke wijziging moet een unieke actie-ID hebben (vaak een UUID) die met het verzoek wordt meegestuurd. Als de app dezelfde wijziging opnieuw stuurt, moet de server die ID herkennen en hetzelfde resultaat teruggeven in plaats van duplicaten te maken.
Voorbeeld: een technicus slaat een afgerond werk offline op en gaat vervolgens in een lift. De app stuurt de update, het time-out en probeert later opnieuw. Met een actie-ID is de tweede verzending onschadelijk. Zonder ID kun je dubbele “afgewerkt” gebeurtenissen krijgen.
In AppMaster behandel je deze staten en regels als eersteklas velden en logica in je sync-proces, zodat je Kotlin- en SwiftUI-apps overal hetzelfde gedrag vertonen.
Pending changes UX: wat de gebruiker ziet en kan doen
Mensen moeten zich veilig voelen bij gebruik van de app offline. Goede “pending changes” UX is rustig en voorspelbaar: het erkent dat werk op het apparaat is opgeslagen en maakt de volgende stap duidelijk.
Een subtiele indicator werkt beter dan een waarschuwingsbanner. Toon bijvoorbeeld een klein “Synchroniseert” icoon in de header, of een rustige “3 wachtend” aanduiding op het scherm waar bewerkingen gebeuren. Gebruik alarmerende kleuren alleen voor echt gevaar (zoals “kan niet uploaden omdat je bent uitgelogd”).
Geef gebruikers één plek om te begrijpen wat er gebeurt. Een simpele Outbox- of Pending-changes-pagina kan items lijst met platte taal als “Opmerking toegevoegd aan Ticket 104” of “Profielfoto bijgewerkt.” Die transparantie voorkomt paniek en vermindert supportvragen.
Wat gebruikers kunnen doen
De meeste mensen hebben maar een paar acties nodig, en die moeten consistent zijn door de hele app:
- Nu opnieuw proberen
- Opnieuw bewerken (maakt een nieuwere wijziging)
- Lokale wijziging weggooien
- Details kopiëren (handig bij het rapporteren van een probleem)
Houd statusteksten simpel: Pending, Syncing, Failed. Als iets faalt, leg het uit zoals een mens zou doen: “Kon niet uploaden. Geen internet.” of “Afgewezen omdat dit record door iemand anders is veranderd.” Vermijd foutcodes.
Blokkeer niet de hele app
Blokkeer alleen acties die echt online moeten zijn, zoals “Betaal met Stripe” of “Nodig een nieuwe gebruiker uit.” Alles behalve dat moet blijven werken, inclusief het bekijken van recente data en het maken van nieuwe concepten.
Een realistische flow: een velddienst-medewerker bewerkt een werkrapport in een kelder. De app toont “1 wachtend” en laat hem verder werken. Later verandert dat in “Synchroniseert” en wordt het automatisch gewist. Als het faalt, blijft het werkrapport beschikbaar, gemarkeerd als “Mislukt”, met een enkele “Nu opnieuw proberen” knop.
Als je in AppMaster bouwt, modelleer deze staten als onderdeel van elk record (pending, failed, synced) zodat de UI ze overal zonder speciale uitzondering kan tonen.
Authenticatie, permissies en veiligheid offline
Offline verandert je beveiligingsmodel. Een gebruiker kan acties ondernemen zonder verbinding, maar je server blijft de waarheid. Behandel elke geplaatste wijziging als “aangevraagd”, niet als “goedgekeurd”.
Token-verloop terwijl je offline bent
Tokens verlopen. Als dat offline gebeurt, laat de gebruiker dan doorgaan met het maken van bewerkingen en sla die als pending op. Geef niet de indruk dat acties die serverbevestiging vereisen klaar zijn. Markeer ze als pending tot een succesvolle auth-refresh.
Wanneer de app weer online is, probeer eerst stil een refresh. Als je de gebruiker moet vragen opnieuw in te loggen, doe het één keer en hervat dan automatisch sync.
Na opnieuw inloggen, valideer elk queued item opnieuw voordat je het verzendt. De identiteit van de gebruiker kan veranderd zijn (gedeeld apparaat) en oude bewerkingen mogen niet onder het verkeerde account synchroniseren.
Permissiewijzigingen en verboden acties
Permissies kunnen veranderen terwijl een gebruiker offline is. Een bewerking die gisteren mocht, kan vandaag verboden zijn. Handel hier expliciet voor:
- Controleer server-side permissies voor elk queued item
- Als het verboden is, stop dat item en toon een duidelijke reden
- Bewaar de lokale bewerking zodat de gebruiker het kan kopiëren of toegang kan aanvragen
- Vermijd herhaalde retries voor “forbidden” fouten
Voorbeeld: een supportmedewerker bewerkt offline een klantnotitie tijdens een vlucht. ‘s Nachts wordt zijn rol ingetrokken. Bij sync weigert de server de update. De app moet tonen “Kon niet uploaden: je hebt geen toegang meer” en de notitie als lokaal concept bewaren.
Sensitieve data offline opslaan
Sla alleen het minimum op om schermen te tonen en de queue af te spelen. Versleutel offline opslag, vermijd het cachen van geheimen en stel duidelijke regels voor uitloggen (bijv. lokale data wissen, of concepten alleen bewaren na expliciete toestemming). Als je met AppMaster bouwt, begin met zijn authenticatiemodule en ontwerp je queue zo dat die altijd op een geldige sessie wacht voordat wijzigingen worden verzonden.
Veelvoorkomende valkuilen die werkverlies of duplicaten veroorzaken
De meeste offline-bugs zijn niet spectaculair. Ze komen door een paar keuzes die onschuldig lijken bij testen op perfecte Wi‑Fi, maar in het echte leven breken.
Een veelvoorkomende fout is stille overschrijving. Als de app een oudere versie uploadt en de server die accepteert zonder controle, kun je iemands nieuwere bewerking wissen en merkt niemand het tot het te laat is. Synchroniseer met een versienummer (of een updatedAt-tijdstempel) en weiger te overschrijven als de server is opgeschoven, zodat de gebruiker een duidelijke keuze krijgt.
Een andere valkuil is een retry-storm. Als een telefoon een zwakke verbinding terugkrijgt, kan de app elke paar seconden de backend bestoken, wat batterij slurpt en duplicaten creëert. Retries moeten rustig zijn: vertraag na elke fout en voeg wat randomisatie toe zodat duizenden apparaten niet exact tegelijk opnieuw proberen.
De fouten die het vaakst leiden tot verloren werk of duplicaten:
- Elke fout als “netwerk” behandelen: scheid permanente fouten (ongeldige data, ontbrekende permissie) van tijdelijke (timeout).
- Sync-fouten verbergen: als mensen niet zien wat faalde, doen ze de taak opnieuw en maken twee records.
- Dezelfde wijziging twee keer sturen zonder bescherming: voeg altijd een unieke request-ID toe zodat de server duplicaten kan negeren.
- Tekstvelden automatisch samenvoegen zonder iemand te informeren: als je automatisch samenvoegt, laat gebruikers het resultaat beoordelen als het belangrijk is.
- Records offline aanmaken zonder een stabiele ID: gebruik een tijdelijke lokale ID en koppel die aan de server-ID na upload, zodat latere bewerkingen geen tweede kopie maken.
Een kort voorbeeld: een velduitvoerder maakt offline een nieuwe “Site Visit”, bewerkt die daarna twee keer voordat er verbinding is. Als de create-aanroep wordt herhaald en twee server-records aanmaakt, kunnen latere bewerkingen aan de verkeerde record vastzitten. Stabiele ID's en server-side deduplicatie voorkomen dit.
Als je dit met AppMaster bouwt, veranderen de regels niet. Het verschil is waar je ze toepast: in je sync-logica, je datamodel en de schermen die “mislukt” vs “verzonden” laten zien.
Voorbeeldscenario: twee mensen bewerken hetzelfde record
Een velduitvoerder, Maya, werkt aan ticket “Job #1842” in een kelder zonder signaal. Ze zet de status van “In uitvoering” naar “Voltooid” en voegt een notitie toe: “Klep vervangen, getest OK.” De app slaat dit meteen op en toont het als pending.
Beneden op kantoor bewerkt haar collega Leo hetzelfde werk en is online. Hij wijzigt de geplande tijd en wijst het werk toe aan een andere technicus, omdat een klant een update gaf.
Als Maya weer bereik heeft, start achtergrond-sync stilletjes. Dit is een voorspelbare, gebruiksvriendelijke flow:
- Maya’s wijziging staat nog in de sync-queue (job-id, gewijzigde velden, tijdstempel en de recordversie die ze laatst zag).
- De app probeert te uploaden. De server antwoordt: “Dit werk is bijgewerkt sinds jouw versie” (conflict).
- Je conflictregel draait: status en notities kunnen worden samengevoegd, maar toewijzingswijzigingen winnen als die later op de server gebeurden.
- De server accepteert een samengevoegd resultaat: status = “Voltooid” (van Maya), notitie toegevoegd (van Maya), toegewezen technicus = Leo’s keuze (van Leo).
- Het werk opent opnieuw in Maya’s app met een duidelijke banner: “Gesynchroniseerd met updates. Toewijzing gewijzigd terwijl je offline was.” Een kleine “Bekijk” actie toont wat er veranderde.
Voeg één faalmoment toe: Maya’s login-token verliep terwijl ze offline was. De eerste sync-poging faalt met “Aanmelden vereist.” De app bewaart haar bewerkingen, markeert ze als “Gepauzeerd” en toont één duidelijke prompt. Nadat ze inlogt, hervat sync automatisch zonder iets opnieuw te hoeven typen.
Als er een validatieprobleem is (bijv. “Voltooid” vereist een foto), moet de app niet gokken. Hij markeert het item als “Moet worden aangepast”, vertelt precies wat te doen en laat haar dan opnieuw indienen.
Platformen zoals AppMaster kunnen hierbij helpen omdat je de queue, conflicregels en pending-staat UX visueel kunt ontwerpen en toch echte native Kotlin- en SwiftUI-apps kunt uitbrengen.
Korte checklist en volgende stappen
Behandel offline-sync als een feature die end-to-end getest kan worden, niet als een stapel toevallige fixes. Het doel is simpel: gebruikers hoeven nooit te twijfelen of hun werk is opgeslagen en de app maakt geen verrassende duplicaten.
Een korte checklist om de basis te bevestigen:
- De sync-queue is op het apparaat opgeslagen en elke wijziging heeft een stabiele lokale ID plus een server-ID wanneer beschikbaar.
- Duidelijke statussen bestaan (queued, syncing, sent, failed, needs review) en worden consequent gebruikt.
- Verzoeken zijn idempotent (veilig om opnieuw te proberen) en elke operatie heeft een idempotency key.
- Records hebben versiebeheer (updatedAt, revisienummer of ETag) zodat conflicten gedetecteerd kunnen worden.
- Conflictregels zijn in eenvoudige taal geschreven (wat wint, wat wordt samengevoegd, wanneer je de gebruiker vraagt).
Als dat staat, controleer dan of de ervaring net zo goed is als het datamodel. Gebruikers moeten kunnen zien wat er in behandeling is, begrijpen wat faalde en acties ondernemen zonder bang te zijn hun werk te verliezen.
Test met scenario's die op het echte leven lijken:
- Vliegtuigmodus-bewerkingen: maak aan, werk bij, verwijder, en verbind daarna.
- Wankel netwerk: verlies de verbinding halverwege sync en zorg dat retries geen duplicaten maken.
- App geforceerd sluiten: sluit tijdens het verzenden, heropen en controleer of de queue herstelt.
- Klokverschil: apparaat staat verkeerd ingesteld, controleer of conflictdetectie nog werkt.
- Dubbel tikken: gebruiker tikt twee keer op Opslaan, controleer dat het één serverwijziging wordt.
Prototype de volledige flow voordat je de UI verfijnt. Bouw één scherm, één recordtype en één conflictgeval (twee bewerkingen op hetzelfde veld). Voeg een eenvoudige sync-statuszone toe, een Retry-knop voor fouten en één duidelijk conflictscherm. Als dat werkt, herhaal je het voor meer schermen.
Als je zonder te coderen bouwt, kan AppMaster (appmaster.io) native Kotlin- en SwiftUI-apps samen met de backend genereren, zodat jij je op de queue, versiecontroles en gebruikersgerichte statussen kunt concentreren in plaats van alles handmatig te koppelen.


