Реалізація синхронізації офлайн-даних з сервером

TRUETECH займається розробкою, підтримкою та обслуговуванням мобільних додатків iOS, Android, PWA. Маємо великий досвід та експертизу для публікації мобільних додатків до популярних маркетів Google Play, App Store, Amazon, AppGallery та інші.

Розробка та підтримка будь-яких видів мобільних додатків:

Інформаційні та розважальні мобільні програми
Новинки, ігри, довідники, онлайн-каталоги, погодні, фітнес та здоров'я, туристичні, освітні, соціальні мережі та месенджери, квіз, блоги та подкасти, форуми, агрегатори
Мобільні програми електронної комерції
Інтернет-магазини, B2B-додатки, маркетплейси, онлайн-обмінники, кешбек-сервіси, біржі, дропшиппінг-платформи, програми лояльності, доставка їжі та товарів, платіжні системи
Мобільні програми для управління бізнес-процесами
CRM-системи, ERP-системи, управління проектами, інструменти для команди продажів, облік фінансів, управління виробництвом, логістика та доставка, управління персоналом, системи моніторингу даних
Мобільні програми електронних послуг
Дошки оголошень, онлайн-школи, онлайн-кінотеатри, платформи надання електронних послуг, платформи кешбеку, відеохостинги, тематичні портали, платформи онлайн-бронювання та запису, платформи онлайн-торгівлі

Це лише деякі з типів мобільних додатків, з якими ми працюємо, і кожен із них може мати свої специфічні особливості та функціональність, а також бути адаптованим під конкретні потреби та цілі клієнта.

Послуги, які ми пропонуємо
Показано 1 з 1Усі 1735 послуг
Реалізація синхронізації офлайн-даних з сервером
Складний
від 1 тижня до 3 місяців
Часті запитання

Наші компетенції:

Етапи розробки

Останні роботи

  • image_mobile-applications_feedme_467_0.webp
    Розробка мобільного додатка для компанії FEEDME
    792
  • image_mobile-applications_xoomer_471_0.webp
    Розробка мобільного додатку для компанії XOOMER
    671
  • image_mobile-applications_rhl_428_0.webp
    Розробка мобільного додатку для компанії RHL
    1097
  • image_mobile-applications_zippy_411_0.webp
    Розробка мобільного додатку для компанії ZIPPY
    969
  • image_mobile-applications_affhome_429_0.webp
    Розробка мобільного додатку для компанії Affhome
    914
  • image_mobile-applications_flavors_409_0.webp
    Розробка мобільного додатку для компанії FLAVORS
    495

Реалізація синхронізації офлайн-даних з сервером

Синхронізація — це не просто «завантажити дані при старті». Це двусторонній процес: клієнт накопичує зміни офлайн, сервер накопичує зміни від інших клієнтів, при відновленні зв'язку обидва мають прийти до узгодженого стану. Правильна архітектура синхронізації — одна з найбільш технічно складних задач у мобільній розробці.

Delta sync vs Full sync

Найочевидніша реалізація — при відновленні мережи запросити всі дані заново. Працює на малих обсягах. При 10,000 записах кожен раз скачувати повний список — це трафік, час та навантаження на сервер.

Delta sync — клієнт присилає lastSyncTimestamp, сервер повертає тільки змінені та видалені записи з того моменту.

data class SyncRequest(
    val lastSyncTimestamp: Long,
    val clientId: String
)

data class SyncResponse(
    val serverTimestamp: Long,       // час відповіді сервера
    val updated: List<ProductDto>,   // змінені чи нові
    val deletedIds: List<String>     // видалені на сервері
)

На клієнті зберігаємо lastSuccessfulSyncTimestamp в MMKV або SharedPreferences. При наступній синхронізації використовуємо як фільтр.

Важливо: час має бути серверним. Якщо клієнт використовує своє час, розбіжність часів дає пропуски. Сервер видає свій timestamp в відповіді — клієнт зберігає саме його.

Архітектура SyncManager

class SyncManager(
    private val api: SyncApi,
    private val dao: ProductDao,
    private val pendingOpsDao: PendingOperationDao,
    private val prefs: SyncPreferences
) {
    suspend fun sync(): SyncResult {
        // 1. Відправляємо накопичені offline-операції
        val pending = pendingOpsDao.getAll()
        if (pending.isNotEmpty()) {
            try {
                val uploadResult = api.uploadOperations(pending.map { it.toRequest() })
                // Видаляємо тільки успішно обробляються
                pendingOpsDao.deleteByIds(uploadResult.processedIds)
                // Невдалі залишаються в очереди
            } catch (e: NetworkException) {
                return SyncResult.NetworkError
            }
        }

        // 2. Скачуємо зміни з сервера
        return try {
            val response = api.sync(
                SyncRequest(
                    lastSyncTimestamp = prefs.lastSyncTimestamp,
                    clientId = prefs.clientId
                )
            )

            dao.applyDelta(
                updated = response.updated.map { it.toEntity() },
                deletedIds = response.deletedIds
            )

            prefs.lastSyncTimestamp = response.serverTimestamp
            SyncResult.Success(
                updatedCount = response.updated.size,
                deletedCount = response.deletedIds.size
            )
        } catch (e: Exception) {
            SyncResult.Error(e)
        }
    }
}

applyDelta в транзакції — атомарно. Або застосовуємо все, або нічого:

@Transaction
suspend fun applyDelta(updated: List<ProductEntity>, deletedIds: List<String>) {
    upsertAll(updated)
    softDeleteByIds(deletedIds, System.currentTimeMillis())
}

Soft delete обов'язковий: не видаляємо фізично, ставимо флаг is_deleted = true та зберігаємо timestamp. Інакше при наступному delta sync ми знову «забудемо» про це видалення.

Триггери синхронізації

Запускаємо синхронізацію в кількох сценаріях:

class SyncScheduler(
    private val workManager: WorkManager,
    private val syncManager: SyncManager,
    private val networkMonitor: NetworkMonitor
) {
    init {
        // Періодична фонова синхронізація
        val periodicSync = PeriodicWorkRequestBuilder<SyncWorker>(15, TimeUnit.MINUTES)
            .setConstraints(Constraints(requiredNetworkType = NetworkType.CONNECTED))
            .build()
        workManager.enqueueUniquePeriodicWork(
            "periodic-sync",
            ExistingPeriodicWorkPolicy.KEEP,
            periodicSync
        )
    }

    // При відновленні мережи — немедленна синхронізація
    fun observeNetworkAndSync() {
        networkMonitor.isOnline
            .filter { it } // тільки перехід offline→online
            .distinctUntilChanged()
            .onEach { triggerImmediateSync() }
            .launchIn(applicationScope)
    }

    // При повертанні в foreground
    fun onAppForeground() {
        val lastSync = prefs.lastSyncTimestamp
        val tooOld = System.currentTimeMillis() - lastSync > 5 * 60 * 1000L
        if (tooOld) triggerImmediateSync()
    }
}

На iOS аналог WorkManager — BGAppRefreshTask та BGProcessingTask. Фонові задачі iOS виконуються за розсудом ОС та обмежені за часом (30 секунд для appRefresh, до 3 хвилин для processing).

Синхронізація зображень та файлів

Бінарні дані — окремо від метаданих. Синхронізуємо список файлів (імена, URL, хеші), скачуємо файли по окремих запитах з пріоритизацією:

class MediaSyncManager {
    suspend fun syncMedia(mediaList: List<MediaMeta>) {
        val toDownload = mediaList.filter { meta ->
            !fileCache.exists(meta.localPath) ||
            fileCache.getHash(meta.localPath) != meta.serverHash
        }

        // Скачуємо паралельно, але обмежуємо конкурентність
        toDownload.chunked(4).forEach { batch ->
            batch.map { meta ->
                async { downloadFile(meta) }
            }.awaitAll()
        }
    }
}

Чанки по 4 — не перегружаємо з'єднання, при втраті зв'язку втрачаємо максимум 4 файли з поточного батчу.

Стан синхронізації в UI

Користувач має видіти актуальність даних. Мінімум: timestamp останньої синхронізації. Краще: іконка статусу (synced / syncing / sync error) поруч з даними, які можуть бути застарілими.

При sync error — не блокуємо UI. Показуємо попередження, дозволяємо роботу з локальними даними, пропонуємо повторити.

Повна реалізація двусторонньої дельта-синхронізації з очередою операцій та обробкою конфліктів: 4–8 тижнів залежно від обсягу даних та кількості типів сутностей. Вартість розраховується індивідуально.