Втрата з'єднання — одне з найпоширеніших і найдратівливіших явищ у мобільних застосунках. Користувач втрачає доступ до даних, операція переривається, застосунок зависає або вилітає. За даними Google Android Developer Blog, 70% користувачів видаляють застосунок, якщо він вилітає або зависає двічі. Розберемо причини втрати з'єднання та способи побудови відмовостійких комунікацій.
Головне
Обривається — користувацький термін, що описує ситуацію, коли застосунок втрачає з'єднання з сервером, перестає відповідати на дії або завершується з помилкою. У технічному сенсі це може бути: мережева помилка (таймаут, DNS failure), ANR (блокування UI-потоку), crash (необроблений виняток) або race condition (стан гонки).
Для користувача всі ці сценарії виглядають однаково: застосунок перестає працювати. Різниця для розробника — у підході до діагностики та виправлення. Мережеві помилки вирішуються retry-механізмами, ANR — винесенням операцій з UI-потоку, crash — exception handling.
За даними Crittercism (тепер Apteligent), в середньому мобільний застосунок втрачає 1-2% користувачів при кожному падінні. Для застосунку з 1 млн користувачів це 10-20 тисяч втрачених встановлень на один баг. Особливо критично для застосунків у фінансовому та медичному секторах.
Нестабільна мережа — мобільні пристрої постійно перемикаються між Wi-Fi та мобільною мережею, заходять у зони без покриття (метро, ліфт, підвал). Кожне перемикання викликає тимчасову втрату з'єднання, яку застосунок має обробляти коректно.
Таймаути — якщо сервер не відповідає протягом встановленого таймауту (зазвичай 10-30 секунд), клієнт кидає SocketTimeoutException. Довгі таймаути без зворотного зв'язку сприймаються користувачем як зависання. Рекомендується встановлювати таймаут не більше 15 секунд.
val client = OkHttpClient.Builder()
.connectTimeout(10, TimeUnit.SECONDS)
.readTimeout(15, TimeUnit.SECONDS)
.writeTimeout(15, TimeUnit.SECONDS)
.retryOnConnectionFailure(true)
.build()
Стан гонки (race condition) — виникає, коли кілька потоків одночасно читають і пишуть одні й ті самі дані без синхронізації. Наприклад, завантаження даних з кешу в UI-потік паралельно з оновленням кешу з мережі може призвести до відображення застарілих або некоректних даних.
Offline-first — архітектурний патерн, при якому локальне сховище (Room, CoreData) є єдиним джерелом істини. Мережа використовується для синхронізації даних у фоні. Користувач завжди бачить актуальні дані з локального кешу, навіть при відсутності мережі.
Repository pattern — єдина точка входу для даних, яка вирішує, брати дані з мережі чи з кешу. Репозиторій абстрагує джерело даних від ViewModel та UI. При мережевій помилці репозиторій автоматично перемикається на локальне джерело.
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) // return cache on network error
} else {
Result.failure(e)
}
}
}
}
Circuit Breaker — патерн захисту сервера від лавини запитів при недоступності. Після N послідовних помилок вимикач розмикається, і всі запити одразу повертають помилку без спроби з'єднання. Через заданий таймаут вимикач переходить у напіввідкритий стан для пробного запиту.
Exponential backoff — стандартний механізм retry. Після першої невдачі зачекати 1 секунду, після другої — 2 секунди, потім 4, 8, 16. Обмежте максимальну кількість спроб (зазвичай 3-5), щоб не перевантажувати сервер та батарею.
User feedback — при мережевій помилці покажіть зрозуміле повідомлення: «Немає з'єднання», «Сервер тимчасово недоступний», «Перевірте інтернет». Використовуйте Snackbar або Inline State View. Ніколи не показуйте технічні помилки (HTTP 500, SocketException) користувачу.
ConnectivityManager — Android API для моніторингу мережі. Дозвольте застосунку реагувати на зміни: при втраті мережі показувати placeholder, при відновленні — автоматично оновлювати дані. У iOS використовуйте NWPathMonitor з Network framework.
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) — стандартний crash-reporting інструмент для мобільних застосунків. Збирає stacktrace всіх необроблених винятків, версію ОС, модель пристрою та час падіння. Дозволяє групувати помилки та призначати відповідальних за фікси.
Sentry — альтернатива Crashlytics з підтримкою performance monitoring. Дозволяє трейсити конкретні транзакції (наприклад, «авторизація користувача») та бачити, на якому кроці сталася помилка. Performance tracing допомагає відрізняти мережеві таймаути від багів у логіці застосунку.
Timber — бібліотека логування для Android з автоматичним додаванням тегів за класом. У debug-збірці логуйте всі мережеві запити та відповіді. У release-збірці — лише помилки та попередження через Crashlytics.setCustomLog.
| Інструмент | Тип | Коли використовувати |
|---|---|---|
| Crashlytics | Crash reporting | Завжди в release — автоматичний збір крашів |
| Sentry | Crash + Performance | Коли потрібно профілювати конкретні користувацькі сценарії |
| Timber | Logging | Debug: повне логування; Release: лише помилки |
| HTTP Toolkit | Network debug | Локальне перехоплення та аналіз HTTP-трафіку |
За даними Firebase Summit 2023, застосунки, що впровадили Crashlytics + Performance Monitoring, скорочують середній час виявлення та виправлення критичних багів з 3 днів до 4 годин. Рекомендується налаштувати алерти на кожен crash з частотою >0.1% від активних користувачів.
Часті запитання
Якщо crash не ловиться в Crashlytics, перевірте native crash (SIGSEGV, SIGABRT) — вони не обробляються Java/Kotlin exception handler. В Android це може бути витік native memory з JNI, в iOS — EXC_BAD_ACCESS. Використовуйте Breakpad (Android) або PLCrashReporter (iOS) для збору native crash stacktrace.
Використовуйте Network Link Conditioner (вбудований в iOS, для Android є Facebook Network Connection Class або налаштування Developer Options > Network > Select network type). Встановіть затримку 500-3000 ms та втрату пакетів 5-30%. Також можна використовувати Charles Proxy або mitmproxy для емуляції мережевих затримок і розривів.
ANR виникає, якщо UI-потік заблокований довше 5 секунд. Мережеві запити мають виконуватися у фоновому потоці: корутини (viewModelScope.launch(Dispatchers.IO)), RxJava (subscribeOn(Schedulers.io)) або WorkManager для синхронізації. Завжди встановлюйте таймаути на HTTP-клієнті — відсутність таймауту може призвести до вічної блокування.
Race condition — ситуація, коли результат операції залежить від порядку виконання потоків. Наприклад, користувач швидко натискає кнопку «Надіслати» двічі, і запит іде двічі. Рішення: використовуйте Mutex, single-threaded executors або state machine (відключіть кнопку після першого натискання). В Kotlin використовуйте Mutex з корутин або @Synchronized анотацію.
Застосовуйте Chaos Engineering для мобільних застосунків: відключайте мережу під час операцій, симулюйте високу затримку, перемикайте між Wi-Fi та мобільною мережею, вбивайте процес системою. Інструменти: Facebook Network Connection Class, Charles Proxy, iOS Network Link Conditioner. В CI/CD додавайте UI-тести з різними мережевими умовами через AndroidTest Orchestrator.
Підсумки
Ми розробимо мобільний застосунок під ключ
IT Sectr створює застосунки для iOS та Android для стартапів і бізнесу з 2017 року. Ми проконсультуємо вас і запропонуємо найкраще рішення.
Читайте також