30 mar 2025·8 min czytania

Wzorce synchronizacji w tle z Kotlin WorkManager dla aplikacji terenowych

Wzorce synchronizacji w tle z Kotlin WorkManager dla aplikacji terenowych: wybierz właściwy typ pracy, ustaw ograniczenia, używaj wykładniczego backoffu i pokazuj widoczny dla użytkownika postęp.

Wzorce synchronizacji w tle z Kotlin WorkManager dla aplikacji terenowych

Co znaczy niezawodna synchronizacja w tle dla aplikacji terenowych i operacyjnych

W aplikacjach terenowych synchronizacja to nie „miły dodatek”. To sposób, w jaki praca opuszcza urządzenie i staje się rzeczywista dla zespołu. Gdy synchronizacja zawodzi, użytkownicy zauważają to szybko: wykonane zadanie wciąż wygląda jak „oczekujące”, zdjęcia znikają, albo ten sam raport wysyła się dwukrotnie i tworzy duplikaty.

Te aplikacje są trudniejsze niż typowe aplikacje konsumenckie, bo telefony działają w najgorszych warunkach. Sieć przełącza się między LTE, słabym Wi‑Fi i brakiem zasięgu. Oszczędzanie baterii blokuje pracę w tle. Aplikacja zostaje zamknięta, OS się aktualizuje, urządzenia się rebootują w trakcie trasy. Niezawodne ustawienie WorkManager musi przetrwać to wszystko bez dramatu.

Niezawodność zwykle oznacza cztery rzeczy:

  • Ostateczna spójność: dane mogą przyjść później, ale przychodzą bez ręcznego doglądania.
  • Odzyskiwalność: jeśli aplikacja umrze w trakcie uploadu, następny przebieg kontynuuje bezpiecznie.
  • Obserwowalność: użytkownicy i support widzą, co się dzieje i co utknęło.
  • Bez destrukcji: retry nie tworzą duplikatów ani nie uszkadzają stanu.

„Uruchom teraz” pasuje do małych, wyzwalanych przez użytkownika akcji, które powinny skończyć się szybko (np. wysłanie pojedynczej aktualizacji statusu przed zamknięciem zadania). „Poczekaj” pasuje do cięższych prac jak uploady zdjęć, aktualizacje w batchu lub cokolwiek, co może wyczerpać baterię lub zawieść przy złej sieci.

Przykład: inspektor wysyła formularz z 12 zdjęciami w piwnicy bez zasięgu. Niezawodna synchronizacja zapisuje wszystko lokalnie, oznacza jako w kolejce i wysyła później, gdy urządzenie ma prawdziwe połączenie, bez konieczności ponownego wykonywania pracy przez inspektora.

Wybierz właściwe elementy WorkManagera

Zacznij od wyboru najmniejszej, najjaśniejszej jednostki pracy. Ta decyzja wpływa na niezawodność bardziej niż jakakolwiek sprytna logika retry później.

One‑time vs periodic work

Używaj OneTimeWorkRequest do pracy, która ma się wydarzyć dlatego, że coś się zmieniło: zapisano nowy formularz, skończyło się kompresowanie zdjęcia, albo użytkownik nacisnął Synchronizuj. Enqueue’uj ją od razu (z ograniczeniami) i pozwól WorkManagerowi wykonać ją, gdy urządzenie będzie gotowe.

Używaj PeriodicWorkRequest do stałej konserwacji, jak „sprawdź aktualizacje” lub nocne czyszczenie. Prace okresowe nie są dokładne. Mają minimalny interwał i mogą dryfować w zależności od baterii i zasad systemu, więc nie powinny być jedyną drogą dla ważnych uploadów.

Praktyczny wzorzec to prace jednorazowe dla „trzeba zsynchronizować wkrótce” i praca okresowa jako siatka bezpieczeństwa.

Wybór Worker, CoroutineWorker lub RxWorker

Jeśli piszesz w Kotlinie i korzystasz z suspend funkcji, preferuj CoroutineWorker. Utrzymuje kod krótki i sprawia, że anulowanie działa zgodnie z oczekiwaniami.

Worker nadaje się do prostego, blokującego kodu, ale trzeba uważać, by nie blokować zbyt długo.

RxWorker ma sens tylko wtedy, gdy aplikacja już mocno używa RxJava. W przeciwnym razie to dodatkowa złożoność.

Łańcuchowanie kroków czy jeden worker z fazami?

Łączenie (chaining) jest świetne, gdy kroki mogą się udawać lub zawodzić niezależnie i chcesz osobnych retry oraz czytelniejszych logów. Jeden worker z fazami może być lepszy, gdy kroki dzielą dane i muszą być traktowane jak jedna transakcja.

Prosta zasada:

  • Łącz, gdy kroki mają różne ograniczenia (np. upload tylko na Wi‑Fi, potem lekkie wywołanie API).
  • Użyj jednego workera, gdy potrzebujesz „wszystko‑albo‑nic” dla jednej synchronizacji.

WorkManager gwarantuje, że praca jest utrwalona, przetrwa zamknięcie procesu i restarty oraz respektuje ograniczenia. Nie gwarantuje natomiast dokładnego czasu wykonania, natychmiastowego uruchomienia ani działania po wymuszeniu zatrzymania aplikacji przez użytkownika. Jeśli budujesz aplikację terenową na Androidzie (w tym taką wygenerowaną jako Kotlin z AppMaster), projektuj synchronizację tak, żeby opóźnienia były bezpieczne i oczekiwane.

Spraw, by synchronizacja była bezpieczna: idempotentna, inkrementalna i wznawialna

Aplikacja terenowa będzie uruchamiać pracę wielokrotnie. Telefony tracą sygnał, OS zabija procesy, a użytkownicy naciskają synchronizuj dwa razy, bo nic się nie wydawało dziać. Jeśli twoja synchronizacja w tle nie nadaje się do powtórzeń, pojawią się duplikaty, brakujące aktualizacje lub nieskończone retry.

Zacznij od tego, by każde wywołanie serwera było bezpieczne do uruchomienia dwa razy. Najprostsze podejście to idempotency key na element (np. UUID przechowywane z lokalnym rekordem), które serwer traktuje jako „to samo żądanie, ten sam wynik”. Jeśli nie możesz zmienić serwera, użyj stabilnego klucza naturalnego i endpointu upsert albo dołącz numer wersji, żeby serwer mógł odrzucać przestarzałe aktualizacje.

Śledź lokalny stan jawnie, żeby worker mógł wznowić po awarii bez zgadywania. Prosty automat stanów często wystarcza:

  • queued
  • uploading
  • uploaded
  • needs-review
  • failed-temporary

Utrzymuj synchronizację inkrementalną. Zamiast „synchronizuj wszystko”, przechowuj kursor taki jak lastSuccessfulTimestamp lub token wydany przez serwer. Czytaj małą stronę zmian, zastosuj je, a potem przesuwaj kursor dopiero po pełnym zatwierdzeniu batchu lokalnie. Małe partie (np. 20–100 elementów) zmniejszają time‑outy, pokazują postęp i ograniczają ilość pracy, którą powtarzasz po przerwaniu.

Spraw, by uploady były wznawialne. Dla zdjęć lub dużych payloadów zachowaj URI pliku i metadane uploadu, i oznacz jako wysłane dopiero po potwierdzeniu serwera. Jeśli worker restartuje, kontynuuje od ostatniego znanego stanu, zamiast zaczynać od początku.

Przykład: technik wypełnia 12 formularzy i dołącza 8 zdjęć pod ziemią. Gdy urządzenie ponownie ma połączenie, worker wysyła batchami; każdy formularz ma idempotency key, a kursor synchronizacji przesuwa się po każdym udanym batchu. Jeśli aplikacja zostanie zabita w połowie, ponowne uruchomienie workera dokończy pozostałe elementy w kolejce bez duplikowania czegokolwiek.

Ograniczenia dopasowane do rzeczywistych warunków urządzeń

Ograniczenia to barierki, które chronią przed rozładowaniem baterii, spaleniem planu danych czy nieudanym uploadem w najgorszym momencie. Chcesz ograniczeń, które odzwierciedlają, jak urządzenia zachowują się w terenie, a nie na twoim biurku.

Zacznij od małego zestawu, który chroni użytkowników, ale pozwala pracy uruchamiać się większość dni. Praktyczna baza to: wymagaj połączenia sieciowego, nie uruchamiaj gdy bateria jest niska i unikaj uruchamiania przy krytycznie małej przestrzeni dyskowej. Dodaj „pod ładowaniem” tylko, jeśli praca jest ciężka i nieterminowa, bo wiele urządzeń terenowych rzadko jest podłączonych w czasie zmiany.

Nadmierne ograniczanie to częsta przyczyna raportów „synchronizacja nigdy nie działa”. Jeśli wymagasz niezmierzonego Wi‑Fi, ładowania i baterii nie niskiej, zasadniczo prosisz o idealny moment, który może nigdy nie nadejść. Jeśli biznes potrzebuje danych dziś, lepiej uruchamiać mniejsze prace częściej niż czekać na idealne warunki.

Kolejnym realnym problemem są captive portale: telefon mówi, że jest podłączony, ale użytkownik musi kliknąć „Akceptuj” na stronie hotelu lub publicznego Wi‑Fi. WorkManager nie wykryje tego niezawodnie. Traktuj to jako normalną porażkę: spróbuj synchronizacji, szybko timeoutuj i spróbuj później. Pokazuj także prosty komunikat w aplikacji jak „Połączono z Wi‑Fi, ale brak dostępu do internetu”, gdy możesz to wykryć podczas żądania.

Używaj różnych ograniczeń dla małych i dużych uploadów, żeby aplikacja pozostała responsywna:

  • Małe payloady (pingi statusu, metadane formularzy): dowolna sieć, bateria nie niska.
  • Duże payloady (zdjęcia, wideo, mapy): najlepiej sieć nie rozliczana (unmetered) i rozważ wymaganie ładowania.

Przykład: technik zapisuje formularz z 2 zdjęciami. Wyślij pola formularza na dowolnym połączeniu, ale uploady zdjęć odłóż do Wi‑Fi albo lepszego momentu. Biuro szybko widzi zadanie, a urządzenie nie spali transferu mobilnego na przesyłanie obrazów w tle.

Retry z wykładniczym backoffem, który nie wkurza użytkowników

Testuj logikę synchronizacji bez przepisywania
Zaprojektuj kolejkę uploadów, logikę retry i UX postępu z wizualną logiką, którą można później dopracować.
Wypróbuj teraz

Retry to moment, w którym aplikacje terenowe albo wydają się spokojne, albo popsute. Wybierz politykę backoff, która pasuje do rodzaju spodziewanego błędu.

Wykładniczy backoff jest zwykle najbezpieczniejszym domyślnym wyborem dla sieci. Szybko zwiększa czas oczekiwania, żeby nie zasypywać serwera ani nie wyczerpywać baterii, gdy pokrycie jest słabe. Liniowy backoff pasuje do krótkich, przejściowych problemów (np. niestabilne VPN), ale ma tendencję do zbyt częstych retry w słabym sygnale.

Podejmuj decyzje o retry na podstawie typu błędu, nie tylko „coś padło”. Prosty zestaw reguł pomaga:

  • Timeout sieci, 5xx, DNS, brak łączności: Result.retry()
  • Wygasłe uwierzytelnienie (401): odśwież token raz, potem zakończ błędem i poproś użytkownika o ponowne logowanie
  • Błędy walidacji lub 4xx (bad request): Result.failure() z jasnym błędem dla supportu
  • Konflikt (409) dla już wysłanych elementów: traktuj jako sukces, jeśli synchronizacja jest idempotentna

Ogranicz szkody, żeby trwały błędy nie powodowały pętli. Ustaw maksymalną liczbę prób, po której zatrzymujesz retry i wyświetlasz ciche, możliwe do podjęcia działanie (nie powtarzane powiadomienia).

Możesz też zmieniać zachowanie wraz ze wzrostem prób. Na przykład po 2 niepowodzeniach wysyłaj mniejsze partie albo pomiń duże uploady aż do następnego udanego pobrania.

val request = OneTimeWorkRequestBuilder\u003cSyncWorker\u003e()
  .setBackoffCriteria(
    BackoffPolicy.EXPONENTIAL,
    30, TimeUnit.SECONDS
  )
  .build()

// in doWork()
if (runAttemptCount \u003e= 5) return Result.failure()
return Result.retry()

To utrzymuje retry w uprzejmym trybie: mniej wybudzeń, mniej przerwań dla użytkownika i szybsze przywrócenie, gdy połączenie w końcu wróci.

Postęp widoczny dla użytkownika: powiadomienia, foreground work i status

Wdróż tam, gdzie działa Twój zespół
Wdróż do AppMaster Cloud lub na własne środowisko AWS, Azure czy Google Cloud.
Wdróż aplikację

Aplikacje terenowe często synchronizują, gdy użytkownik tego nie oczekuje: w piwnicy, na wolnej sieci, z prawie rozładowaną baterią. Jeśli synchronizacja wpływa na to, na co użytkownik czeka (uploady, wysyłanie raportów, pakiety zdjęć), pokaż to i ułatw zrozumienie. Cicha praca w tle jest świetna dla małych, szybkich aktualizacji. Wszystko dłuższe powinno być uczciwe.

Kiedy wymagana jest praca w foreground

Użyj wykonania w foreground, gdy zadanie jest długotrwałe, czasowo krytyczne lub wyraźnie związane z działaniem użytkownika. Na nowoczesnym Androidzie duże uploady mogą zostać zatrzymane lub opóźnione, chyba że uruchomisz je jako foreground. W WorkManagerze oznacza to zwrócenie ForegroundInfo, aby system pokazał trwające powiadomienie.

Dobre powiadomienie odpowiada na trzy pytania: co się synchronizuje, jak daleko jesteśmy i jak to zatrzymać. Dodaj jasną akcję anulowania, żeby użytkownik mógł się wycofać, jeśli jest na płatnym transferze lub potrzebuje telefonu teraz.

Postęp, któremu można zaufać

Postęp powinien mapować się na realne jednostki, nie na mgławicowe procenty. Aktualizuj postęp za pomocą setProgress i czytaj go z WorkInfo w UI (lub ekranie statusu).

Jeśli wysyłasz 12 zdjęć i 3 formularze, zgłaszaj „5 z 15 elementów wysłanych”, pokaż, co zostało, i zachowaj ostatnią wiadomość o błędzie dla supportu.

Utrzymuj postęp znaczący:

  • Elementy wykonane i pozostałe
  • Bieżący krok ("Wysyłanie zdjęć", "Wysyłanie formularzy", "Finalizacja")
  • Ostatni udany czas synchronizacji
  • Ostatni błąd (krótki, zrozumiały dla użytkownika)
  • Widoczna opcja anulowania/zatrzymania

Jeśli twój zespół szybko buduje wewnętrzne narzędzia z AppMaster, zachowaj tę samą zasadę: użytkownicy ufają synchronizacji, gdy ją widzą i gdy odpowiada temu, co naprawdę chcą zrobić.

Unikalna praca, tagi i unikanie duplikatów zadań synchronizacji

Duplikaty prac synchronizacji są jednym z najprostszych sposobów na drenowanie baterii, spalanie danych mobilnych i tworzenie konfliktów po stronie serwera. WorkManager daje dwa proste narzędzia, by temu zapobiec: unikalne nazwy pracy i tagi.

Dobrym domyślnym rozwiązaniem jest traktować „sync” jako jedną nitkę. Zamiast enqueować nowe zadanie za każdym razem, gdy aplikacja się wybudza, enqueue’uj tę samą unikalną pracę. W ten sposób nie dostaniesz synchronizacyjnej burzy, gdy użytkownik otworzy aplikację, zmiana sieci wyzwoli akcję, a praca okresowa odpali jednocześnie.

val request = OneTimeWorkRequestBuilder\u003cSyncWorker\u003e()
  .addTag("sync")
  .build()

WorkManager.getInstance(context)
  .enqueueUniqueWork("sync", ExistingWorkPolicy.KEEP, request)

Wybór polityki to główny wybór zachowania:

  • KEEP: jeśli synchronizacja już działa (lub jest w kolejce), zignoruj nowe żądanie. Używaj tego dla większości przycisków „Sync now” i automatycznych triggerów.
  • REPLACE: anuluj bieżącą i zacznij od nowa. Używaj tego, gdy rzeczywiście zmieniły się wejścia, np. użytkownik zmienił konto lub wybrał inny projekt.

Tagi to uchwyt do kontroli i widoczności. Ze stabilnym tagiem jak sync możesz anulować, sprawdzać status lub filtrować logi bez śledzenia konkretnych ID. To szczególnie przydatne dla ręcznej akcji „synchronizuj teraz”: możesz sprawdzić, czy coś już działa i pokazać jasny komunikat, zamiast uruchamiać kolejnego workera.

Synchronizacja okresowa i na żądanie nie powinny ze sobą walczyć. Trzymaj je oddzielnie, ale skoordynowane:

  • Użyj enqueueUniquePeriodicWork("sync_periodic", KEEP, ...) dla zaplanowanej pracy.
  • Użyj enqueueUniqueWork("sync", KEEP, ...) dla synchronizacji na żądanie.
  • W workerze szybko wyjdź, jeśli nie ma nic do wysłania ani pobrania, żeby okresowy przebieg był tani.
  • Opcjonalnie: pozwól workerowi okresowemu enqueue’ować tę samą jednorazową, unikalną synchronizację, aby cała prawdziwa praca odbywała się w jednym miejscu.

Te wzorce utrzymują synchronizację przewidywalną: jedna synchronizacja na raz, łatwa do anulowania i obserwowania.

Krok po kroku: praktyczny pipeline synchronizacji w tle

Daj ops jasny widok administracyjny
Dodaj panel administracyjny dla zespołów operacyjnych, by widziały, co się zsynchronizowało, co się nie powiodło i dlaczego.
Zbuduj panel

Niezawodny pipeline synchronizacji jest łatwiejszy do zbudowania, gdy traktujesz go jak mały automat stanów: elementy pracy najpierw żyją lokalnie, a WorkManager tylko przesuwa je do przodu, gdy warunki są spełnione.

Prosty pipeline, który możesz wypuścić

  1. Zacznij od lokalnych tabel kolejki. Przechowuj najmniejsze metadane potrzebne do wznowienia: id elementu, typ (formularz, zdjęcie, notatka), status (pending, uploading, done), licznik prób, ostatni błąd i kursor lub rewizję serwera dla pobrań.

  2. Dla „Sync now” wywołanego przez użytkownika enqueue’uj OneTimeWorkRequest z ograniczeniami dopasowanymi do rzeczywistości. Typowe wybory to: połączenie sieciowe i bateria nie niska. Jeśli uploady są ciężkie, wymagaj też ładowania.

  3. Zaimplementuj CoroutineWorker z jasnymi fazami: upload, download, reconcile. Trzymaj każdą fazę inkrementalną. Wysyłaj tylko elementy oznaczone jako pending, pobieraj tylko zmiany od twojego ostatniego kursora, potem rozwiązuj konflikty prostymi regułami (np. serwer wygrywa dla pól przypisań, klient dla szkiców lokalnych).

  4. Dodaj retry z backoffem, ale bądź wybiórczy w tym, co retryujesz. Timeouty i 500 powinny retryować. 401 (wylogowany) powinno szybko się zakończyć i powiedzieć UI, co się stało.

  5. Obserwuj WorkInfo, aby napędzać UI i powiadomienia. Używaj aktualizacji postępu dla faz typu „Wysyłanie 3 z 10” i pokaż krótki komunikat o błędzie, który wskazuje kolejne działanie (retry, zaloguj się, połącz z Wi‑Fi).

val constraints = Constraints.Builder()
  .setRequiredNetworkType(NetworkType.CONNECTED)
  .setRequiresBatteryNotLow(true)
  .build()

val request = OneTimeWorkRequestBuilder\u003cSyncWorker\u003e()
  .setConstraints(constraints)
  .setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 30, TimeUnit.SECONDS)
  .build()

Kiedy trzymasz kolejkę lokalnie i fazy workera jawne, otrzymujesz przewidywalne zachowanie: praca może się wstrzymać, wznowić i wyjaśnić się użytkownikowi bez zgadywania, co się stało.

Częste błędy i pułapki (i jak ich unikać)

Niezawodna synchronizacja najczęściej zawodzi z powodu kilku małych wyborów, które wyglądają nieszkodliwie podczas testów, a potem rozpadają się na prawdziwych urządzeniach. Celem nie jest uruchamiać jak najczęściej. Celem jest uruchamiać we właściwym czasie, robić właściwą pracę i zatrzymać się czysto, gdy nie można kontynuować.

Pułapki, na które warto uważać

  • Wykonywanie dużych uploadów bez ograniczeń. Jeśli wysyłasz zdjęcia lub duże payloady na dowolnej sieci i przy dowolnym stanie baterii, użytkownicy to odczują. Dodaj ograniczenia dla typu sieci i niskiej baterii, i podziel dużą pracę na mniejsze kawałki.
  • Retry wszystkich błędów w nieskończoność. 401, wygasły token lub brak uprawnień to nie przejściowy problem. Oznacz te błędy jako trwałe, powierz jasną akcję (ponowne logowanie) i retryuj tylko prawdziwie przejściowe problemy jak timeouty.
  • Tworzenie duplikatów przez przypadek. Jeśli worker może uruchomić się dwukrotnie, serwer zobaczy podwójne tworzenia, chyba że żądania będą idempotentne. Użyj stabilnego, generowanego po stronie klienta ID dla każdego elementu i spraw, by serwer traktował powtórzenia jako aktualizacje, nie nowe rekordy.
  • Używanie pracy okresowej do potrzeb niemal w czasie rzeczywistym. Praca okresowa jest najlepsza do konserwacji, nie do „synchronizuj teraz”. Dla synchronizacji inicjowanej przez użytkownika enqueue’uj jednorazową, unikalną pracę i pozwól użytkownikowi ją wywołać, gdy potrzeba.
  • Raportowanie „100%” za wcześnie. Zakończenie uploadu nie znaczy, że dane zostały zaakceptowane i zapisane. Śledź postęp po etapach (queued, uploading, server confirmed) i pokazuj zakończenie dopiero po potwierdzeniu.

Konkretne podsumowanie: technik wysyła formularz z trzema zdjęciami w windzie o słabym sygnale. Jeśli zaczniesz natychmiast bez ograniczeń, uploady utkną, retry skoczą, a formularz może powstać dwa razy po restarcie aplikacji. Jeśli ograniczysz do użytecznej sieci, uploady wykonasz etapami i ponumerujesz formularz stabilnym ID, scenariusz kończy się jednym czystym rekordem na serwerze i prawdomównym komunikatem postępu.

Szybka lista kontrolna przed wysyłką

Zachowaj pełną kontrolę nad kodem źródłowym
Otrzymaj prawdziwy kod źródłowy, żeby zespół mógł go przeglądać, rozbudowywać i samodzielnie hostować.
Generuj kod

Przed wydaniem testuj synchronizację tak, jak rzeczywiści użytkownicy terenowi ją zepsują: niestabilny sygnał, rozładowane baterie i dużo stukania po ekranie. Co wygląda dobrze na telefonie deweloperskim, może nadal zawieść w terenie, jeśli harmonogramowanie, retry lub raportowanie statusu są niepoprawne.

Testuj na co najmniej jednym wolnym urządzeniu i jednym nowszym. Trzymaj logi, ale obserwuj też, co widzi użytkownik w UI.

  • Brak sieci, potem odzyskanie: Zacznij synchronizację bez łączności, potem włącz. Potwierdź, że praca jest w kolejce (nie kończy się natychmiast błędem) i wznawia się później bez duplikowania uploadów.
  • Restart urządzenia: Rozpocznij synchronizację, zrestartuj w połowie, potem otwórz aplikację. Zweryfikuj, że praca kontynuuje lub zostaje poprawnie przeplanowana i że aplikacja pokazuje właściwy stan (nie utknięcie na "synchronizuje").
  • Niska bateria i mało miejsca: Włącz oszczędzanie baterii, spadnij poniżej progu niskiej baterii, jeśli to możliwe, i zapełnij przestrzeń dyskową blisko pełnej. Potwierdź, że zadanie czeka, gdy powinno, a potem kontynuuje po poprawie warunków, bez spalania baterii w pętli retry.
  • Wielokrotne wyzwalania: Kilkukrotnie naciśnij przycisk "Synchronizuj" albo wyzwól synchronizację z kilku ekranów. Powinieneś skończyć z jedną logiczną synchronizacją, a nie stertą równoległych workerów konkurujących o te same rekordy.
  • Błędy serwera, które możesz wytłumaczyć: Zasimuluj 500, timeouty i błędy auth. Sprawdź, że retry się wycofują i zatrzymują po capie, a użytkownik widzi jasny komunikat jak "Nie można połączyć się z serwerem, spróbuje ponownie" zamiast ogólnego błędu.

Jeśli którykolwiek test zostawia aplikację w niejasnym stanie, traktuj to jako błąd. Użytkownicy wybaczają wolną synchronizację, ale nie wybaczają utraty danych lub braku jasności co się stało.

Przykładowy scenariusz: formularze offline i uploady zdjęć w aplikacji terenowej

Zamień modele danych w aplikacje
Zamodeluj swoje dane w minutach i zamień je w API i ekrany, które naprawdę można wystawić.
Zacznij budować

Technik przyjeżdża na miejsce o słabym zasięgu. Wypełnia formularz offline, robi 12 zdjęć i naciska Wyślij przed wyjazdem. Aplikacja zapisuje wszystko najpierw lokalnie (np. w lokalnej bazie danych): jeden rekord dla formularza i jeden rekord na zdjęcie z jasnym statusem takim jak PENDING, UPLOADING, DONE lub FAILED.

Gdy naciska Wyślij, aplikacja enqueue’uje unikalne zadanie synchronizacji, by nie tworzyć duplikatów, jeśli naciśnie ponownie. Typowe rozwiązanie to łańcuch WorkManagera, który najpierw uploaduje zdjęcia (większe, wolniejsze), a potem wysyła payload formularza po potwierdzeniu załączników.

Synchronizacja uruchamia się tylko, gdy warunki odpowiadają rzeczywistości. Na przykład czeka na połączenie sieciowe, brak niskiego poziomu baterii i wystarczającą ilość miejsca. Jeśli technik jest nadal w piwnicy bez sygnału, nic nie spala baterii w pętli w tle.

Postęp jest oczywisty i przyjazny dla użytkownika. Upload działa jako foreground work i pokazuje powiadomienie typu „Wysyłanie 3 z 12” z jasną akcją Anuluj. Jeśli anuluje, aplikacja zatrzymuje pracę i utrzymuje pozostałe elementy w PENDING, żeby mogły być ponownie wysłane później bez utraty danych.

Retry zachowuje się uprzejmie po niestabilnym hotspotcie: pierwsza porażka retryuje szybko, ale każda kolejna czeka coraz dłużej (wykładniczy backoff). Na początku wydaje się responsywne, potem wycofuje się, by nie wyczerpywać baterii i nie spamować sieci.

Dla zespołu operacyjnego wartość jest praktyczna: mniej duplikatów dzięki idempotentnym i unikalnym kolejkom, czytelne stany błędów (które zdjęcie zawiodło, dlaczego i kiedy spróbuje ponownie) oraz większe zaufanie, że „wysłane” znaczy „bezpiecznie zapisane i zsynchronizowane”.

Kolejne kroki: najpierw wypuść niezawodność, potem rozszerzaj zakres synchronizacji

Zanim dodasz więcej funkcji synchronizacji, ustal, co znaczy „zrobione”. W większości aplikacji terenowych to nie „żądanie wysłane”. To „serwer zaakceptował i potwierdził”, plus stan UI, który odpowiada rzeczywistości. Formularz oznaczony jako „Zsynchronizowany” powinien tak pozostać po restarcie aplikacji, a formularz, który nie udał się zsynchronizować, powinien jasno pokazywać, co zrobić dalej.

Ułatw zaufanie do aplikacji, dodając mały zestaw sygnałów, które ludzie mogą zobaczyć (a support może o nie zapytać). Trzymaj je proste i spójne na ekranach:

  • Ostatnia udana synchronizacja
  • Ostatni błąd synchronizacji (krótka wiadomość, nie stack trace)
  • Elementy w kolejce (np. 3 formularze, 12 zdjęć)
  • Bieżący stan synchronizacji (Idle, Syncing, Needs attention)

Traktuj obserwowalność jako część funkcji. Oszczędza to godzin w terenie, gdy ktoś jest na słabym połączeniu i nie wie, czy aplikacja działa.

Jeśli budujesz też backend i narzędzia administracyjne, generowanie ich razem pomaga utrzymać stabilny kontrakt synchronizacji. AppMaster (appmaster.io) może wygenerować produkcyjny backend, panel administracyjny web i natywne aplikacje mobilne, co pomaga trzymać modele i auth zgodne, podczas gdy ty skupiasz się na trudnych krawędziach synchronizacji.

Wreszcie, przeprowadź mały pilotaż. Wybierz jedną end‑to‑end część synchronizacji (np. „prześlij formularz inspekcji z 1–2 zdjęciami”) i wypuść ją z ograniczeniami, retry i widocznym postępem działającymi poprawnie. Gdy ten fragment stanie się nudny i przewidywalny, rozszerzaj po jednym elemencie naraz.

FAQ

Co w praktyce znaczy „niezawodna synchronizacja w tle” w aplikacji terenowej?

Niezawodna synchronizacja w tle oznacza, że praca utworzona na urządzeniu jest najpierw zapisana lokalnie i później wysłana bez konieczności powtarzania przez użytkownika. Powinna przetrwać zamknięcia aplikacji, restart urządzenia, słabe sieci i ponowienia prób, nie gubiąc danych ani nie tworząc duplikatów.

Kiedy użyć OneTimeWorkRequest a kiedy PeriodicWorkRequest do synchronizacji?

Używaj pracy jednorazowej (OneTimeWorkRequest) dla wszystkiego, co wywołane faktycznym zdarzeniem: „formularz zapisany”, „dodano zdjęcie” lub użytkownik nacisnął Synchronizuj. Używaj pracy okresowej (PeriodicWorkRequest) do konserwacji i jako siatki bezpieczeństwa, ale nie polegaj na niej jako jedynym mechanizmie ważnych uploadów, bo jej timing może się przesuwać.

Który typ Worker wybrać: Worker, CoroutineWorker czy RxWorker?

Jeśli piszesz w Kotlinie i używasz suspend funkcji, CoroutineWorker jest najprostszy i najprzewidywalniejszy, szczególnie jeśli chodzi o anulowanie. Worker nadaje się do krótkich, blokujących zadań, a RxWorker tylko gdy aplikacja już intensywnie używa RxJava.

Czy lepiej łączyć wiele workerów czy wszystko robić w jednym workerze?

Łącz workerów, gdy kroki mają różne ograniczenia lub powinny retryować niezależnie (np. duże pliki na Wi‑Fi, potem drobne wywołanie API). Użyj jednego workera z fazami, gdy kroki dzielą stan i chcesz zachować zachowanie „wszystko albo nic” dla jednego logicznego przebiegu synchronizacji.

Jak zatrzymać retry, które tworzą duplikaty na serwerze?

Spraw, by każde tworzenie/aktualizacja było bezpieczne do powtórzenia, np. przez idempotency key dla każdego elementu (UUID przechowywane z lokalnym rekordem). Jeśli nie możesz zmienić serwera, użyj stabilnego klucza naturalnego i endpointu upsert albo numeru wersji, aby serwer odrzucał przestarzałe aktualizacje.

Jak zrobić, by uploady były wznawialne, jeśli aplikacja zostanie zabita w trakcie?

Przechowuj jawne lokalne statusy jak queued, uploading, uploaded i failed, aby worker mógł wznowić pracę bez zgadywania. Oznaczaj element jako zakończony dopiero po potwierdzeniu serwera i zapisuj metadane (np. URI pliku, licznik prób), aby kontynuować po awarii lub restarcie.

Jakie ograniczenia są dobrym domyślnym wyborem dla zadań synchronizacji w aplikacji terenowej?

Zacznij od minimalnych ograniczeń, które chronią użytkowników, ale pozwalają synchronizacji działać na co dzień: wymagana sieć, brak niskiego poziomu baterii i wystarczająco miejsca na dysku. Ostrożnie podchodź do wymogu „unmetered” i „pod ładowaniem”, bo dla urządzeń terenowych mogą one zablokować synchronizację na stałe.

Jak aplikacja powinna sobie radzić z captive portalami lub „Wi‑Fi bez internetu"?

Traktuj „połączony, ale bez internetu” jako normalny błąd: szybko time‑outuj, zwróć Result.retry() i spróbuj później. Jeśli możesz wykryć to podczas żądania, pokaż prosty komunikat, żeby użytkownik zrozumiał, dlaczego urządzenie wygląda na online, a synchronizacja się nie posuwa.

Jaka strategia retry jest najbezpieczniejsza przy niestabilnych sieciach?

Dla problemów sieciowych używaj wykładniczego backoffu, żeby retry rzadły, gdy pokrycie jest słabe. Retryuj timeouty i błędy 5xx, szybko zakończ przy błędach trwałych (np. niepoprawne żądanie), i ogranicz liczbę prób, żeby nie wpadać w nieskończoną pętlę, gdy użytkownik musi podjąć działanie (np. ponowne zalogowanie).

Jak zapobiegać „burzom synchronizacji” i jednocześnie pokazywać postęp widoczny dla użytkownika?

Enqueue’uj synchronizację jako unikalną pracę, by wiele triggerów nie startowało równolegle, i pokazuj postęp, któremu użytkownicy mogą zaufać. Jeśli zadanie jest długotrwałe lub wywołane przez użytkownika, uruchom je jako foreground work z trwającym powiadomieniem, które pokazuje realne liczniki i daje jasną opcję anulowania.

Łatwy do uruchomienia
Stworzyć coś niesamowitego

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

Rozpocznij