Реализация офлайн-режима работы мобильного приложения

TRUETECH занимается разработкой, поддержкой и обслуживанием мобильных приложений iOS, Android, PWA. Имеем большой опыт и экспертизу для публикации мобильных приложений в популярные маркеты Google Play, App Store, Amazon, AppGallery и другие.
Разработка и поддержка любых видов мобильных приложений:
Информационные и развлекательные мобильные приложения
Новостные приложения, игры, справочники, онлайн-каталоги, погодные, фитнес и здоровье, туристические, образовательные, социальные сети и мессенджеры, квиз, блоги и подкасты, форумы, агрегаторы
Мобильные приложения электронной коммерции
Интернет-магазины, B2B-приложения, маркетплейсы, онлайн-обменники, кэшбэк-сервисы, биржи, дропшиппинг-платформы, программы лояльности, доставка еды и товаров, платежные системы
Мобильные приложения для управления бизнес-процессами
CRM-системы, ERP-системы, управление проектами, инструменты для команды продаж, учет финансов, управление производством, логистика и доставка, управление персоналом, системы мониторинга данных
Мобильные приложения электронных услуг
Доски объявлений, онлайн-школы, онлайн-кинотеатры, платформы предоставления электронных услуг, платформы кешбека, видеохостинги, тематические порталы, платформы онлайн-бронирования и записи, платформы онлайн-торговли

Это лишь некоторые из типы мобильных приложений, с которыми мы работаем, и каждый из них может иметь свои специфические особенности и функциональность, а также быть адаптированным под конкретные потребности и цели клиента.

Предлагаемые услуги
Показано 1 из 1 услугВсе 1735 услуг
Реализация офлайн-режима работы мобильного приложения
Сложная
~3-5 рабочих дней
Часто задаваемые вопросы
Наши компетенции:
Этапы разработки
Последние работы
  • image_mobile-applications_feedme_467_0.webp
    Разработка мобильного приложения для компании FEEDME
    756
  • image_mobile-applications_xoomer_471_0.webp
    Разработка мобильного приложения для компании XOOMER
    624
  • image_mobile-applications_rhl_428_0.webp
    Разработка мобильного приложения для компании RHL
    1052
  • image_mobile-applications_zippy_411_0.webp
    Разработка мобильного приложения для компании ZIPPY
    947
  • image_mobile-applications_affhome_429_0.webp
    Разработка мобильного приложения для компании Affhome
    862
  • image_mobile-applications_flavors_409_0.webp
    Разработка мобильного приложения для компании FLAVORS
    445

Реализация офлайн-режима работы мобильного приложения

Офлайн-режим — не просто «показывать кэш когда нет сети». Это архитектурное решение, которое затрагивает все слои приложения: как хранить данные, как отображать актуальность, что разрешать делать пользователю без интернета, как ставить действия в очередь и как синхронизировать после восстановления связи.

Мобильный интернет рвётся в метро, в лифте, при плохом покрытии. Приложение, которое просто вешает спиннер и ждёт — теряет пользователей.

Архитектурный фундамент: 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
) {
    // UI подписан на этот Flow — получает данные сразу из базы
    fun observeArticles(categoryId: String): Flow<List<Article>> =
        localDao.observeByCategory(categoryId)
            .map { entities -> entities.map { it.toDomain() } }

    // Вызывается при старте, pull-to-refresh, восстановлении сети
    suspend fun refresh(categoryId: String) {
        try {
            val remote = api.getArticles(categoryId)
            localDao.upsertAll(remote.map { it.toEntity() })
        } catch (e: NetworkException) {
            // Не пробрасываем — UI просто видит старые данные
            syncManager.scheduleSyncWhenOnline(SyncTask.RefreshArticles(categoryId))
        }
    }
}

Определение состояния сети

На Android — ConnectivityManager с NetworkCallback:

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)
            .build()
        connectivityManager.registerNetworkCallback(request, callback)
        awaitClose { connectivityManager.unregisterNetworkCallback(callback) }
    }.stateIn(
        scope = CoroutineScope(Dispatchers.IO),
        started = SharingStarted.WhileSubscribed(5000),
        initialValue = connectivityManager.isCurrentlyConnected()
    )
}

NET_CAPABILITY_INTERNET не означает реального интернета — captive portal (WiFi в отеле без авторизации) проходит эту проверку. Для надёжности добавляем NET_CAPABILITY_VALIDATED.

На iOS — NWPathMonitor из Network framework:

let monitor = NWPathMonitor()
monitor.pathUpdateHandler = { path in
    let isConnected = path.status == .satisfied
    DispatchQueue.main.async {
        self.networkState = isConnected ? .online : .offline
    }
}
monitor.start(queue: DispatchQueue.global(qos: .background))

Offline-действия: очередь операций

Пользователь нажал «Отправить» без интернета. Нельзя просто выдать ошибку. Правильно — поставить действие в очередь:

@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"  // PENDING, PROCESSING, FAILED
)

При восстановлении сети — WorkManager обрабатывает очередь:

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" -> {
                        val order = Json.decodeFromString<CreateOrderRequest>(operation.payload)
                        api.createOrder(order)
                    }
                    "UPDATE_PROFILE" -> {
                        val update = Json.decodeFromString<UpdateProfileRequest>(operation.payload)
                        api.updateProfile(update)
                    }
                }
                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 с условием наличия сети
val syncRequest = OneTimeWorkRequestBuilder<OfflineSyncWorker>()
    .setConstraints(Constraints(requiredNetworkType = NetworkType.CONNECTED))
    .setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 15, TimeUnit.SECONDS)
    .build()

WorkManager на Android — правильный инструмент для отложенных операций. Переживает перезапуск приложения и устройства. Не используйте корутины напрямую для этой задачи — они живут только пока жив процесс.

На iOS — Background Tasks framework (BGTaskScheduler):

BGTaskScheduler.shared.register(forTaskWithIdentifier: "com.app.offline-sync",
    using: nil) { task in
    self.handleOfflineSync(task: task as! BGProcessingTask)
}

UX: что показывать пользователю

Банальный toast «Нет интернета» — плохо. Пользователю важно понимать:

  • Данные актуальны или устарели (и насколько)
  • Что он может делать оффлайн
  • Что встанет в очередь и выполнится позже

Показываем timestamp последней синхронизации в шапке экрана. Кнопка «Отправить» в офлайне — меняет текст на «Отправить при подключении» и меняет стиль. Pending-операции отображаются в UI как «ожидает синхронизации» до подтверждения с сервера.

Типичные проблемы

Optimistic update без rollback. Обновили UI сразу (оптимистично), операция в очереди — пользователь видит изменение. Сервер вернул ошибку — нужно откатить локальное изменение. Без механизма rollback UI показывает несуществующее состояние.

Конкурентные записи. Пользователь сделал изменения оффлайн, параллельно те же данные изменили на другом устройстве. Нужна стратегия конфликт-резолюции — это отдельная задача.

Большие объёмы данных. Не нужно кэшировать всё. Нужно кэшировать то, что пользователь с высокой вероятностью откроет: текущий экран, данные за последние N дней, избранное.

Реализация офлайн-режима с очередью операций, WorkManager и UX для двух платформ: 3–5 недель в зависимости от сложности доменной логики. Стоимость рассчитывается индивидуально.