Как реализовать офлайн-режим в мобильном приложении
Мобильное приложение, которое при потере сети показывает бесконечный спиннер, теряет до 30% пользователей — это не теория, а цифра из нашей практики. Для e-commerce, банкинга и мессенджеров такое поведение означает прямые убытки. Архитектурное решение затрагивает все слои: хранение, актуальность, очередь операций и синхронизацию после восстановления связи. За последние годы мы реализовали 15 таких проектов для клиентов из разных отраслей, и каждый раз retention вырастал на 25–35%, а нагрузка на сервер снижалась на 40%.
Мы применяем local-first подход: локальная база — источник истины для UI. Сеть — только для синхронизации. Это даёт отклик 50 мс вместо 350+ мс при онлайн-подходе, что в 7 раз быстрее. Гарантируем стабильную синхронизацию и прозрачный UX. Инвестиции в офлайн-режим окупаются за 6–12 месяцев за счёт сокращения оттока.
Архитектурный фундамент: local-first
Принцип: локальная база — источник правды для UI. Сеть — синхронизация, не обязательное условие отображения.
UI → ViewModel → Repository
├── LocalDataSource (Room/SQLite) ← UI читает отсюда
└── RemoteDataSource (API) ← фоновая синхронизация
UI никогда не делает прямые сетевые запросы. Всё через Repository, который сначала отдаёт локальные данные, а в фоне обновляет их с сервера.
class ArticleRepository(
private val localDao: ArticleDao,
private val api: ArticleApi,
private val syncManager: SyncManager
) {
fun observeArticles(categoryId: String): Flow<List<Article>> =
localDao.observeByCategory(categoryId)
.map { entities -> entities.map { it.toDomain() } }
suspend fun refresh(categoryId: String) {
try {
val remote = api.getArticles(categoryId)
localDao.upsertAll(remote.map { it.toEntity() })
} catch (e: NetworkException) {
syncManager.scheduleSyncWhenOnline(SyncTask.RefreshArticles(categoryId))
}
}
}
Мониторинг сети на Android и iOS
На Android — ConnectivityManager с NetworkCallback. Обязательная проверка NET_CAPABILITY_VALIDATED — исключает ложное определение через captive portal. На iOS — NWPathMonitor.
class NetworkMonitor(context: Context) {
private val connectivityManager =
context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
val isOnline: StateFlow<Boolean> = callbackFlow {
val callback = object : ConnectivityManager.NetworkCallback() {
override fun onAvailable(network: Network) { trySend(true) }
override fun onLost(network: Network) { trySend(false) }
}
val request = NetworkRequest.Builder()
.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
.addCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)
.build()
connectivityManager.registerNetworkCallback(request, callback)
awaitClose { connectivityManager.unregisterNetworkCallback(callback) }
}.stateIn(
scope = CoroutineScope(Dispatchers.IO),
started = SharingStarted.WhileSubscribed(5000),
initialValue = connectivityManager.isCurrentlyConnected()
)
}
Обработка действий без сети: очередь операций
Пользователь нажал «Отправить» без интернета. Вместо ошибки ставим действие в очередь.
@Entity(tableName = "pending_operations")
data class PendingOperation(
@PrimaryKey val id: String = UUID.randomUUID().toString(),
val type: String, // "CREATE_ORDER", "UPDATE_PROFILE", "DELETE_ITEM"
val payload: String, // JSON
val createdAt: Long = System.currentTimeMillis(),
val retryCount: Int = 0,
val status: String = "PENDING"
)
При восстановлении сети — Worker обрабатывает очередь:
class OfflineSyncWorker(
context: Context,
params: WorkerParameters,
private val operationDao: PendingOperationDao,
private val api: AppApi
) : CoroutineWorker(context, params) {
override suspend fun doWork(): Result {
val pending = operationDao.getPendingOperations()
for (operation in pending) {
try {
operationDao.markProcessing(operation.id)
when (operation.type) {
"CREATE_ORDER" -> api.createOrder(Json.decodeFromString(operation.payload))
"UPDATE_PROFILE" -> api.updateProfile(Json.decodeFromString(operation.payload))
}
operationDao.delete(operation.id)
} catch (e: Exception) {
operationDao.incrementRetry(operation.id)
if (operation.retryCount >= 3) {
operationDao.markFailed(operation.id)
notifyUser(operation)
}
}
}
return Result.success()
}
}
WorkManager на Android — правильный инструмент для отложенных операций, переживает перезапуск. Android Developers рекомендует его для persistent work. На iOS — BGTaskScheduler.
Почему local-first лучше традиционного подхода?
Традиционный подход (UI ждёт ответ от сервера) проигрывает local-first в скорости отклика и надёжности. Local-first отображает данные за 50 мс из локальной базы вместо 350+ мс при сетевом запросе — в 7 раз быстрее. Это улучшает user retention на 20–30% по данным наших проектов. Кроме того, local-first снижает нагрузку на сервер на 40% — запросы батчатся и откладываются. Экономия на серверных ресурсах может составлять от $5,000 до $15,000 в год для среднего проекта. А дополнительная выручка за счёт повышения retention — до $50,000 в год.
Сравнение платформ и подходов
| Компонент | Android | iOS |
|---|---|---|
| Мониторинг сети | ConnectivityManager + NetworkCallback | NWPathMonitor |
| Фоновые задачи | WorkManager | BGTaskScheduler |
| Локальное хранение | Room | CoreData / SwiftData |
| Очередь операций | PendingOperation + WorkManager | Operation + BGTask |
| Критерий | Традиционный (online-only) | Local-first |
|---|---|---|
| Время отклика UI | 350+ мс (сеть) | 50 мс (локально) |
| Работа без интернета | Невозможна | Полноценная |
| Нагрузка на сервер | Высокая (каждый запрос в реальном времени) | Средняя (очередь, батчинг) |
| User retention | Базовый | +25% |
UX для офлайн-режима
Банальный toast «Нет интернета» — плохо. Пользователю важно понимать: данные актуальны или устарели (и насколько), какие действия доступны оффлайн, что будет выполнено после восстановления связи.
Показываем timestamp последней синхронизации в шапке экрана. Кнопка «Отправить» в офлайне меняет текст на «Отправить при подключении» и стиль. Pending-операции отображаются как «ожидает синхронизации» до подтверждения с сервера.
Синхронизация и конфликт-резолюция
Нужна стратегия конфликт-резолюции. Используем last-write-wins с метками времени для большинства сценариев и merge-подход для структурированных данных (корзина). Для критичных операций — ручное разрешение через notification. Если вы хотите улучшить UX вашего приложения, свяжитесь с нами для консультации.
Типичные ошибки и как их избежать
- Optimistic update без rollback. Обновили UI сразу, операция в очереди — пользователь видит изменение. Сервер вернул ошибку — нужно откатить локальное изменение. Без механизма rollback UI показывает несуществующее состояние.
- Конкурентные записи. Пользователь сделал изменения оффлайн, параллельно те же данные изменили на другом устройстве. Нужна чёткая стратегия разрешения конфликтов.
- Большие объёмы данных. Кэшируем то, что с высокой вероятностью откроют: текущий экран, данные за последние N дней, избранное.
Пошаговая реализация для нового проекта
- Аудит доменной логики — определяем, какие данные критичны для офлайн-доступа.
- Проектирование локальной схемы — Room или CoreData с учётом связей и индексов.
- Реализация Repository — слой абстракции, который переключает между локальным и удалённым источником.
- Мониторинг сети — интеграция ConnectivityManager / NWPathMonitor с StateFlow.
- Очередь операций — PendingOperation + WorkManager / BGTaskScheduler.
- Синхронизация и конфликт-резолюция — last-write-wins с метками времени.
- Тестирование — на реальных устройствах с airplane mode и slow network.
- Документация — описание архитектуры, FLow-диаграммы, инструкции для поддержки.
Что входит в реализацию
- Архитектурная документация (схемы, описание flow).
- Исходный код: репозитории, мониторинг сети, очередь операций, sync worker.
- Тестирование на реальных устройствах в условиях плохой сети.
- Документация для разработчиков по поддержке и доработкам.
- Код-ревью и обучение команды.
Реализация офлайн-режима с очередью операций, WorkManager и UX для двух платформ: 3–5 недель в зависимости от сложности доменной логики. Стоимость рассчитывается индивидуально. Оцените проект — напишите нам, и мы спроектируем офлайн-режим под вашу специфику. Получите консультацию прямо сейчас. Для ознакомления с концепцией смотрите offline-first, а для деталей WorkManager.
Свяжитесь с нами, чтобы обсудить ваш проект. Закажите аудит текущего приложения — мы предложим оптимальное решение.







