20 sie 2025·7 min czytania

Rozwiązywanie konfliktów formularzy offline-first dla Kotlin + SQLite

Poznaj rozwiązywanie konfliktów formularzy offline-first: jasne reguły scalania, prosty przepływ synchronizacji Kotlin + SQLite i praktyczne wzorce UX dla konfliktów edycji.

Rozwiązywanie konfliktów formularzy offline-first dla Kotlin + SQLite

Co się właściwie dzieje, gdy dwie osoby edytują offline

Formularze offline-first pozwalają przeglądać i edytować dane nawet gdy sieć jest wolna lub niedostępna. Zamiast czekać na serwer, aplikacja zapisuje zmiany najpierw w lokalnej bazie SQLite, a potem synchronizuje je później.

To daje wrażenie natychmiastowości, ale tworzy prostą rzeczywistość: dwa urządzenia mogą zmienić ten sam rekord, nie wiedząc o sobie nawzajem.

Typowy konflikt wygląda tak: technik w terenie otwiera zlecenie na tablecie w piwnicy bez zasięgu. Oznacza status jako "Done" i dodaje notatkę. W tym samym czasie przełożony na innym telefonie aktualizuje to samo zlecenie, przekazuje je komuś innemu i zmienia termin. Obie osoby naciskają Zapisz. Obie zmiany zapisują się lokalnie. Nikt niczego nie popsuł.

Gdy synchronizacja w końcu nastąpi, serwer musi zdecydować, jaki jest „prawdziwy” rekord. Jeśli nie obsłużysz konfliktów explicite, zwykle trafiasz na jedną z tych sytuacji:

  • Ostatni zapis wygrywa: późniejsza synchronizacja nadpisuje wcześniejsze zmiany i ktoś traci dane.
  • Twardy błąd: synchronizacja odrzuca jedną aktualizację, a aplikacja pokazuje mało pomocny błąd.
  • Duplikaty: system tworzy drugą kopię, żeby uniknąć nadpisania, a raportowanie robi się chaotyczne.
  • Ciche scalanie: system łączy zmiany, ale miesza pola w sposób, którego użytkownicy się nie spodziewają.

Konflikty nie są błędem. To przewidywalny skutek pozwalania ludziom pracować bez aktywnego połączenia — co jest celem podejścia offline-first.

Cel jest dwojaki: chronić dane i utrzymać aplikację łatwą w użyciu. Zwykle oznacza to jasne reguły scalania (często na poziomie pola) i doświadczenie użytkownika, które przerywa pracę tylko wtedy, gdy to naprawdę istotne. Jeśli dwie edycje dotykają różnych pól, często możesz scalić je cicho. Jeśli dwie osoby zmieniły to samo pole na różne sposoby, aplikacja powinna to uwidocznić i pomóc komuś wybrać właściwy wynik.

Wybierz strategię konfliktów dopasowaną do twoich danych

Konflikty nie są najpierw problemem technicznym. To decyzja produktowa o tym, co oznacza „poprawne”, gdy dwie osoby zmieniły ten sam rekord przed synchronizacją.

Trzy strategie obejmują większość aplikacji offline:

  • Ostatni zapis wygrywa (LWW): akceptuj najnowszą edycję i nadpisz starszą.
  • Ręczna weryfikacja: zatrzymaj i poproś człowieka o wybór, co zachować.
  • Scalanie per-pole: łącz zmiany pole po polu i pytaj tylko wtedy, gdy obie strony zmieniły to samo pole.

LWW może być w porządku, gdy szybkość jest ważniejsza niż pełna precyzja, a koszt błędu jest niski. Pomyśl o notatkach wewnętrznych, niekrytycznych tagach czy statusach szkiców, które można edytować ponownie później.

Ręczna weryfikacja jest bezpieczniejszym wyborem dla pól o dużym znaczeniu, gdzie aplikacja nie powinna zgadywać: teksty prawne, potwierdzenia zgodności, kwoty płatności i faktur, dane bankowe, instrukcje dotyczące leków i wszystko, co może generować odpowiedzialność.

Scalanie per-pole jest zwykle najlepszym domyślnym podejściem dla formularzy, gdzie różne role aktualizują różne części. Agent wsparcia edytuje adres, a dział sprzedaży aktualizuje datę odnowienia. Scalanie per-pole zachowuje obie zmiany bez zawracania głowy użytkownikom. Ale jeśli obaj edytowali datę odnowienia, to pole powinno wywołać decyzję.

Zanim wdrożysz cokolwiek, zapisz, co oznacza „poprawne” dla twojego biznesu. Szybka lista kontrolna:

  • Które pola zawsze muszą odzwierciedlać najnowszą wartość z realnego świata (np. bieżący status)?
  • Które pola są historyczne i nigdy nie powinny być nadpisywane (np. czas wysłania)?
  • Kto może zmieniać każde pole (rola, właściciel, akceptacje)?
  • Co jest źródłem prawdy, gdy wartości się nie zgadzają (urządzenie, serwer, zatwierdzenie menedżera)?
  • Co się stanie, jeśli wybierzesz źle (drobna niedogodność vs konsekwencje finansowe lub prawne)?

Gdy te reguły są jasne, kod synchronizacji ma jedno zadanie: je egzekwować.

Zdefiniuj reguły scalania per-pole, nie per-ekran

Gdy konflikt występuje, rzadko wpływa równomiernie na cały formularz. Jeden użytkownik może zmienić numer telefonu, podczas gdy inny dopisuje notatkę. Jeśli potraktujesz cały rekord jako wszystko-lub-nic, zmusisz ludzi do powtarzania dobrej pracy.

Scalanie per-pole jest przewidywalniejsze, ponieważ każde pole ma znane zachowanie. UX pozostaje spokojny i szybki.

Prosty sposób, by zacząć, to podzielić pola na „zwykle bezpieczne” i „zwykle niebezpieczne” do automatycznego scalania.

Zwykle bezpieczne do automatycznego scalania: notatki i komentarze wewnętrzne, tagi, załączniki (często unia), oraz znaczniki czasu jak last contacted (często zachowaj najnowszy).

Zwykle niebezpieczne do automatycznego scalania: status/stan, assignee/właściciel, sumy/ceny, flagi zatwierdzeń i stany magazynowe.

Następnie wybierz regułę priorytetu dla każdego pola. Powszechne wybory to server wins, client wins, role wins (np. menedżer ma przewagę nad agentem) albo deterministyczne rozstrzygnięcie, jak najnowsza wersja serwera.

Kluczowe pytanie to: co się dzieje, gdy obie strony zmieniły to samo pole. Dla każdego pola wybierz jedno zachowanie:

  • Auto-scalanie z jasną regułą (np. tagi to unia)
  • Zachowaj obie wartości (np. doklej notatki z autorem i czasem)
  • Oznacz do przeglądu (np. status i assignee wymagają wyboru)

Przykład: dwóch przedstawicieli wsparcia edytuje ten sam ticket offline. Rep A zmienia status z Open na Pending. Rep B edytuje notes i dodaje tag refund. Przy synchronizacji możesz bezpiecznie scalić notes i tags, ale nie powinieneś cicho scalać status. Wyświetl tylko status do decyzji, reszta już jest połączona.

Aby uniknąć sporów później, udokumentuj każdą regułę w jednym zdaniu na pole:

  • "notes: zachowaj obie, dopisz najnowsze na końcu, dołącz autora i czas."
  • "tags: unia, usunięcie tylko jeśli usunięte po obu stronach."
  • "status: jeśli zmienione po obu stronach, wymagaj wyboru użytkownika."
  • "assignee: menedżer wygrywa, w przeciwnym razie serwer wygrywa."

To jednozdaniowe wyjaśnienie staje się źródłem prawdy dla kodu Kotlin, zapytań SQLite i UI konfliktów.

Podstawy modelu danych: wersje i pola audytu w SQLite

Jeśli chcesz, by konflikty były przewidywalne, dodaj mały zestaw kolumn metadanych do każdej tabeli synchronizowanej z serwerem. Bez nich nie powiesz, czy patrzysz na świeżą edycję, stary egzemplarz, czy dwie edycje, które trzeba scalić.

Praktyczne minimum dla każdego rekordu synchronizowanego przez serwer:

  • id (stabilny klucz główny): nigdy go nie ponownie używaj
  • version (integer): inkrementuje przy każdym udanym zapisie na serwerze
  • updated_at (timestamp): kiedy rekord został ostatnio zmieniony
  • updated_by (tekst lub id użytkownika): kto wykonał ostatnią zmianę

Na urządzeniu dodaj pola tylko lokalne do śledzenia zmian, które nie zostały potwierdzone przez serwer:

  • dirty (0/1): istnieją lokalne zmiany
  • pending_sync (0/1): oczekuje na wysłanie, ale niepotwierdzone
  • last_synced_at (timestamp): ostatni raz, gdy wiersz odpowiadał serwerowi
  • sync_error (tekst, opcjonalnie): ostatni powód błędu do pokazania w UI

Optymistyczna współbieżność to najprostsza reguła zapobiegająca cichym nadpisaniom: każda aktualizacja zawiera wersję, którą myślisz, że edytujesz (expected_version). Jeśli rekord serwera nadal ma tę wersję, aktualizacja jest przyjmowana, a serwer zwraca nową wersję. Jeśli nie — jest konflikt.

Przykład: Użytkownik A i B pobrali version = 7. A synchronizuje pierwszy; serwer podbija do 8. Gdy B próbuje zsynchronizować z expected_version = 7, serwer odrzuca z konfliktem, więc aplikacja B scala zamiast nadpisywać.

Dla dobrego ekranu konfliktu przechowaj wspólny punkt wyjścia: od czego użytkownik zaczął edycję. Dwa powszechne podejścia:

  • Przechowaj snapshot ostatniego zsynchronizowanego rekordu (jedna kolumna JSON lub równoległa tabela).
  • Przechowaj log zmian (wiersz na edycję lub pole na edycję).

Snapshoty są prostsze i często wystarczające dla formularzy. Logi zmian są cięższe, ale potrafią wyjaśnić dokładnie, co się zmieniło pole po polu.

W każdym przypadku UI powinno móc pokazać trzy wartości na pole: edycję użytkownika, bieżącą wartość serwera i wspólny punkt wyjścia.

Snapshoty rekordów vs logi zmian: wybierz podejście

Utrzymuj recenzje konfliktów małe
Twórz skupione ekrany przeglądu, które pokazują tylko pola naprawdę będące w konflikcie.
Wypróbuj AppMaster

Podczas synchronizacji formularzy offline możesz wysyłać cały rekord (snapshot) lub listę operacji (change log). Oba działają z Kotlin i SQLite, ale zachowują się inaczej, gdy dwie osoby edytują ten sam rekord.

Opcja A: Snapshot całego rekordu

Przy snapshotach każde zapisanie zapisuje pełny stan (wszystkie pola). Przy synchronizacji wysyłasz rekord plus numer wersji. Jeśli serwer widzi, że wersja jest stara — masz konflikt.

To proste do zbudowania i szybkie do odczytu, ale często tworzy większe konflikty niż to konieczne. Jeśli Użytkownik A zmienia numer telefonu, a B zmienia adres, podejście ze snapshotem może potraktować to jako jeden duży spór, choć zmiany się nie nakładają.

Opcja B: Log zmian (operacje)

W logach zmian przechowujesz to, co się zmieniło, a nie cały rekord. Każda lokalna edycja staje się operacją, którą możesz odtworzyć na najnowszym stanie serwera.

Operacje, które łatwiej się scalają:

  • Ustaw pole na wartość (ustaw email na nowy adres)
  • Doklej notatkę (dodaje nowy element notatki)
  • Dodaj tag (dodaje tag do zbioru)
  • Usuń tag (usuwa jeden tag ze zbioru)
  • Oznacz checkbox jako wykonany (ustaw isDone na true z timestampem)

Log operacji może zmniejszyć liczbę konfliktów, bo wiele akcji się nie pokrywa. Doklejanie notatek rzadko koliduje z kimś innym doklejającym inną notatkę. Dodania/usuwa­nia tagów mogą się scalić jak działania na zbiorach. Dla pól jednowartościowych nadal potrzebujesz reguł per-pole, gdy konkurują dwie różne edycje.

Wadą jest złożoność: stabilne ID operacji, porządek (lokalna sekwencja i czas serwera) oraz reguły dla operacji, które nie przemienne.

Sprzątanie: kompaktowanie po udanej synchronizacji

Logi operacji rosną, więc zaplanuj, jak je zmniejszyć.

Częste podejście to kompaktowanie per-rekord: gdy wszystkie operacje do znanej wersji serwera są potwierdzone, złóż je w nowy snapshot, a następnie usuń starsze operacje. Zachowaj krótki ogon tylko jeśli potrzebujesz cofnąć, audytu lub łatwiejszego debugowania.

Krok po kroku: flow synchronizacji dla Kotlin + SQLite

Zbuduj aplikację offline-first
Zbuduj przepływ formularzy offline-first z jasnymi stanami synchronizacji i obsługą konfliktów w jednym projekcie.
Wypróbuj AppMaster

Dobra strategia synchronizacji to w większości kwestia rygorystycznego podejścia do tego, co wysyłasz i co akceptujesz z powrotem. Cel jest prosty: nigdy przypadkowo nie nadpisz nowszych danych i ujawniaj konflikty, gdy nie można ich bezpiecznie scalić.

Praktyczny flow:

  1. Zapisz każdą edycję najpierw w SQLite. Zapisz zmiany w lokalnej transakcji i oznacz rekord jako pending_sync = 1. Zapisz local_updated_at i ostatnią znaną server_version.

  2. Wyślij patch, nie cały rekord. Gdy wróci łączność, wyślij id rekordu plus tylko pola, które się zmieniły, wraz z expected_version.

  3. Pozwól serwerowi odrzucać niepasujące wersje. Jeśli aktualna wersja serwera nie zgadza się z expected_version, zwraca payload konfliktu (rekord serwera, proponowane zmiany i które pola różnią się). Jeśli wersje pasują, zastosuje patch, zwiększy wersję i zwróci zaktualizowany rekord.

  4. Najpierw zastosuj auto-scalanie, potem poproś użytkownika. Uruchom reguły scalania per-pole. Traktuj bezpieczne pola jak notatki inaczej niż wrażliwe pola jak status, cena czy assignee.

  5. Zatwierdź końcowy wynik i usuń flagi oczekujące. Niezależnie czy było to auto-scalanie czy ręczne rozwiązanie, zapisz ostateczny rekord z powrotem do SQLite, zaktualizuj server_version, ustaw pending_sync = 0 i zapisz wystarczające dane audytu, aby później wyjaśnić, co się stało.

Przykład: dwóch przedstawicieli sprzedaży edytuje to samo zamówienie offline. A zmienia datę dostawy. B zmienia numer telefonu klienta. Przy patchach serwer może przyjąć obie zmiany czysto. Jeśli obaj zmienili datę dostawy, ujawnisz jedną jasną decyzję zamiast zmuszać do ponownego wpisywania wszystkiego.

Utrzymaj obietnicę UI: „Zapisano” powinno znaczyć zapis lokalny. „Zsynchronizowano” to oddzielny, jawny stan.

Wzorce UX do rozwiązywania konfliktów w formularzach

Konflikty powinny być wyjątkiem, nie normalnym przebiegiem. Zacznij od automatycznego scalania tego, co jest bezpieczne, a potem pytaj użytkownika tylko wtedy, gdy decyzja jest naprawdę potrzebna.

Spraw, by konflikty były rzadkie poprzez bezpieczne domyślne ustawienia

Jeśli dwie osoby edytują różne pola, scal je bez pokazywania modala. Zachowaj obie zmiany i pokaż mały komunikat „Zaktualizowano po synchronizacji”.

Zarezerwuj monit dla prawdziwych zderzeń: to samo pole zmienione na oba sposoby albo zmiana zależna od innego pola (np. status + powód statusu).

Gdy musisz pytać, pozwól szybko zakończyć

Ekran konfliktu powinien odpowiedzieć na dwa pytania: co się zmieniło i co zostanie zapisane. Porównaj wartości obok siebie: „Twoja edycja”, „Ich edycja” i „Zapisany wynik”. Jeśli konfliktuje tylko kilka pól, nie pokazuj całego formularza. Przejdź prosto do tych pól i trzymaj resztę tylko do odczytu.

Ogranicz akcje do tego, czego ludzie naprawdę potrzebują:

  • Zachowaj moją
  • Zachowaj ich
  • Edytuj wynik
  • Przejrzyj pole-po-polu (tylko gdy potrzebne)

Częściowe scalania to miejsce, gdzie UX robi się trudny. Wyróżnij tylko pola w konflikcie i wyraźnie oznacz źródło ("Twoje" i "Ich"). Wstępnie wybierz najbezpieczniejszą opcję, żeby użytkownik mógł potwierdzić i iść dalej.

Ustal oczekiwania, żeby użytkownicy nie czuli się uwięzieni. Powiedz, co się stanie, jeśli opuszczą ekran: np. "Zachowamy twoją wersję lokalnie i spróbujemy zsynchronizować ponownie później" albo "Ten rekord będzie w stanie Potrzebuje przeglądu, dopóki nie dokonasz wyboru." Pokaż ten stan w liście, żeby konflikty się nie zgubiły.

Jeśli budujesz ten przepływ w AppMaster, ta sama zasada UX obowiązuje: najpierw auto-scalanie bezpiecznych pól, potem skupiony krok przeglądu tylko gdy pola faktycznie kolidują.

Trudne przypadki: usunięcia, duplikaty i „zaginione” rekordy

Wybierz opcję wdrożenia
Wdróż na AppMaster Cloud, AWS, Azure, Google Cloud lub self-host z wyeksportowanego kodu.
Wdróż aplikację

Większość problemów z synchronizacją, które wydają się losowe, pochodzi z trzech sytuacji: ktoś usuwa, podczas gdy ktoś inny edytuje; dwa urządzenia tworzą „to samo” offline; albo rekord znika, a potem pojawia się z powrotem. To wymagają reguł explicite, bo LWW często zaskakuje użytkowników.

Usunięcie kontra edycja: kto wygrywa?

Zdecyduj, czy usunięcie jest silniejsze niż edycja. W wielu aplikacjach biznesowych usunięcie wygrywa, bo użytkownicy spodziewają się, że usunięty rekord zostanie usunięty wszędzie.

Zestaw praktycznych reguł:

  • Jeśli rekord jest usunięty na dowolnym urządzeniu, traktuj go jako usunięty wszędzie, nawet jeśli są późniejsze edycje.
  • Jeśli usunięcia mają być odwracalne, zamień „usuniecie” na stan archiwalny zamiast twardego usunięcia.
  • Jeśli przyjdzie edycja dla usuniętego rekordu, zachowaj tę edycję w historii dla audytu, ale nie przywracaj rekordu.

Kolizje tworzenia offline i duplikaty szkiców

Formularze offline-first często tworzą tymczasowe ID (np. UUID) zanim serwer przypisze finalne ID. Duplikaty pojawiają się, gdy użytkownicy tworzą dwie wersje tego samego realnego obiektu offline (ten sam paragon, ten sam ticket, ten sam element).

Jeśli masz stabilny naturalny klucz (numer paragonu, kod kreskowy, email + data), użyj go do wykrywania kolizji. Jeśli nie, zaakceptuj, że duplikaty będą się pojawiać i zapewnij prostą opcję scalania później.

Wskazówka implementacyjna: przechowuj zarówno local_id, jak i server_id w SQLite. Gdy serwer odpowie, zapisz mapowanie i trzymaj je co najmniej do momentu, gdy będziesz pewien, że żadne oczekujące zmiany nie odnoszą się już do lokalnego ID.

Zapobieganie „wskrzeszeniu” po synchronizacji

Wskrzeszenie zdarza się, gdy Urządzenie A usuwa rekord, ale Urządzenie B jest offline i później wysyła starą kopię jako upsert, przez co rekord zostaje odtworzony.

Rozwiązanie to tombstone. Zamiast usuwać wiersz natychmiast, oznacz go jako usunięty z deleted_at (często także deleted_by i delete_version). Podczas synchronizacji traktuj tombstone’y jako prawdziwe zmiany, które mogą nadpisać starsze nieusunięte stany.

Zdecyduj, jak długo trzymać tombstone’y. Jeśli użytkownicy mogą być offline przez tygodnie, przechowuj je dłużej niż ten okres. Usuwaj dopiero, gdy będziesz pewien, że aktywne urządzenia zsynchronizowały się po usunięciu.

Jeśli wspierasz cofnięcie, traktuj cofnięcie jako kolejną zmianę: usuń deleted_at i podbij wersję.

Częste błędy prowadzące do utraty danych lub frustracji użytkowników

Dostarcz UI przyjazne konfliktom
Twórz interfejsy webowe i natywne mobilne, które rozróżniają „Zapisano lokalnie” i „Zsynchronizowano”.
Zbuduj aplikację

Wiele awarii synchronizacji pochodzi z małych założeń, które cicho nadpisują dobre dane.

Błąd 1: poleganie na czasie urządzenia do porządkowania edycji

Telefony mogą mieć niepoprawne zegary, zmieniają się strefy czasowe, a użytkownicy mogą ręcznie ustawić czas. Jeśli porządkujesz zmiany po znacznikach czasu z urządzeń, w końcu zastosujesz aktualizacje w złej kolejności.

Preferuj wersje wydawane przez serwer (monotoniczny serverVersion) i traktuj znaczniki czasu po stronie klienta jako wyłącznie do wyświetlania. Jeśli musisz używać czasu, dodaj zabezpieczenia i rozliczaj to na serwerze.

Błąd 2: przypadkowe LWW na wrażliwych polach

LWW wydaje się proste, dopóki nie dotrzesz do pól, które nie powinny być „wygrywane” przez to, kto zsynchronizował się później. Status, sumy, zatwierdzenia i przypisania zwykle wymagają explicite reguł.

Lista kontrolna dla pól wysokiego ryzyka:

  • Traktuj przejścia statusów jako maszynę stanów, a nie dowolny tekst.
  • Przeliczaj sumy z elementów linii. Nie łącz sum jako surowe liczby.
  • Dla liczników scalaj przez stosowanie delt, nie wybieranie zwycięzcy.
  • Dla własności lub przypisania wymagaj explicite potwierdzenia przy konflikcie.

Błąd 3: nadpisywanie nowszych wartości serwera przestarzałymi danymi z cache

To dzieje się, gdy klient edytuje stary snapshot, a potem wypycha cały rekord. Serwer akceptuje i nowsze zmiany po stronie serwera znikają.

Napraw kształt tego, co wysyłasz: wysyłaj tylko zmienione pola (lub log zmian) oraz bazową wersję, którą edytowałeś. Jeśli bazowa wersja jest przestarzała, serwer odrzuca lub wymusza scalanie.

Błąd 4: brak historii „kto co zmienił”

Gdy występuje konflikt, użytkownicy chcą jedną odpowiedź: co ja zmieniłem, a co zmieniła druga osoba? Bez tożsamości edytora i per-pole śladu zmian, ekran konfliktu staje się zgadywanką.

Przechowuj updatedBy, czas aktualizacji po stronie serwera, jeśli go masz, oraz przynajmniej lekką per-pole historię audytu.

Błąd 5: UI konfliktu zmuszające do porównania całego rekordu

Zmuszanie ludzi do porównania całych rekordów jest męczące. Większość konfliktów dotyczy jednego do trzech pól. Pokaż tylko pola w konflikcie, wstępnie wybierz najbezpieczniejszą opcję i pozwól użytkownikowi zaakceptować resztę automatycznie.

Jeśli budujesz formularze w narzędziu no-code jak AppMaster, dąż do tego samego rezultatu: rozwiązuj konflikty na poziomie pól, aby użytkownicy podejmowali jedną jasną decyzję zamiast przewijać cały formularz.

Szybka lista kontrolna i kolejne kroki

Jeśli chcesz, żeby edycje offline były bezpieczne, traktuj konflikty jako normalny stan, nie błąd. Najlepsze rezultaty wynikają z jasnych reguł, powtarzalnych testów i UX, który po ludzku wyjaśnia, co się stało.

Zanim dodasz więcej funkcji, upewnij się, że podstawy są dopracowane:

  • Dla każdego typu rekordu przypisz regułę scalania per-pole (LWW, keep max/min, append, union lub zawsze pytaj).
  • Przechowuj kontrolowaną przez serwer wersję plus updated_at, które walidujesz podczas synchronizacji.
  • Przeprowadź test dwóch urządzeń, gdzie oba edytują ten sam rekord offline, a potem synchronizują w obu kolejnościach (A potem B, B potem A). Wynik powinien być przewidywalny.
  • Przetestuj twarde konflikty: usunięcie vs edycja oraz edycja vs edycja na różnych polach.
  • Uczyń stany widocznymi: Synced, Pending upload i Needs review.

Prototypuj pełny przepływ end-to-end z jednym realnym formularzem, nie tylko ekranem demo. Użyj realistycznego scenariusza: technik terenowy aktualizuje notatkę zlecenia na telefonie, a dyspozytor edytuje tytuł zlecenia na tablecie. Jeśli dotykają różnych pól, auto-scalaj i pokaż mały hint „Zaktualizowano z innego urządzenia”. Jeśli dotykają tego samego pola, skieruj na prosty ekran przeglądu z dwoma wyborami i wyraźnym podglądem.

Gdy będziesz gotowy zbudować pełną aplikację mobilną i API backendowe razem, AppMaster (appmaster.io) może pomóc. Możesz modelować dane, definiować logikę biznesową i tworzyć interfejsy webowe oraz natywne mobilne w jednym miejscu, a potem wdrożyć lub wyeksportować kod źródłowy, gdy reguły synchronizacji będą dopracowane.

FAQ

What is an offline sync conflict, in plain terms?

Konflikt ma miejsce, gdy dwa urządzenia zmieniają ten sam rekord powiązany z serwerem podczas pracy offline (lub zanim któreś z nich zsynchronizuje zmiany), a serwer później widzi, że obie aktualizacje bazują na starszej wersji. System wtedy musi zdecydować, jaka powinna być końcowa wartość dla każdego pola, które się różnią.

Which conflict strategy should I choose: last write wins, manual review, or field-level merge?

Zacznij od scalania per-pole jako domyłu dla większości formularzy biznesowych, ponieważ różne role często edytują różne pola i można zachować obie zmiany bez niepotrzebnego przerywania pracy. Użyj ręcznej weryfikacji tylko dla pól, które mogą wyrządzić realne szkody, jeśli system zgadnie (pieniądze, zatwierdzenia, zgodność). Ostatni zapis wygrywa (LWW) sprawdza się jedynie dla niskiego ryzyka pól, gdzie utrata starszej zmiany jest akceptowalna.

When should the app ask the user to resolve a conflict?

Jeśli dwie edycje dotyczą różnych pól, zwykle możesz scalić je automatycznie i nie przerywać pracy użytkownika. Jeśli dwie edycje zmieniają to samo pole na różne wartości, to pole powinno wywołać decyzję, bo automatyczny wybór może kogoś zaskoczyć. Ogranicz zakres decyzji — pokaż tylko pola w konflikcie, nie cały formularz.

How do record versions prevent silent overwrites?

Traktuj version jako monotoniczny licznik rekordu na serwerze i wymagaj od klienta wysłania expected_version z każdą aktualizacją. Jeśli bieżąca wersja serwera nie pasuje, odrzuć z odpowiedzią o konflikcie zamiast nadpisywać. Ta prosta reguła zapobiega „cichej utracie danych”, nawet gdy dwa urządzenia synchronizują się w różnej kolejności.

What metadata should every synced SQLite table include?

Praktyczne minimum to stabilne id, kontrolowana przez serwer version, oraz updated_at/updated_by po stronie serwera, żeby wyjaśnić, co się zmieniło. Na urządzeniu śledź, czy wiersz jest zmieniony i czeka na wysłanie (np. pending_sync) oraz ostatnią zsynchronizowaną wersję serwera. Bez tych pól nie da się wiarygodnie wykryć konfliktów ani pokazać pomocnego ekranu rozwiązywania.

Should I sync the whole record or only changed fields?

Wysyłaj tylko pola, które się zmieniły (patch) plus bazową expected_version. Przesyłanie całego rekordu powoduje, że małe, niepokrywające się zmiany zamieniają się w niepotrzebne konflikty i zwiększa ryzyko nadpisania nowszych wartości serwera przestarzałymi danymi z cache. Patche ułatwiają też określenie, które pola wymagają reguł scalania.

Is it better to store snapshots or a change log for offline edits?

Snapshot jest prostszy: przechowujesz pełny rekord i porównujesz go później z serwerem. Change log jest bardziej elastyczny: przechowujesz operacje takie jak „ustaw pole” lub „dodaj notatkę” i odtwarzasz je na najnowszym stanie serwera, co często lepiej scala notatki, tagi i inne przyrostowe aktualizacje. Wybierz snapshot dla szybszej implementacji; wybierz change log, jeśli scalania są częste i potrzebujesz jasnego śladu „kto zmienił co”.

How should I handle delete vs edit conflicts?

Zdecyduj zawczasu, czy usunięcie jest silniejsze od edycji, bo użytkownicy oczekują spójnego zachowania. Dla wielu aplikacji biznesowych bezpiecznym domyślnym podejściem jest traktowanie usunięć jako tombstone’ów (oznaczonych deleted_at i wersją), aby starsze, offline’owe upserty nie przywracały przypadkowo usuniętego rekordu. Jeśli potrzebujesz możliwości cofnięcia, użyj stanu „archiwalnego” zamiast twardego usunięcia.

What are the most common mistakes that cause offline sync data loss?

Nie polegaj na czasie urządzenia do porządkowania zapisów — zegary się rozjeżdżają i strefy czasowe zmieniają. Jeśli porządkujesz zmiany po znacznikach czasu z klientów, w końcu zastosujesz aktualizacje w złej kolejności. Preferuj wersje wydane przez serwer i traktuj znaczniki czasu klienta jako dane wyświetlane. Jeśli musisz używać czasu, dodaj zabezpieczenia i scalanie po stronie serwera.

How can I implement a conflict-friendly UX when building with AppMaster?

Utrzymaj obietnicę: „Zapisano” oznacza zapisano lokalnie i pokaż oddzielny stan „Zsynchronizowano”, aby użytkownicy rozumieli, co się dzieje. Jeśli budujesz to w AppMaster, dąż do tej samej struktury: zdefiniuj reguły scalania per-pole jako część logiki produktu, automatycznie scalaj bezpieczne pola i kieruj tylko prawdziwe kolizje pól do krótkiego kroku przeglądu. Testuj dwoma urządzeniami edytującymi ten sam rekord offline i synchronizującymi w obu kolejnościach, aby potwierdzić przewidywalne wyniki.

Łatwy do uruchomienia
Stworzyć coś niesamowitego

Eksperymentuj z AppMaster z darmowym planem.
Kiedy będziesz gotowy, możesz wybrać odpowiednią subskrypcję.

Rozpocznij