Utrata połączenia — jedno z najczęstszych i najbardziej irytujących zjawisk w aplikacjach mobilnych. Użytkownik traci dostęp do danych, operacja zostaje przerwana, aplikacja zawiesza się lub ulega awarii. Według Google Android Developer Blog, 70% użytkowników usuwa aplikację, jeśli ulegnie ona awarii lub zawiesi się dwukrotnie. Omówimy przyczyny utraty połączenia oraz sposoby budowania niezawodnej komunikacji.
Najważniejsze
Odpada — termin użytkownika opisujący sytuację, gdy aplikacja traci połączenie z serwerem, przestaje reagować na działania lub kończy się błędem. W sensie technicznym może to być: błąd sieciowy (timeout, DNS failure), ANR (blokada wątku UI), crash (nieobsłużony wyjątek) lub race condition (stan wyścigu).
Dla użytkownika wszystkie te scenariusze wyglądają tak samo: aplikacja przestaje działać. Różnica dla programisty polega na podejściu do diagnostyki i naprawy. Błędy sieciowe rozwiązuje się mechanizmami retry, ANR — przenoszeniem operacji poza wątek UI, crash — obsługą wyjątków.
Według Crittercism (obecnie Apteligent), przeciętna aplikacja mobilna traci 1-2% użytkowników przy każdym błędzie. Dla aplikacji z 1 milionem użytkowników to 10-20 tysięcy utraconych instalacji na jeden błąd. Szczególnie krytyczne jest to dla aplikacji w sektorze finansowym i medycznym.
Niestabilna sieć — urządzenia mobilne stale przełączają się między Wi-Fi a siecią komórkową, wchodzą w strefy bez zasięgu (metro, winda, piwnica). Każde przełączenie powoduje chwilową utratę połączenia, którą aplikacja musi poprawnie obsłużyć.
Timeouty — jeśli serwer nie odpowiada w ustalonym czasie (zwykle 10-30 sekund), klient zgłasza SocketTimeoutException. Długie timeouty bez informacji zwrotnej są odbierane przez użytkownika jako zawieszenie. Zaleca się ustawienie timeoutu nie dłuższego niż 15 sekund.
val client = OkHttpClient.Builder()
.connectTimeout(10, TimeUnit.SECONDS)
.readTimeout(15, TimeUnit.SECONDS)
.writeTimeout(15, TimeUnit.SECONDS)
.retryOnConnectionFailure(true)
.build()
Stan wyścigu (race condition) — powstaje, gdy wiele wątków jednocześnie czyta i zapisuje te same dane bez synchronizacji. Na przykład ładowanie danych z pamięci podręcznej w wątku UI równolegle z aktualizacją pamięci podręcznej z sieci może prowadzić do wyświetlania nieaktualnych lub nieprawidłowych danych.
Offline-first — wzorzec architektoniczny, w którym lokalne przechowywanie (Room, CoreData) jest jedynym źródłem prawdy. Sieć jest używana do synchronizacji danych w tle. Użytkownik zawsze widzi aktualne dane z lokalnej pamięci podręcznej, nawet przy braku sieci.
Repository pattern — jeden punkt wejścia dla danych, który decyduje, czy pobrać dane z sieci czy z pamięci podręcznej. Repozytorium abstrahuje źródło danych od ViewModel i UI. Przy błędzie sieciowym repozytorium automatycznie przełącza się na lokalne źródło.
class UserRepository(
private val api: UserApi,
private val dao: UserDao
) {
suspend fun getUsers(): Result<List<User>> {
return try {
val remote = api.fetchUsers()
dao.insertAll(remote)
Result.success(remote)
} catch (e: IOException) {
val cached = dao.getAll()
if (cached.isNotEmpty()) {
Result.success(cached) // zwróć pamięć podręczną przy błędzie sieci
} else {
Result.failure(e)
}
}
}
}
Circuit Breaker — wzorzec ochrony serwera przed lawiną żądań przy niedostępności. Po N kolejnych błędach wyłącznik otwiera się i wszystkie żądania natychmiast zwracają błąd bez próby połączenia. Po zadanym czasie oczekiwania wyłącznik przechodzi do stanu półotwartego w celu wykonania próbnego żądania.
Exponential backoff — standardowy mechanizm retry. Po pierwszej porażce odczekaj 1 sekundę, po drugiej — 2 sekundy, następnie 4, 8, 16. Ogranicz maksymalną liczbę prób (zwykle 3-5), aby nie przeciążać serwera i baterii.
Informacja zwrotna dla użytkownika — przy błędzie sieciowym pokaż zrozumiały komunikat: „Brak połączenia”, „Serwer tymczasowo niedostępny”, „Sprawdź internet”. Użyj Snackbar lub Inline State View. Nigdy nie pokazuj użytkownikowi błędów technicznych (HTTP 500, SocketException).
ConnectivityManager — Android API do monitorowania sieci. Pozwól aplikacji reagować na zmiany: przy utracie sieci pokazuj placeholder, przy przywróceniu — automatycznie aktualizuj dane. W iOS używaj NWPathMonitor z frameworku Network.
class NetworkMonitor(private val context: Context) {
private val manager =
context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
fun isOnline(): Boolean {
val network = manager.activeNetwork ?: return false
val caps = manager.getNetworkCapabilities(network) ?: return false
return caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
}
}
Crashlytics (Firebase) — standardowe narzędzie do raportowania awarii dla aplikacji mobilnych. Zbiera stacktrace wszystkich nieobsłużonych wyjątków, wersję systemu operacyjnego, model urządzenia i czas awarii. Pozwala grupować błędy i przypisywać osoby odpowiedzialne za naprawy.
Sentry — alternatywa dla Crashlytics z obsługą monitorowania wydajności. Pozwala śledzić konkretne transakcje (np. „autoryzacja użytkownika”) i widzieć, na którym kroku wystąpił błąd. Performance tracing pomaga odróżnić timeouty sieciowe od błędów w logice aplikacji.
Timber — biblioteka logowania dla Android z automatycznym dodawaniem tagów według klasy. W kompilacji debug loguj wszystkie żądania sieciowe i odpowiedzi. W kompilacji release — tylko błędy i ostrzeżenia przez Crashlytics.setCustomLog.
| Narzędzie | Typ | Kiedy używać |
|---|---|---|
| Crashlytics | Crash reporting | Zawsze w release — automatyczne zbieranie awarii |
| Sentry | Crash + Performance | Gdy potrzebujesz profilować konkretne scenariusze użytkownika |
| Timber | Logging | Debug: pełne logowanie; Release: tylko błędy |
| HTTP Toolkit | Network debug | Lokalne przechwytywanie i analiza ruchu HTTP |
Według Firebase Summit 2023, aplikacje, które wdrożyły Crashlytics + Performance Monitoring, skracają średni czas wykrywania i naprawy krytycznych błędów z 3 dni do 4 godzin. Zaleca się skonfigurowanie alertów na każdy błąd z częstotliwością większą niż 0.1% aktywnych użytkowników.
Często zadawane pytania
Jeśli crash nie jest łapany w Crashlytics, sprawdź native crash (SIGSEGV, SIGABRT) — nie są one obsługiwane przez handler wyjątków Java/Kotlin. W Android może to być wyciek pamięci native z JNI, w iOS — EXC_BAD_ACCESS. Użyj Breakpad (Android) lub PLCrashReporter (iOS) do zbierania stacktrace native crash.
Użyj Network Link Conditioner (wbudowany w iOS, dla Android jest Facebook Network Connection Class lub ustawienia Developer Options > Network > Select network type). Ustaw opóźnienie 500-3000 ms i utratę pakietów 5-30%. Można również użyć Charles Proxy lub mitmproxy do emulacji opóźnień sieciowych i zerwań połączeń.
ANR występuje, gdy wątek UI jest zablokowany dłużej niż 5 sekund. Żądania sieciowe powinny być wykonywane w wątku tła: coroutines (viewModelScope.launch(Dispatchers.IO)), RxJava (subscribeOn(Schedulers.io)) lub WorkManager do synchronizacji. Zawsze ustawiaj timeouty na kliencie HTTP — brak timeoutu może prowadzić do wiecznej blokady.
Race condition — sytuacja, w której wynik operacji zależy od kolejności wykonywania wątków. Na przykład użytkownik szybko naciska przycisk „Wyślij” dwa razy i żądanie zostaje wysłane dwukrotnie. Rozwiązanie: użyj Mutex, single-threaded executors lub state machine (wyłącz przycisk po pierwszym naciśnięciu). W Kotlin używaj Mutex z coroutines lub adnotacji @Synchronized.
Stosuj Chaos Engineering dla aplikacji mobilnych: wyłączaj sieć podczas operacji, symuluj wysokie opóźnienie, przełączaj między Wi-Fi a siecią komórkową, zabijaj proces przez system. Narzędzia: Facebook Network Connection Class, Charles Proxy, iOS Network Link Conditioner. W CI/CD dodawaj testy UI z różnymi warunkami sieciowymi przez AndroidTest Orchestrator.
Podsumowanie
Opracujemy aplikację mobilną pod klucz
IT Sectr tworzy aplikacje na iOS i Androida dla startupów i firm od 2017 roku. Doradzimy Ci i zaproponujemy najlepsze rozwiązanie.
Przeczytaj również