# Š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#
| Opcija | Najbolje za | Prednosti | Nedostaci | Tipična offline-first primjena |
|---|---|---|---|---|
| Drift (SQLite) | Relacijski podaci, složeni upiti | Zreo SQL, joinovi, migracije, snažni alati | Više boilerplatea, potreban dizajn sheme | Zadaci, inventar, CRM-like aplikacije |
| Isar | Brza pohrana objekata i upiti | Visoke performanse, jednostavan objektni model | Manje prirodno za složene relacije, promjene sheme traže planiranje | Content aplikacije, katalozi, local-first feedovi |
| Hive | Jednostavan key-value i mali skupovi podataka | Jednostavno postavljanje, lagano | Nije idealno za složene upite, manje ograničenja | Postavke, mali cachevi, feature flagovi |
| SharedPreferences | Sitna konfiguracija | Ugrađeno, trivialno | Nije baza, nema upita | Onboarding flagovi, odabrani račun |
| Sembast | Document store | Fleksibilan JSON, bez native ovisnosti | Performanse variraju, manje naprednih mogućnosti upita | Prototipi, 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:
| Polje | Tip | Svrha |
|---|---|---|
id | string | Stabilan identifikator (po mogućnosti UUID v4) |
updatedAt | datetime | Vrijeme zadnje lokalne izmjene za UX i spajanja |
serverUpdatedAt | datetime nullable | Zadnje poznato vrijeme izmjene na serveru |
version | int nullable | Token optimističke konkurentnosti sa servera |
syncStatus | enum | Clean, pending, syncing, failed, conflicted |
deleted | bool | Tombstone 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#
| Komponenta | Odgovornost | Napomene |
|---|---|---|
| Lokalna DB (Drift ili Isar) | Spremanje entiteta, outboxa, sync metapodataka | Transakcijska ažuriranja su bitna |
| Repository | Izlaganje streamova i write metoda | Uvijek prvo pišite lokalno |
| Outbox | Red pending mutacija | Trajan, uređen, idempotentan |
| Sync Engine | Upload outboxa, zatim pull promjena sa servera | Radi u foregroundu i backgroundu |
| Conflict Resolver | Odlučuje strategiju spajanja | Automatski gdje je sigurno, ručno gdje nije |
| API Client | Mrežni pozivi s retryjem | Mora podržavati idempotency keys |
Tok podataka: write putanja#
- 1Korisnik uredi zapis.
- 2Repository upiše promjene u lokalnu DB unutar transakcije.
- 3Repository doda outbox stavku koja predstavlja mutaciju.
- 4UI se odmah ažurira iz lokalnog streama.
- 5Sync 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#
| Stupac | Primjer | Zašto je važno |
|---|---|---|
id | uuid | Jedinstveni id outbox stavke |
entityType | task | Pomaže routingu |
entityId | task_123 | Ciljani entitet |
operation | create / update / delete | Tip mutacije |
payload | JSON string | Delta ili cijeli objekt |
createdAt | timestamp | Redoslijed i debugiranje |
attemptCount | 0..n | Odluke o backoffu |
status | pending/sending/failed | Oporavljivost |
idempotencyKey | uuid | Sprječava duple upise na serveru |
Drift primjer: outbox tablica i enqueue#
// 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:
| Strategija | Kako radi | Kada koristiti |
|---|---|---|
| Sažimanje po entitetu | Zadrži samo zadnji update po entitetu | Česte izmjene poput bilješki, formi |
| Čuvanje pune povijesti | Uploadaj sve mutacije redom | Workflowovi 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:
| Endpoint | Vraća | Ključna polja |
|---|---|---|
GET /changes?since=token | Popis promijenjenih zapisa plus novi token | entityType, entityId, operation, data, version, serverUpdatedAt |
GET /snapshot?cursor=x | Paginirani puni dataset | Za 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.
| Prednosti | Nedostaci | Najbolje za |
|---|---|---|
| Jednostavno, brzo | Moguć tihi gubitak podataka | Nekritič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.
| Prednosti | Nedostaci | Najbolje za |
|---|---|---|
| Sprječava tiho prepisivanje | Zahtijeva UX za rješavanje konflikata | Zadaci, 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 polja | Merge pristup | Primjer |
|---|---|---|
| Niz tagova | Unija | Dodavanje tagova bez brisanja drugih |
| Brojači | Zbroj delti | Offline increment “likeova” |
| Tekst sa sekcijama | Merge po blokovima | Bilješ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:
- 1Označite zapis lokalno kao
conflicted. - 2Prikažite ekran “Resolve” s:
- Server verzijom (najnovijom)
- Vašom lokalnom verzijom
- Istaknutim poljima koja se razlikuju
- 3Ponudite 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čin | Okidač | Pouzdanost | Primjena |
|---|---|---|---|
| Foreground sync | Aplikacija otvorena, korisnička akcija, periodični timer | Visoka | Trenutni upload nakon izmjena |
| Background scheduled | Poslovi koje upravlja OS | Srednja na Androidu, niža na iOS-u | “Catch-up” kada korisnik nije aktivan |
| Push-triggered | Silent push ili tap na notifikaciju | Srednja | Potaknuti 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ške | Primjer | Akcija |
|---|---|---|
| Mreža nedostupna | airplane mode | Pauzirati dok se konekcija ne promijeni |
| Timeout | slaba mreža | Eksponencijalni backoff, max interval 15–30 minuta |
| 401/403 | istekao token | Osvježi token, zatim retry jednom |
| 409 konflikt | mismatch verzije | Prebaci 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:
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:
- 1Dohvatite sync lock da se po računu izvršava samo jedna sinkronizacija.
- 2Uploadajte pending outbox stavke redom, uz coalescing.
- 3Povucite server promjene od zadnjeg tokena.
- 4Primijenite promjene u transakciji.
- 5Otpustite lock i ažurirajte
lastSyncAtilichangeToken.
Pseudokod (Flutter-friendly)#
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 provjeravate | Očekivani rezultat |
|---|---|---|
| Offline create | Kreiranje zapisa offline, restart aplikacije | Zapis ostaje, outbox stavka ostaje |
| Offline update | Update istog zapisa 5 puta offline | UI prikazuje zadnje, outbox je sažet |
| Offline delete | Brisanje zapisa offline, zatim ponovno kreiranje | Tombstone ponašanje je ispravno |
| Timeout tijekom uploada | Kill aplikacije usred requesta | Retry ne duplicira podatke na serveru |
| Konflikt | Edit na dva uređaja | Pojavi se conflict stanje, rezolucija radi |
| Djelomičan pull | Pull vraća paginirane promjene | Token se ispravno ažurira, nema missing zapisa |
| Istek autha | Token istekne usred synka | Osvjež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 servera | Zašto je važno |
|---|---|
| Odgođeni odgovori | Testiranje timeouta i retryja |
| Replay duplih odgovora | Validacija idempotency obrade |
| Ubrizgavanje konflikata | Forsiranje 409 odgovora deterministički |
| Promjene izvan redoslijeda | Provjera robusnosti token i version logike |
Observability: logovi koji su vam stvarno potrebni#
Minimalno logirajte sljedeće s correlation id-jem po sync runu:
| Log polje | Primjer |
|---|---|
syncRunId | UUID po runu |
accountId | trenutni korisnik |
outboxItemId | trenutna mutacija |
requestId | server trace id ako postoji |
result | success, 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
Više iz kategorije Mobilni razvoj
Sve →Optimizacija performansi u Flutteru: kako održavamo aplikacije na 60 fps (profiliranje + popravci)
Ponovljiv workflow za optimizaciju performansi u Flutteru uz DevTools: dijagnosticiranje trzanja (jank), zatim ciljane dorade — smanjenje rebuildova, poboljšanja renderiranja, image pipeline i isolates — uz budžete i kontrolne liste.
Flutter arhitektura aplikacije koja se skalira: Clean Architecture vs Feature-First (s realnim strukturama mapa)
Praktičan vodič za arhitekturu Flutter aplikacije u 2026.: usporedite Clean Architecture i Feature-First, pogledajte stvarne strukture mapa, granice ovisnosti i strategije testiranja te odaberite pravi pristup za svoj tim i ritam izdanja.
Flutter + Firebase: Potpuni vodič za 2026. (Auth, Firestore, Functions, Deploy)
Korak-po-korak Flutter Firebase vodič za 2026.: postavite Firebase, dodajte autentifikaciju, izgradite Firestore CRUD, napišite Cloud Functions i deployajte aplikaciju spremnu za produkciju.
Trebate pomoć s projektom?
Gradimo prilagođena rješenja koristeći tehnologije iz ovog članka. Senior tim, fiksne cijene.
Povezani članci
Optimizacija performansi u Flutteru: kako održavamo aplikacije na 60 fps (profiliranje + popravci)
Ponovljiv workflow za optimizaciju performansi u Flutteru uz DevTools: dijagnosticiranje trzanja (jank), zatim ciljane dorade — smanjenje rebuildova, poboljšanja renderiranja, image pipeline i isolates — uz budžete i kontrolne liste.
Flutter arhitektura aplikacije koja se skalira: Clean Architecture vs Feature-First (s realnim strukturama mapa)
Praktičan vodič za arhitekturu Flutter aplikacije u 2026.: usporedite Clean Architecture i Feature-First, pogledajte stvarne strukture mapa, granice ovisnosti i strategije testiranja te odaberite pravi pristup za svoj tim i ritam izdanja.
Flutter + Firebase: Potpuni vodič za 2026. (Auth, Firestore, Functions, Deploy)
Korak-po-korak Flutter Firebase vodič za 2026.: postavite Firebase, dodajte autentifikaciju, izgradite Firestore CRUD, napišite Cloud Functions i deployajte aplikaciju spremnu za produkciju.