Уявіть: кур'єрська служба використовує мобільний додаток для прийому замовлень. Сотні замовлень губляться при заїзді в тунель — синхронізація не встигає. Після відновлення мережі дані розходяться, клієнти не отримують посилки. Такі ситуації — наша спеціалізація. Ми розробляємо архітектуру офлайн-синхронізації мобільних додатків, яка гарантує цілісність даних навіть при нестабільному з'єднанні. Команда має понад 5 років досвіду та виконала понад 30 проєктів із синхронізацією для fintech, e-commerce та logistics.
Без правильної синхронізації офлайн-зміни губляться, виникають конфлікти, користувацький досвід страждає. Наш підхід — двостороння дельта-синхронізація з чергою операцій та обробкою конфліктів. Приклад із практики: для e-commerce з 500 000 товарів і 10 000 замовлень на день ми впровадили дельта-синхронізацію. Конфлікти виникали у 2% замовлень — вирішили через LWW. Середній час синхронізації скоротився з 30 до 2 секунд, трафік на 90%. Delta sync краща за full sync у 10 разів за швидкістю та в 50 разів за трафіком.
Як реалізувати двосторонню дельта-синхронізацію?
Дельта-синхронізація — ефективний спосіб мінімізувати трафік. На відміну від повної синхронізації (full sync), клієнт завантажує лише зміни з моменту останньої синхронізації. Це знижує навантаження на сервер на 90% і прискорює синхронізацію в 5–10 разів.
| Метод | Трафік | Час | Навантаження на сервер | Підходить для |
|---|---|---|---|---|
| Full sync | Високий | Повільно | Висока | Мало даних (<100 записів) |
| Delta sync | Низький | Швидко | Низька | Часті зміни, багато записів |
| Incremental sync | Середній | Середньо | Середня | Дані з монотонним ID |
Ми використовуємо delta sync як основний механізм. Клієнт надсилає lastSyncTimestamp, сервер повертає лише змінені та видалені записи.
Код SyncManager
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
SyncManager — центральний компонент. Він координує надсилання накопичених операцій та отримання дельти. Код нижче — основа для iOS та Android, з адаптацією під платформу.
Код SyncManager (kotlin)
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 }
.distinctUntilChanged()
.onEach { triggerImmediateSync() }
.launchIn(applicationScope)
}
fun onAppForeground() {
val lastSync = prefs.lastSyncTimestamp
val tooOld = System.currentTimeMillis() - lastSync > 5 * 60 * 1000L
if (tooOld) triggerImmediateSync()
}
}
На iOS аналог WorkManager — BGAppRefreshTask та BGProcessingTask (див. документацію Apple). Згідно з Apple Background Execution Guide, фонові завдання iOS обмежені 30 секундами. Наше рішення враховує ці обмеження та використовує оптимальні тригери.
Синхронізація зображень та файлів
Бінарні дані — окремо від метаданих. Синхронізуємо список файлів (імена, 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 файли з поточного батчу.
Порівняння стратегій вирішення конфліктів
| Стратегія | Принцип | Продуктивність | Складність |
|---|---|---|---|
| LWW (Last Writer Wins) | Перемагає остання зміна за серверним часом | Дуже швидко | Низька |
| CRDT | Автоматичне злиття без конфліктів | Середньо | Висока |
| Custom merge | Ручне вирішення через UI | Повільно | Висока |
Для більшості сценаріїв достатньо LWW. CRDT виправданий для колаборативних редакторів або фінансових даних, де не можна втратити жодну зміну (див. Wikipedia: Conflict-free replicated data type).
Стан синхронізації в UI
Користувач повинен бачити актуальність даних. Мінімум: timestamp останньої синхронізації. Краще: іконка статусу (synced / syncing / sync error) поруч із даними, які можуть бути застарілими.
При sync error — не блокувати UI. Показувати попередження, дозволяти роботу з локальними даними, пропонувати повторити.
Що входить у роботу
- Аудит поточної архітектури даних та підготовка специфікації
- Проєктування схеми синхронізації (сутності, ідентифікатори, конфлікти)
- Реалізація SyncManager, черги операцій та API-інтеграції
- Налаштування фонових завдань (WorkManager / BGAppRefreshTask)
- Розробка обробників конфліктів (LWW або CRDT)
- Покриття коду unit-тестами та сценаріїв інтеграційного тестування
- Надання документації API та посібника з розгортання
- Підтримка протягом 30 днів після запуску
Процес роботи
- Аналіз — вивчаємо модель даних, навантаження, вимоги до консистентності
- Проєктування — обираємо стратегію конфліктів, визначаємо поля для синхронізації
- Реалізація — пишемо код, інтегруємо з вашим бекендом (REST, GraphQL, Firebase)
- Тестування — емуляція офлайн-сценаріїв, навантажувальне тестування, перевірка edge cases
- Деплой — налаштування CI/CD, моніторинг, логування помилок синхронізації
Терміни та вартість
Повна реалізація двосторонньої дельта-синхронізації з чергою операцій та обробкою конфліктів: від 4 до 8 тижнів залежно від обсягу даних та кількості типів сутностей. Вартість розраховується індивідуально після аналізу проєкту. Впровадження дельта-синхронізації скорочує витрати на серверну інфраструктуру до 80% та прискорює вихід на ринок на 2 місяці.
Отримайте безкоштовну консультацію та оцінку вашого проєкту. Замовте розробку під ключ та забезпечте вашим користувачам безшовну роботу навіть без інтернету.
Наша команда сертифікованих iOS та Android розробників гарантує дотримання гайдлайнів App Store та Google Play. Досвід у проєктах від 50 000 до 10 млн користувачів.







