20 kwi 2025·7 min czytania

Kotlin — sieciowanie przy wolnych połączeniach: timeouty i bezpieczne ponawianie

Praktyczne sieciowanie w Kotlinie dla wolnych połączeń: ustaw timeouty, bezpiecznie cache’uj, retry bez duplikatów i chroń krytyczne akcje przy niestabilnych sieciach mobilnych.

Kotlin — sieciowanie przy wolnych połączeniach: timeouty i bezpieczne ponawianie

Co psuje się przy wolnych i niestabilnych połączeniach

Na urządzeniach mobilnych „wolne” rzadko oznacza „brak internetu”. Częściej to łącze, które działa tylko przerywanie. Żądanie może trwać 8–20 sekund, zatrzymać się w połowie, a potem dokończyć. Albo sukces przy jednym połączeniu, porażka przy następnym, bo telefon przełączył się z Wi‑Fi na LTE, wszedł w strefę słabego zasięgu, albo system umieścił aplikację w tle.

„Niestabilne” jest gorsze. Pakiety giną, zapytania DNS timeoutują, handshake TLS się nie udaje, a połączenia losowo resetują. Możesz mieć idealny kod i nadal obserwować błędy, bo sieć zmienia się pod twoimi stopami.

Tu domyślne ustawienia zwykle zawodzą. Wielu deweloperów polega na domyślnych wartościach bibliotek dla timeoutów, retry i cache bez zastanowienia, co jest „wystarczająco dobre” dla prawdziwych użytkowników. Domyślne ustawienia często są dopasowane do stabilnego Wi‑Fi i szybkich API, a nie do pociągu do pracy, windy czy zatłoczonej kawiarni.

Użytkownicy nie mówią „timeout gniazda” czy „HTTP 503”. Widzą objawy: nieskończone spinnery, nagłe błędy po długim oczekiwaniu (a potem działa przy następnym podejściu), podwójne akcje (dwie rezerwacje, dwa zamówienia, podwójne obciążenia), utracone aktualizacje i stany mieszane, gdzie UI mówi „nieudane”, a serwer tak naprawdę zakończył operację sukcesem.

Wolne sieci zamieniają małe luki w projekcie w problemy z pieniędzmi i zaufaniem. Jeśli aplikacja nie rozróżnia wyraźnie „wciąż wysyłam” od „nieudane” od „zrobione”, użytkownicy stukają ponownie. Jeśli klient blind‑retryuje, może wywołać duplikaty. Jeśli serwer nie wspiera idempotencji, jedno niestabilne połączenie może wyprodukować wiele „udanych” zapisów.

„Krytyczne akcje” to wszystko, co musi wykonać się co najwyżej raz i być poprawne: płatności, finalizacje zamówień, rezerwacje, transfer punktów, zmiana hasła, zapis adresu wysyłkowego, zgłaszanie roszczenia czy wysyłanie akceptacji.

Realistyczny przykład: ktoś zatwierdza checkout przy słabym LTE. Aplikacja wysyła żądanie, po czym łącze przerywa przed otrzymaniem odpowiedzi. Użytkownik widzi błąd, naciska „Zapłać” ponownie i teraz dwa żądania dochodzą do serwera. Bez jasnych reguł aplikacja nie wie, czy powinna retryować, czekać czy przerwać. Użytkownik nie wie, czy ma ponowić próbę.

Ustal reguły zanim dotkniesz kodu

Gdy łącze jest wolne lub niestabilne, większość błędów wynika z niejasnych reguł, nie z klienta HTTP. Zanim zmienisz timeouty, cache czy retry, zapisz, co znaczy „poprawne” dla twojej aplikacji.

Zacznij od akcji, które nigdy nie mogą wykonać się dwa razy. To zwykle działania związane z pieniędzmi i kontem: złożenie zamówienia, obciążenie karty, wypłata, zmiana hasła, usunięcie konta. Jeśli użytkownik dwukrotnie stuknie albo aplikacja retryuje, serwer i tak powinien potraktować to jak jedno żądanie. Jeśli jeszcze nie potrafisz tego zagwarantować, traktuj te endpointy jako „bez automatycznego retry”.

Następnie zdecyduj, co każdy ekran może robić przy złej sieci. Niektóre ekrany mogą być użyteczne offline (ostatni znany profil, poprzednie zamówienia). Inne powinny przejść w tryb tylko do odczytu lub pokazywać wyraźny stan „spróbuj ponownie” (stany magazynowe, ceny na żywo). Mieszanie tych oczekiwań prowadzi do mylącego UI i niebezpiecznego cache.

Ustal akceptowalny czas oczekiwania dla każdej akcji w oparciu o to, jak użytkownicy to postrzegają, a nie co ładnie wygląda w kodzie. Logowanie zniesie krótki czas oczekiwania. Upload pliku potrzebuje dłużej. Checkout powinien być szybki, ale też bezpieczny. Timeout 30 sekund może być „na papierze” niezawodny, a w praktyce sprawiać, że użytkownik poczuje się, jakby coś się zepsuło.

Na koniec zdecyduj, co będziesz przechowywać na urządzeniu i jak długo. Cache pomaga, ale przeterminowane dane mogą prowadzić do złych decyzji (stare ceny, wygasłe uprawnienia).

Zapisz reguły w miejscu dostępnym dla całego zespołu (README wystarczy). Utrzymaj prostotę:

  • Które endpointy „nie mogą się zduplikować” i wymagają obsługi idempotencji?
  • Które ekrany muszą działać offline, a które są tylko do odczytu offline?
  • Jaki jest maksymalny czas oczekiwania dla poszczególnych akcji (logowanie, odświeżanie feedu, upload, checkout)?
  • Co można cache’ować na urządzeniu i jaki jest czas wygaśnięcia?
  • Po porażce: pokazujesz błąd, odkładasz do kolejki na później, czy wymagasz ręcznego ponowienia?

Gdy te reguły są jasne, wartości timeoutów, nagłówki cache, polityka retry i stany UI łatwiej zaimplementować i przetestować.

Timeouty dopasowane do oczekiwań użytkownika

Wolne sieci zawodzą na różne sposoby. Dobre ustawienie timeoutów nie polega na „wylosowaniu liczby”. Powinno odpowiadać temu, co użytkownik próbuje zrobić, i kończyć się na tyle szybko, by aplikacja mogła się zregenerować.

Trzy timeouty, prosto:

  • Connect timeout: jak długo czekasz na zestawienie połączenia z serwerem (DNS, TCP, TLS). Jeśli to się nie uda, żądanie w zasadzie nigdy się nie rozpoczęło.
  • Write timeout: jak długo czekasz podczas wysyłania ciała żądania (uploady, duże JSONy, wolny uplink).
  • Read timeout: jak długo czekasz, aż serwer zacznie wysyłać dane po wysłaniu żądania. To często objawia się na niestabilnych sieciach mobilnych.

Timeouty powinny odzwierciedlać ekran i wagę akcji. Feed może być wolniejszy bez większych konsekwencji. Krytyczna akcja powinna albo zakończyć się, albo wyraźnie zgłosić błąd, by użytkownik wiedział, co dalej.

Praktyczny punkt wyjścia (dostosuj po pomiarach):

  • Ładowanie listy (niski risk): connect 5–10 s, read 20–30 s, write 10–15 s.
  • Szukaj w czasie pisania: connect 3–5 s, read 5–10 s, write 5–10 s.
  • Akcje krytyczne (wysokie ryzyko, np. „Zapłać” lub „Złóż zamówienie”): connect 5–10 s, read 30–60 s, write 15–30 s.

Spójność jest ważniejsza niż perfekcja. Jeśli użytkownik stuknie „Wyślij” i widzi spinner przez dwie minuty, stuknie ponownie.

Unikaj „nieskończonego ładowania” też w UI — dodaj górny limit. Pokaż progres od razu, pozwól anulować, a po (powiedzmy) 20–30 sekundach pokaż „Wciąż próbujemy…” z opcjami ponowienia lub sprawdzenia połączenia. To daje uczciwe doświadczenie nawet gdy biblioteka sieciowa wciąż czeka.

Gdy timeout nastąpi, loguj wystarczająco dużo, by debugować wzorce, ale bez sekretów. Przydatne pola: ścieżka URL (nie pełny query), metoda HTTP, status (jeśli jest), rozbicie czasów (connect vs write vs read jeśli dostępne), typ sieci (Wi‑Fi, komórkowa, tryb samolotowy), przybliżony rozmiar żądania/odpowiedzi i ID żądania, by powiązać logi klienta z logami serwera.

Proste, spójne ustawienie sieciowe w Kotlinie

Przy wolnych połączeniach drobne niespójności w konfiguracji klienta stają się dużymi problemami. Czysta baza pomaga szybciej debugować i daje każdemu żądaniu te same reguły.

Jeden klient, jedna polityka

Zacznij od jednego miejsca, gdzie budujesz klienta HTTP (zwykle jeden OkHttpClient używany przez Retrofit). Umieść tam podstawy, żeby każde żądanie zachowywało się tak samo: domyślne nagłówki (wersja aplikacji, locale, token auth), jasny User‑Agent, timeouty ustawione centralnie (nie rozsypane po wywołaniach), logowanie możliwe do włączenia i jedna decyzja dotycząca retry (nawet jeśli to „bez automatycznych retry”).

Oto mały przykład, który trzyma konfigurację w jednym pliku:

val okHttp = OkHttpClient.Builder()
  .connectTimeout(10, TimeUnit.SECONDS)
  .readTimeout(20, TimeUnit.SECONDS)
  .writeTimeout(20, TimeUnit.SECONDS)
  .callTimeout(30, TimeUnit.SECONDS)
  .addInterceptor { chain ->
    val request = chain.request().newBuilder()
      .header("User-Agent", "MyApp/${BuildConfig.VERSION_NAME}")
      .header("Accept", "application/json")
      .build()
    chain.proceed(request)
  }
  .build()

val retrofit = Retrofit.Builder()
  .baseUrl(BASE_URL)
  .client(okHttp)
  .addConverterFactory(MoshiConverterFactory.create())
  .build()

Centralne mapowanie błędów na komunikaty dla użytkownika

Błędy sieciowe to nie tylko „wyjątek”. Jeśli każdy ekran obsługuje je inaczej, użytkownicy dostają chaotyczne komunikaty.

Stwórz jeden mapper, który konwertuje awarie na mały zestaw przyjaznych wyników: brak połączenia/tryb samolotowy, timeout, błąd serwera (5xx), błąd walidacji lub autoryzacji (4xx) oraz nieznany fallback.

To utrzymuje spójność copy w UI („Brak połączenia” vs „Spróbuj ponownie”) bez wycieku technicznych szczegółów.

Oznaczaj i anuluj żądania przy zamykaniu ekranów

Na niestabilnych sieciach wywołania mogą zakończyć się późno i zaktualizować ekran, którego już nie ma. Uczyń anulowanie standardową regułą: gdy ekran się zamyka, jego praca powinna zostać zatrzymana.

Z Retrofitem i Kotlin coroutines anulowanie scope’a (np. w ViewModel) anuluje odpowiadające wywołanie HTTP. Dla wywołań nie‑coroutine trzymaj referencję do Call i wywołaj cancel(). Możesz też tagować żądania i anulować grupy wywołań przy opuszczeniu funkcji.

Prace w tle nie powinny zależeć od UI

Wszystko, co musi się dokończyć niezależnie od UI (wysłanie raportu, synchronizacja kolejki, dopięcie zgłoszenia), powinno być oddelegowane do scheduler’a. Na Androidzie zwykle używa się WorkManagera, bo potrafi retryować później i przetrwać restarty aplikacji. Trzymaj akcje UI lekkie i oddelegowuj dłuższe operacje do pracy w tle, gdy ma to sens.

Zasady cache bezpieczne na mobilnych łączach

Generuj backend i aplikacje mobilne
Stwórz produkcyjny backend w Go i natywne aplikacje mobilne bez dopinania każdego endpointu ręcznie.
Zacznij budować

Cache może dać duże korzyści przy wolnych połączeniach: zmniejsza powtarzane pobrania i sprawia, że ekrany wydają się natychmiastowe. Może też zaszkodzić, pokazując przeterminowane dane w niewłaściwym momencie, np. stary stan konta czy nieaktualny adres dostawy.

Bezpieczne podejście: cache’uj tylko to, co użytkownik może tolerować jako lekko przeterminowane, i wymuszaj świeżość dla wszystkiego, co wpływa na pieniądze, bezpieczeństwo lub ostateczną decyzję.

Podstawy Cache‑Control, na których możesz polegać

Większość zasad sprowadza się do kilku nagłówków:

  • max-age=60: możesz ponownie użyć odpowiedzi z cache przez 60 sekund bez pytania serwera.
  • no-store: nie zapisuj tej odpowiedzi wcale (najlepsze dla tokenów i wrażliwych ekranów).
  • must-revalidate: jeśli wygasła, musisz sprawdzić u serwera przed ponownym użyciem.

Na mobilnym urządzeniu must-revalidate zapobiega „cicho błędnym” danym po chwilowym offline. Jeśli użytkownik otwiera aplikację po jeździe metrem, chcesz szybki ekran, ale też potwierdzenie, że dane nadal są aktualne.

Odtwarzanie przez ETag: szybkie, tanie i niezawodne

Dla endpointów odczytu, walidacja oparta na ETag często działa lepiej niż długie max-age. Serwer wysyła ETag z odpowiedzią. Następnym razem aplikacja wysyła If-None-Match z tą wartością. Jeśli nic się nie zmieniło, serwer odpowiada 304 Not Modified, co na słabym łączu jest małe i szybkie.

To dobrze działa dla list produktów, szczegółów profilu i ustawień.

Proste reguły:

  • Cache’uj endpointy „read” z krótkim max-age plus must-revalidate i wspieraj ETag, jeśli możesz.
  • Nie cache’uj endpointów zapisu (POST/PUT/PATCH/DELETE). Traktuj je jako zawsze zależne od sieci.
  • Używaj no-store dla wszystkiego wrażliwego (odpowiedzi auth, kroki płatności, prywatne wiadomości).
  • Cache’uj dłużej statyczne zasoby (ikony, publiczna konfiguracja), bo ryzyko przeterminowania jest niskie.

Utrzymuj spójne decyzje cache w całej aplikacji. Użytkownicy zauważą niespójności bardziej niż opóźnienia.

Bezpieczne retry, które nie pogorszy sprawy

Posiadaj kod źródłowy
Zdobądź prawdziwy kod źródłowy, który możesz eksportować, przeglądać i wdrażać tam, gdzie potrzebujesz.
Wygeneruj kod

Retry wydają się prostym remedium, ale mogą zaszkodzić. Jeśli retryujesz złe żądania, zwiększasz obciążenie, drenżysz baterię i sprawiasz, że aplikacja wydaje się zawieszona.

Zacznij od retry tylko tam, gdzie awaria jest prawdopodobnie tymczasowa. Upuszczenie połączenia, timeout odczytu czy krótka awaria serwera mogą się powieść przy kolejnym podejściu. Błędne hasło, brakujące pole czy 404 nie.

Praktyczny zestaw reguł:

  • Retryuj timeouty i błędy połączenia.
  • Retryuj 502, 503 i czasem 504.
  • Nie retryuj 4xx (poza 408 lub 429, jeśli masz jasną regułę czekania).
  • Nie retryuj żądań, które już dotarły do serwera i mogą być przetwarzane.
  • Trzymaj retry nisko (zwykle 1–3 próby).

Backoff + jitter: mniej fal retry

Jeśli wielu użytkowników trafi na tę samą awarię, natychmiastowe retry mogą stworzyć falę ruchu, która utrudni powrót systemu. Użyj wykładniczego backoffu (coraz dłuższe przerwy) i dodaj jitter (małe losowe opóźnienie), żeby urządzenia nie retryowały synchronicznie.

Przykład: czekaj ~0.5 s, potem 1 s, potem 2 s, z losowym +/-20%.

Nałóż limit na całkowity czas retry

Bez limitów retry mogą uwięzić użytkownika przy spinnerze na minuty. Ustal maksymalny czas całej operacji, wliczając wszystkie przerwy. Wiele aplikacji celuje w 10–20 sekund, po których pokazuje wyraźną opcję ponowienia.

Dopasuj też kontekst. Przy formularzu użytkownik chce szybkiej odpowiedzi. Przy background sync możesz retryować później.

Nigdy nie auto‑retryuj nie‑idempotentnych akcji (np. złożenie zamówienia, wysłanie płatności), chyba że masz zabezpieczenie, jak klucz idempotencyjny lub serwerowe sprawdzenie duplikatów. Jeśli nie możesz zagwarantować bezpieczeństwa, zakończ z błędem i pozwól użytkownikowi zdecydować.

Zapobieganie duplikatom przy akcjach krytycznych

Na wolnym lub niestabilnym łączu użytkownicy stukają dwukrotnie. System operacyjny może retryować w tle. Twoja aplikacja może ponownie wysłać po timeout. Jeśli akcja tworzy coś nowego (zamówienie, transfer środków, zmiana hasła), duplikaty są szkodliwe.

Idempotencja oznacza, że powtórzenie żądania powinno dać ten sam wynik. Jeśli żądanie się powtórzy, serwer nie powinien stworzyć drugiego zamówienia — powinien zwrócić pierwszy rezultat lub odpowiedzieć „już wykonane”.

Użyj klucza idempotencyjnego dla każdej krytycznej próby

Dla krytycznych akcji generuj unikalny klucz idempotencyjny przy starcie próby i wysyłaj go z żądaniem (np. w nagłówku Idempotency-Key albo w polu body).

Praktyczny przebieg:

  • Stwórz UUID jako klucz idempotencyjny, gdy użytkownik kliknie „Zapłać”.
  • Zapisz go lokalnie z małym rekordem: status = pending, createdAt, hash payloadu żądania.
  • Wyślij żądanie z kluczem.
  • Po sukcesie oznacz status = done i zapisz ID wyniku zwrócone przez serwer.
  • Przy retry używaj tego samego klucza, a nie nowego.

To „używaj tego samego klucza” powstrzymuje przypadkowe podwójne obciążenia.

Obsłuż restarty aplikacji i przerwy offline

Jeśli aplikacja zostanie zabita w trakcie żądania, kolejne uruchomienie nadal musi być bezpieczne. Przechowaj klucz idempotencyjny i stan żądania w lokalnym storage (np. mały rekord w bazie). Po restarcie albo retryjuj z tym samym kluczem, albo wywołaj endpoint „check status” używając zapisanego klucza lub ID serwera.

Po stronie serwera kontrakt powinien być jasny: przy duplikacie klucza odrzuć drugą próbę lub zwróć oryginalną odpowiedź (to samo ID zamówienia, ten sam paragon). Jeśli serwer tego nie potrafi, klientowa ochrona przed duplikatami nigdy nie będzie w pełni niezawodna, bo aplikacja nie zobaczy, co się wydarzyło po wysłaniu żądania.

Dobry dotyk UX: jeśli próba jest pending, pokaż „Płatność w toku” i zablokuj przycisk do czasu otrzymania finalnego wyniku.

Wzorce UI redukujące przypadkowe ponowne wysyłanie

Zmieniaj specyfikacje bez bałaganu
Modeluj dane PostgreSQL i logikę biznesową w jednym miejscu, a potem regeneruj, gdy wymagania się zmienią.
Utwórz projekt

Wolne połączenia nie tylko psują żądania. Zmienią sposób, w jaki ludzie stukają. Gdy ekran „zamiera” na 2 sekundy, wielu użytkowników zakłada, że nic się nie stało i naciska przycisk ponownie. UI musi sprawić, żeby „jedno stuknięcie” było wiarygodne nawet przy słabym łączu.

Optimistyczne UI jest bezpieczne, gdy akcja jest odwracalna lub niskiego ryzyka (oznaczenie gwiazdką, zapis szkicu, oznaczenie jako przeczytane). Potwierdzone UI jest lepsze dla pieniędzy, stanów magazynowych, nieodwracalnych usuń i wszystkiego, co może stworzyć duplikaty.

Dobry domyślny wzorzec dla akcji krytycznych to wyraźny stan pending. Po pierwszym tapnięciu natychmiast zamień główny przycisk na „Wysyłanie…”, wyłącz go i pokaż krótki komunikat wyjaśniający, co się dzieje.

Wzorce działające dobrze przy niestabilnych sieciach:

  • Wyłącz główną akcję po tapnięciu i trzymaj ją wyłączoną aż do finalnego wyniku.
  • Pokaż widoczny status „Pending” z detalami (kwota, odbiorca, liczba pozycji).
  • Dodaj prosty widok „Ostatnia aktywność”, żeby użytkownicy mogli potwierdzić, co już wysłali.
  • Jeśli aplikacja przejdzie do tła, zachowaj stan pending po powrocie.
  • Preferuj jeden jasny przycisk główny zamiast wielu celów kliknięcia na tym samym ekranie.

Czasem żądanie się powiedzie, ale odpowiedź zostanie utracona. Traktuj to jako normalny rezultat, a nie błąd, który zachęca do powtórzeń. Zamiast „Nie powiodło się, spróbuj ponownie”, pokaż „Nie jesteśmy jeszcze pewni” i zaproponuj bezpieczny krok, jak „Sprawdź status”. Jeśli nie możesz sprawdzić statusu, zachowaj lokalny rekord pending i powiadom użytkownika, że zaktualizujesz po powrocie połączenia.

Spraw, by „Spróbuj ponownie” było jawne i bezpieczne. Pokazuj je tylko wtedy, gdy możesz powtórzyć żądanie używając tego samego ID klienta lub klucza idempotencyjnego.

Realistyczny przykład: niestabilne złożenie checkoutu

Zamień reguły w API
Zaprojektuj modele danych i endpointy API wizualnie, a następnie wdrażaj spójne zachowanie we wszystkich klientach.
Utwórz API

Klient jest w pociągu ze słabym sygnałem. Dodaje produkty do koszyka i klika Zapłać. Aplikacja musi być cierpliwa, ale też nie może stworzyć dwóch zamówień.

Bezpieczna sekwencja wygląda tak:

  1. Aplikacja tworzy klient‑side attempt ID i wysyła request checkout z kluczem idempotencyjnym (np. UUID zapisany z koszykiem).
  2. Żądanie czeka na connect timeout, potem na dłuższy read timeout. Pociąg wjeżdża w tunel i połączenie timeoutu.
  3. Aplikacja retryuje raz, ale tylko po krótkim opóźnieniu i tylko jeśli nigdy nie otrzymała odpowiedzi od serwera.
  4. Serwer otrzymuje drugie żądanie i widzi ten sam klucz idempotencyjny, więc zwraca oryginalny rezultat zamiast tworzyć nowe zamówienie.
  5. Aplikacja pokazuje finalne potwierdzenie, gdy otrzyma odpowiedź sukcesu, nawet jeśli przyszła z retry.

Cache trzyma się ścisłych reguł. Listy produktów, opcje dostawy i tabele podatków można cache’ować krótko (GET). Złożenie checkoutu (POST) nigdy nie jest cache’owane. Nawet jeśli używasz HTTP cache, traktuj go jako pomoc do przeglądania, nie jako mechanizm „pamiętający” płatność.

Zapobieganie duplikatom to miks decyzji sieciowych i UI. Gdy użytkownik stuknie Zapłać, przycisk jest wyłączony, a ekran pokazuje „Wysyłanie zamówienia...” z opcją Cancel. Jeśli aplikacja straci sieć, przechodzi w „Wciąż próbujemy” i trzyma ten sam attempt ID. Jeśli użytkownik wymusi zamknięcie i ponowne otwarcie, aplikacja może wznowić, sprawdzając status zamówienia przy użyciu tego ID, zamiast prosić o ponowną płatność.

Szybka checklista i następne kroki

Jeśli twoja aplikacja działa „w miarę” na biurowym Wi‑Fi, a pada w pociągach, windach czy na terenach wiejskich, potraktuj to jako warunek wypuszczenia. Ta praca to mniej sprytne algorytmy, a więcej jasnych reguł, które można powielać.

Checklista przed wydaniem:

  • Ustaw timeouty per typ endpointu (logowanie, feed, upload, checkout) i testuj na throttle’owanych i wysokolatencyjnych sieciach.
  • Retryuj tylko tam, gdzie to naprawdę bezpieczne, i ogranicz to backoffem (kilka prób dla odczytów, zwykle żadnych dla zapisów).
  • Dodaj klucz idempotencyjny dla każdego krytycznego zapisu (płatności, zamówienia, wysyłki formularzy), by retry lub podwójne tapnięcie nie tworzyły duplikatów.
  • Wyraźnie określ reguły cache: co można serwować przeterminowane, co musi być świeże, a co nigdy nie powinno być cache’owane.
  • Uczyń stany widocznymi: pending, failed i completed powinny wyglądać inaczej, a aplikacja powinna pamiętać zakończone akcje po restarcie.

Jeśli coś z tego jest „zdecydujemy później”, otrzymasz losowe zachowanie między ekranami.

Następne kroki, żeby to ugruntować

Napisz jednostronicową politykę sieciową: kategorie endpointów, cele timeoutów, reguły retry i oczekiwania cache. Wymuś ją w jednym miejscu (interceptory, fabryka klienta lub małe wrappery), żeby każdy członek zespołu domyślnie miał te same zachowania.

Zrób krótki drill duplikatów. Wybierz jedną krytyczną akcję (np. checkout), zasymuluj zamrożony spinner, wymuś zamknięcie aplikacji, przełącz tryb samolotowy i naciśnij przycisk ponownie. Jeśli nie potrafisz udowodnić, że jest bezpiecznie, użytkownicy w końcu znajdą sposób, by to połamać.

Jeśli chcesz wdrożyć te same reguły po obu stronach — backend i klient — bez ręcznego dopinania wszystkiego, AppMaster (appmaster.io) może pomóc, generując produkcyjny backend i natywne aplikacje mobilne. Nawet wtedy klucz to polityka: zdefiniuj idempotencję, retry, cache i stany UI raz i stosuj konsekwentnie w całym flow.

FAQ

Co powinienem zrobić najpierw, zanim poprawię timeouty i retry?

Zacznij od zdefiniowania, co „poprawne” znaczy dla każdego ekranu i akcji, zwłaszcza tych, które muszą wykonać się co najwyżej raz — płatności czy zamówienia. Gdy zasady będą jasne, ustaw timeouty, retry, cache i stany UI tak, by je odwzorowywały, zamiast polegać na domyślnych ustawieniach bibliotek.

Jakie są najczęstsze objawy, które zauważają użytkownicy przy wolnych lub niestabilnych sieciach?

Użytkownicy zwykle widzą wieczne ładowanie, błędy po długim oczekiwaniu, akcje udane dopiero za drugim razem lub zduplikowane wyniki (dwa zamówienia, podwójne obciążenie). To często wynika z niejasnych zasad retry i rozróżnienia „oczekujące vs nieudane”, a nie tylko ze słabego zasięgu.

Jak myśleć o connect, read i write timeoutach na urządzeniach mobilnych?

Użyj connect timeout do określenia, jak długo czekasz na nawiązanie połączenia; write timeout dla wysyłania ciała żądania (uploady); read timeout dla oczekiwania na odpowiedź po wysłaniu. Rozsądna zasada: krótsze timeouty dla niskoryzykownych odczytów, dłuższe dla krytycznych zapisów, a w UI daj górny limit, żeby użytkownik nie czekał w nieskończoność.

Jeśli mogę ustawić tylko jeden timeout w OkHttp, który powinienem wybrać?

Tak — jeśli możesz ustawić tylko jedno, użyj callTimeout, aby ograniczyć całe żądanie end-to-end i uniknąć „nieskończego” oczekiwania. Na to możesz nałożyć connect/read/write timeouty, jeśli chcesz lepszej kontroli, zwłaszcza dla uploadów i wolnych strumieni odpowiedzi.

Jakie błędy zwykle można bezpiecznie ponowić, a jakie nie?

Powtarzaj tylko tymczasowe awarie: utratę połączenia, DNS, timeouty, i czasami 502/503/504. Unikaj retry dla błędów 4xx (chyba że to 408 lub 429 i masz jasną regułę czekania). Nie retryuj zapisu, chyba że masz ochronę idempotencji — inaczej ryzykujesz duplikaty.

Jak dodać retry, nie sprawiając, że aplikacja będzie wydawać się zawieszona?

Używaj niewielkiej liczby prób (1–3) z wykładniczym backoffem i odrobiną jittera, żeby urządzenia nie retryowały synchronicznie. Nałóż też limit całkowitego czasu retry, żeby użytkownik otrzymał jasny wynik zamiast spinnera trwającego minuty.

Czym jest idempotencja i dlaczego jest ważna przy płatnościach i zamówieniach?

Idempotencja oznacza, że powtórzenie tego samego żądania nie stworzy drugiego wyniku — podwójne kliknięcie lub retry nie zdubluje płatności ani zamówienia. Dla krytycznych akcji wysyłaj klucz idempotencyjny dla próby i wykorzystuj go ponownie przy retry, aby serwer mógł zwrócić pierwotny rezultat.

Jak generować i przechowywać klucz idempotencyjny na Androidzie?

Wygeneruj unikalny klucz przy rozpoczęciu akcji, zapisz go lokalnie z krótkim rekordem „pending”, i wyślij z żądaniem. Przy retry lub restarcie aplikacji użyj tego samego klucza i albo bezpiecznie powtórz żądanie, albo zapytaj serwer o status, tak żeby jedna intencja użytkownika nie stała się dwoma zapisami po stronie serwera.

Jakie zasady cache są najbezpieczniejsze dla aplikacji mobilnych na zawodnych łączach?

Cacheuj tylko dane, które mogą być lekko przeterminowane, i wymuszaj świeżość dla wszystkiego, co dotyczy pieniędzy, bezpieczeństwa czy ostatecznej decyzji. Dla odczytów preferuj krótką świeżość plus rewalidację (ETag); dla zapisów nie cacheuj niczego, a dla wrażliwych odpowiedzi użyj no-store.

Jakie wzorce UI ograniczają podwójne przyciski i przypadkowe ponowne wysyłanie na wolnych sieciach?

Wyłącz główny przycisk po pierwszym tapnięciu, pokaż natychmiastowe „Submitting…” i widoczny status pending, który przetrwa wyjście do tła i restarty. Jeśli odpowiedź mogła zostać utracona, zamiast zachęcać do ponownych klików pokaż niepewność („Nie jesteśmy pewni”) i zaoferuj bezpieczny krok, np. „Sprawdź status”.

Łatwy do uruchomienia
Stworzyć coś niesamowitego

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

Rozpocznij