Mobilni razvoj
FlutterOffline-FirstSinkronizacijaMobilna arhitekturaLokalna pohranan8n

Flutter offline-first aplikacije: lokalna pohrana, strategije sinkronizacije i rješavanje konflikata

AO
Adrijan Omićević
·15 min čitanja

# Što ćete naučiti#

Offline-first aplikacije nastavljaju raditi i kada mreža padne, korisnik uključi airplane mode ili je backend pod opterećenjem. Ta pouzdanost izravno se prevodi u zadržavanje korisnika i prihod: Googleovo istraživanje o mobilnim performansama pokazuje da se, kako se vrijeme učitavanja stranice povećava s 1 na 3 sekunde, vjerojatnost napuštanja povećava za 32 posto, a nepouzdano ponašanje mreže stvara sličnu krivulju frustracije i kod aplikacija.

Ovaj vodič objašnjava ključne principe i daje referentni pristup za Flutter offline-first sinkronizaciju: izbor lokalne pohrane, strategije sinkronizacije u pozadini, obrasce rješavanja konflikata i kako testirati cijeli sustav u uvjetima nestabilne mreže.

# Offline-first principi koji zaista znače nešto#

Offline-first nije “keširaj malo podataka”. To je odluka o proizvodu i arhitekturi: aplikacija mora ispravno funkcionirati bez mreže, a zatim se kasnije uskladiti.

Princip 1: Lokalno je izvor istine#

Čitanja i pisanja trebaju prvo ići u lokalnu pohranu. UI se treba renderirati iz lokalne baze, a ne iz Future rezultata requesta.

To smanjuje latenciju gotovo na nulu za ponovljene prikaze i izbjegava trzanje UI-ja uzrokovano mrežnim roundtripovima. Ako vam je cilj gladak UI, pogledajte i optimizacija performansi u Flutteru za stabilnih 60fps jer offline-first aplikacije često rade više lokalnog posla na glavnoj dretvi ako niste oprezni.

Princip 2: Mrežu tretirajte kao “eventualno dostupnu”#

Sinkronizacija mora moći nastaviti nakon prekida (resumable), mora biti idempotentna i sigurna za ponavljanje. Logika “pošalji jednom” ne funkcionira u stvarnim mobilnim uvjetima: ograničenja u pozadini, promjene IP-a, captive portali i gašenje aplikacije usred procesa.

Praktičan cilj je: svaka sync operacija može se izvršiti više puta i svejedno konvergirati prema ispravnom stanju na serveru.

Princip 3: Neuspjehe učinite vidljivima i rješivima#

Korisnici će tolerirati offline način rada ako je aplikacija transparentna. Dodajte jasna stanja poput:

  • Broj promjena na čekanju
  • Vrijeme zadnje sinkronizacije
  • Gumb za ponovni pokušaj za “zaglavljene” stavke
  • Banner za konflikt kada je potrebna ručna rezolucija

Princip 4: Dajte prednost inkrementalnoj sinkronizaciji umjesto potpunog osvježavanja#

Potpuno osvježavanje brzo postaje skupo na mobilnim podacima i bateriji. Inkrementalna sinkronizacija je i temelj pouzdanog rješavanja konflikata jer možete razumno upravljati deltama.

Česta početna baza je “sinkroniziraj od lastSyncAt” uz provjere verzije po zapisu.

# Opcije lokalne pohrane u Flutteru#

Vaša lokalna pohrana nije samo cache; to je vaša operativna baza podataka. Birajte prema obrascima upita, relacijama i načinu na koji planirate modelirati sync metapodatke.

Usporedba: popularni izbori lokalne pohrane#

OpcijaNajbolje zaPrednostiNedostaciTipična offline-first primjena
Drift (SQLite)Relacijski podaci, složeni upitiZreo SQL, joinovi, migracije, snažni alatiViše boilerplatea, potreban dizajn shemeZadaci, inventar, CRM-like aplikacije
IsarBrza pohrana objekata i upitiVisoke performanse, jednostavan objektni modelManje prirodno za složene relacije, promjene sheme traže planiranjeContent aplikacije, katalozi, local-first feedovi
HiveJednostavan key-value i mali skupovi podatakaJednostavno postavljanje, laganoNije idealno za složene upite, manje ograničenjaPostavke, mali cachevi, feature flagovi
SharedPreferencesSitna konfiguracijaUgrađeno, trivialnoNije baza, nema upitaOnboarding flagovi, odabrani račun
SembastDocument storeFleksibilan JSON, bez native ovisnostiPerformanse variraju, manje naprednih mogućnosti upitaPrototipi, umjereni setovi podataka

Za “offline-first sinkronizaciju” s netrivijalnom domenom, Drift ili Isar su najčešće najbolja polazišta. Drift je često sigurniji izbor kada trebate pouzdana ograničenja i transakcijska ažuriranja kroz više tablica.

💡 Savjet: Ako očekujete spajanja i konflikte, modelirajte podatke relacijski ili barem spremite sync metapodatke uz svaki entitet. Trebat će vam kasnije za debugiranje i rezoluciju.

Minimalni podaci koje trebate spremati za sync#

Većina timova premalo modelira sync metapodatke i to kasnije “plati” u rubnim slučajevima. Za svaki zapis spremite barem:

PoljeTipSvrha
idstringStabilan identifikator (po mogućnosti UUID v4)
updatedAtdatetimeVrijeme zadnje lokalne izmjene za UX i spajanja
serverUpdatedAtdatetime nullableZadnje poznato vrijeme izmjene na serveru
versionint nullableToken optimističke konkurentnosti sa servera
syncStatusenumClean, pending, syncing, failed, conflicted
deletedboolTombstone za soft delete i sinkronizaciju

Također spremite i trajni outbox za promjene koje moraju doći do servera.

# Referentna arhitektura za Flutter offline-first sinkronizaciju#

Pouzdana arhitektura odvaja odgovornosti: UI čita lokalno stanje, domena odlučuje što sinkronizirati, data layer sprema podatke, a sync engine koordinira.

Ako strukturirate novi Flutter codebase, uskladite ovo s feature-first pristupom kao u članku arhitektura Flutter aplikacije: Clean Architecture s feature-first.

Komponente i odgovornosti#

KomponentaOdgovornostNapomene
Lokalna DB (Drift ili Isar)Spremanje entiteta, outboxa, sync metapodatakaTransakcijska ažuriranja su bitna
RepositoryIzlaganje streamova i write metodaUvijek prvo pišite lokalno
OutboxRed pending mutacijaTrajan, uređen, idempotentan
Sync EngineUpload outboxa, zatim pull promjena sa serveraRadi u foregroundu i backgroundu
Conflict ResolverOdlučuje strategiju spajanjaAutomatski gdje je sigurno, ručno gdje nije
API ClientMrežni pozivi s retryjemMora podržavati idempotency keys

Tok podataka: write putanja#

  1. 1
    Korisnik uredi zapis.
  2. 2
    Repository upiše promjene u lokalnu DB unutar transakcije.
  3. 3
    Repository doda outbox stavku koja predstavlja mutaciju.
  4. 4
    UI se odmah ažurira iz lokalnog streama.
  5. 5
    Sync engine upload-a outbox kada je dopušteno.

Ključno: UI nikad ne čeka da mreža potvrdi izmjenu.

Tok podataka: read putanja#

  • UI sluša streamove iz lokalne DB.
  • Sync engine periodički povlači promjene sa servera i primjenjuje ih lokalno.
  • Primjena promjena sa servera mora biti sigurna čak i ako postoje lokalne izmjene na čekanju.

# Implementacija outbox patterna#

Outbox je tablica ili kolekcija “stvari koje moramo javiti serveru”. Ovo je najpouzdaniji obrazac za mobilni sync jer preživljava restarte aplikacije i nestabilne mreže.

Outbox shema#

StupacPrimjerZašto je važno
iduuidJedinstveni id outbox stavke
entityTypetaskPomaže routingu
entityIdtask_123Ciljani entitet
operationcreate / update / deleteTip mutacije
payloadJSON stringDelta ili cijeli objekt
createdAttimestampRedoslijed i debugiranje
attemptCount0..nOdluke o backoffu
statuspending/sending/failedOporavljivost
idempotencyKeyuuidSprječava duple upise na serveru

Drift primjer: outbox tablica i enqueue#

Dart
// Keep code blocks short and copy-pasteable.
// This snippet illustrates the core idea, not the full database file.
 
class OutboxItems extends Table {
  TextColumn get id => text()();
  TextColumn get entityType => text()();
  TextColumn get entityId => text()();
  TextColumn get operation => text()(); // create|update|delete
  TextColumn get payload => text()();   // JSON
  TextColumn get idempotencyKey => text()();
  IntColumn get attemptCount => integer().withDefault(const Constant(0))();
  TextColumn get status => text().withDefault(const Constant('pending'))();
  DateTimeColumn get createdAt => dateTime()();
 
  @override
  Set<Column> get primaryKey => {id};
}
 
Future<void> enqueueOutboxItem(OutboxItemsCompanion item) async {
  await db.into(db.outboxItems).insert(item);
}

Strategija uploada: redoslijed i sažimanje (coalescing)#

Ako korisnik offline uredi isti zapis pet puta, obično ne želite uploadati pet updateova. Dvije praktične strategije:

StrategijaKako radiKada koristiti
Sažimanje po entitetuZadrži samo zadnji update po entitetuČeste izmjene poput bilješki, formi
Čuvanje pune povijestiUploadaj sve mutacije redomWorkflowovi koji se moraju auditirati

Sažimanje može dramatično smanjiti bandwidth. U stvarnim aplikacijama može smanjiti outbound sync pozive za 50 do 90 posto kod “draft-style” uređivanja.

⚠️ Upozorenje: Sažimanje deleteova zahtijeva posebnu obradu. Ako se zapis izbriše nakon updateova, osigurajte da je finalno stanje delete i da raniji updateovi ne “ožive” zapis tijekom retryja.

# Pull sinkronizacija: inkrementalno, paginirano i sigurno#

Sync samo u smjeru uploada nije dovoljan. Trebate pull sync da biste dobili promjene s drugih uređaja i iz server-side procesa.

Preporučeni oblik pull API-ja#

Preferirajte ove endpointove na serveru:

EndpointVraćaKljučna polja
GET /changes?since=tokenPopis promijenjenih zapisa plus novi tokenentityType, entityId, operation, data, version, serverUpdatedAt
GET /snapshot?cursor=xPaginirani puni datasetZa prvu instalaciju ili prisilni resync

Korištenje change tokena je obično bolje od sirovog timestamp-a jer izbjegavate probleme clock skewa i možete izgraditi determinističku paginaciju.

Primjena server promjena lokalno#

Primijenite promjene u transakciji i ažurirajte sync metapodatke. Kada stigne server update za zapis koji ima lokalne pending izmjene, morate odlučiti hoćete li:

  • odgoditi primjenu server updatea dok se lokalne izmjene ne uploadaju
  • primijeniti server update u shadow kopiju
  • mergeati na razini polja

Točan izbor ovisi o vašoj strategiji konflikata, opisanoj u nastavku.

# Obrasci rješavanja konflikata (s vodičem kada koristiti)#

Konflikti su neizbježni ako korisnici mogu uređivati na više uređaja ili ako server-side automatizacije mijenjaju iste podatke. Cilj su predvidljivi ishodi i minimalna bol za korisnika.

Obrazac 1: Last-Write-Wins (LWW)#

Pravilo: pobjeđuje zapis s najnovijim serverUpdatedAt.

PrednostiNedostaciNajbolje za
Jednostavno, brzoMoguć tihi gubitak podatakaNekritični podaci poput “last viewed”, presence, preference koje se mogu resetirati

LWW je prihvatljiv za polja gdje gubitak updatea nije štetan. Za business-kritične entitete koristite jači pristup.

Obrazac 2: Optimistic Concurrency Control (OCC) s verzioniranjem#

Pravilo: server sprema version kao integer. Klijent šalje expectedVersion pri updateu. Ako se ne poklapa, server vraća konflikt.

PrednostiNedostaciNajbolje za
Sprječava tiho prepisivanjeZahtijeva UX za rješavanje konflikataZadaci, narudžbe, profili, fakture

Ovo je radni konj offline-first sinkronizacije. Prisiljava vas da konflikte rješavate eksplicitno.

Tipičan server odgovor uključuje i server zapis i pokušanu klijentovu promjenu, što omogućuje merge UI.

Obrazac 3: Spajanje na razini polja (sigurni merge)#

Pravilo: mergeajte samo polja koja su sigurna, npr. aditivna ili nepreklapajuća.

Primjeri sigurnih mergeova:

Tip poljaMerge pristupPrimjer
Niz tagovaUnijaDodavanje tagova bez brisanja drugih
BrojačiZbroj deltiOffline increment “likeova”
Tekst sa sekcijamaMerge po blokovimaBilješke s uređivanjem po paragrafima, ako je podržano

Merge na razini polja drastično smanjuje konflikte kada su izmjene neovisne. Zahtijeva jasna pravila i dosljednu serijalizaciju.

Obrazac 4: Ručna rezolucija (korisnik odlučuje)#

Pravilo: prikažite obje verzije i pustite korisnika da odabere ili uredi spojenu verziju.

Koristite ručnu rezoluciju kada aplikacija ne može sigurno mergeati, na primjer:

  • dva korisnika uređuju isto polje cijene
  • dvije različite adrese dostave
  • konfliktne tranzicije statusa

🎯 Ključna poruka: Ako ne možete objasniti pravilo mergea u jednoj rečenici, ne biste ga trebali raditi automatski.

Praktičan conflict UX koji korisnici razumiju#

Upotrebljiv UX obrazac:

  1. 1
    Označite zapis lokalno kao conflicted.
  2. 2
    Prikažite ekran “Resolve” s:
    • Server verzijom (najnovijom)
    • Vašom lokalnom verzijom
    • Istaknutim poljima koja se razlikuju
  3. 3
    Ponudite akcije:
    • Zadrži moje
    • Zadrži server
    • Uredi spojeni rezultat

Rezoluciju držite lokaliziranom na entitet. Ne blokirajte cijelu aplikaciju.

# Sinkronizacija u pozadini u Flutteru: što radi u produkciji#

Background sync je mjesto gdje mnoge offline-first aplikacije padaju jer mobilni OS-ovi strogo ograničavaju izvođenje u pozadini.

Foreground, background i oportunistička sinkronizacija#

NačinOkidačPouzdanostPrimjena
Foreground syncAplikacija otvorena, korisnička akcija, periodični timerVisokaTrenutni upload nakon izmjena
Background scheduledPoslovi koje upravlja OSSrednja na Androidu, niža na iOS-u“Catch-up” kada korisnik nije aktivan
Push-triggeredSilent push ili tap na notifikacijuSrednjaPotaknuti sync pri važnim događajima

Dizajnirajte sync tako da foreground sync pokrije većinu slučajeva. Background sync je best-effort sigurnosna mreža.

Backoff i retry politika#

Retry politika treba biti predvidljiva i štedljiva prema bateriji:

Tip greškePrimjerAkcija
Mreža nedostupnaairplane modePauzirati dok se konekcija ne promijeni
Timeoutslaba mrežaEksponencijalni backoff, max interval 15–30 minuta
401/403istekao tokenOsvježi token, zatim retry jednom
409 konfliktmismatch verzijePrebaci u conflicted stanje, prestani s auto-retryjem

Idempotency keys za sigurne retryje#

Za svaki create ili update šaljite idempotencyKey generiran po outbox stavci. Ako request timeouta nakon što ga je server primijenio, ponovni pokušaj ne smije stvarati duplikate.

Jednostavan HTTP header je dovoljan:

Bash
curl -X POST https://api.example.com/tasks \
  -H "Idempotency-Key: 9d1b0d7b-7c3b-4c24-9e2a-1d9f5b7f5b8a" \
  -H "Content-Type: application/json" \
  -d '{"title":"Buy milk"}'

Server mora spremiti ključ i za ponavljanja unutar retention prozora vratiti isti rezultat, često 24 sata.

ℹ️ Napomena: Na iOS-u pretpostavite da background taskovi mogu biti ubijeni u bilo kojem trenutku. Pobrinite se da je svaki sync korak mali, da ima checkpoint i da se može nastaviti bez korupcije lokalnog stanja.

# Konkretna referentna sync petlja (prvo upload pa pull)#

Pouzdan default je:

  1. 1
    Dohvatite sync lock da se po računu izvršava samo jedna sinkronizacija.
  2. 2
    Uploadajte pending outbox stavke redom, uz coalescing.
  3. 3
    Povucite server promjene od zadnjeg tokena.
  4. 4
    Primijenite promjene u transakciji.
  5. 5
    Otpustite lock i ažurirajte lastSyncAt ili changeToken.

Pseudokod (Flutter-friendly)#

Dart
Future<void> runSync() async {
  if (!await syncLock.tryAcquire()) return;
 
  try {
    await uploadOutbox();      // idempotent, retries, conflict detection
    await pullChanges();       // incremental, paginated
  } finally {
    syncLock.release();
  }
}

Držite sync petlju kratkom i sigurnom za prekid. Izbjegavajte dugotrajne monolitne sync operacije.

# Testiranje offline-first sinkronizacije na nestabilnim mrežama#

Ako testirate samo na savršenom Wi‑Fi-ju, isporučit ćete sync bugove. Vaš plan testiranja treba uključiti varijabilnost mreže, killanje aplikacije i konkurentnost.

Što testirati (checklista)#

ScenarijŠto provjeravateOčekivani rezultat
Offline createKreiranje zapisa offline, restart aplikacijeZapis ostaje, outbox stavka ostaje
Offline updateUpdate istog zapisa 5 puta offlineUI prikazuje zadnje, outbox je sažet
Offline deleteBrisanje zapisa offline, zatim ponovno kreiranjeTombstone ponašanje je ispravno
Timeout tijekom uploadaKill aplikacije usred requestaRetry ne duplicira podatke na serveru
KonfliktEdit na dva uređajaPojavi se conflict stanje, rezolucija radi
Djelomičan pullPull vraća paginirane promjeneToken se ispravno ažurira, nema missing zapisa
Istek authaToken istekne usred synkaOsvježi pa nastavi ili se čisto pauzira

Simuliranje mrežnih uvjeta#

Koristite kombinaciju:

  • Prekidača mreže na uređaju i airplane modea
  • OS alata za network conditioning
  • Throttlinga preko proxyja

Na Android Emulatoru možete simulirati latenciju i packet loss kroz extended controls. Na iOS Simulatoru koristite Network Link Conditioner na macOS-u za emulaciju profila poput 3G ili mreža s velikim gubicima.

Deterministički sync testovi s lažnim serverom#

Fake server koji može vraćati skriptirane odgovore rano će uhvatiti race conditione.

Mogućnost fake serveraZašto je važno
Odgođeni odgovoriTestiranje timeouta i retryja
Replay duplih odgovoraValidacija idempotency obrade
Ubrizgavanje konflikataForsiranje 409 odgovora deterministički
Promjene izvan redoslijedaProvjera robusnosti token i version logike

Observability: logovi koji su vam stvarno potrebni#

Minimalno logirajte sljedeće s correlation id-jem po sync runu:

Log poljePrimjer
syncRunIdUUID po runu
accountIdtrenutni korisnik
outboxItemIdtrenutna mutacija
requestIdserver trace id ako postoji
resultsuccess, retry, conflict, auth-failed

Bez ovih logova conflict bugovi postaju ticketi tipa “ne možemo reproducirati”.

# Trošak i opseg: planirajte offline-first rano#

Offline-first nije mala “checkbox” značajka. Utječe na backend API-je, klijentsku pohranu, QA i UX.

Ako procjenjujete MVP, offline-first eksplicitno uključite u opseg i budžet. Tipičan MVP bez offline-firsta može brže van, ali naknadno “ugraditi” sync često košta znatno više jer morate redizajnirati tokove podataka i dodati migracije. Za smjernice oko budžetiranja pogledajte trošak MVP-a za mobilnu aplikaciju.

# Ključne poruke#

  • Tretirajte lokalnu pohranu kao izvor istine i dizajnirajte UI da se renderira iz lokalnih streamova, a ne iz mrežnih Future-a.
  • Koristite trajni outbox s idempotency keyjevima kako bi upload bio siguran za retry i preživio restarte.
  • Preferirajte inkrementalni pull sync s change tokenima i verzioniranjem kako biste smanjili bandwidth i izbjegli clock skew.
  • Implementirajte eksplicitno rješavanje konflikata: OCC s verzijama za kritične podatke, sigurne mergeove na razini polja gdje je moguće i ručnu rezoluciju za dvosmislene izmjene.
  • Testirajte na nestabilnim mrežama s timeoutima, killanjem aplikacije i determinističkim ubrizgavanjem konflikata te uvedite sync observability logove od prvog dana.

# Zaključak#

Kvalitetna Flutter offline-first sinkronizacija je kombinacija local-first UX-a, trajnog outboxa, inkrementalnog synka i rješavanja konflikata koje nikad tiho ne gubi korisničke podatke. Ako trebate pomoć oko dizajna arhitekture, backend endpointova ili produkcijskog sync enginea u Flutteru, Samioda može implementirati cijeli offline-first stack i testni harness end-to-end. Krenite pregledom postojećeg data modela i sync zahtjeva, a zatim se javite za arhitektonsku radionicu i plan implementacije.

FAQ

Share
A
Adrijan OmićevićSamioda Team
All articles →

Trebate pomoć s projektom?

Gradimo prilagođena rješenja koristeći tehnologije iz ovog članka. Senior tim, fiksne cijene.