Урок по треку Архитектура мобильных приложений 8 мин

Офлайн-режим и синхронизация данных

Offline-first, единый источник правды, очередь изменений, конфликты, идемпотентность и фоновая синхронизация.

Мобильная сеть ненадёжна: метро, лифт, роуминг, переключение Wi-Fi. Поэтому «как приложение работает без сети» это один из главных вопросов мобильного собеса, особенно в system design.

Единый источник правды

В offline-first приложении источник правды для UI это локальная база (Room, Core Data или SwiftData, SQLite, Drift, Realm). Экран подписан на базу, а сеть только обновляет её:

сеть → репозиторий → база → (подписка) → view model → UI

Поведение одинаково с сетью и без, нет гонок «что показать: кеш или ответ».

Чтение

  1. Показать то, что есть в базе, сразу.
  2. Параллельно запросить изменения с сервера.
  3. Записать их в базу, UI обновится сам.

Для больших наборов нужна дельта-синхронизация: клиент передаёт курсор последней синхронизации, сервер отдаёт изменённые записи и удаления. Удаления передаются «надгробиями» (tombstones): иначе клиент, который был офлайн во время удаления, не узнает о нём.

Запись: outbox

пользователь → база (сразу) + операция в outbox (в одной транзакции)
синхронизатор → отправка операций по порядку → удаление из outbox после подтверждения
  • Изменение видно сразу (оптимистичный UI).
  • Очередь на диске переживает выгрузку процесса и перезагрузку.
  • У каждой операции ключ идемпотентности: если ответ потерялся и запрос повторился, сервер не создаст дубль.
  • Ошибки повторяются с экспоненциальной задержкой; «вечные» ошибки (валидация) показываются пользователю.

Когда синхронизировать

Система сама решает, когда запускать фоновую работу: WorkManager на Android, BGTaskScheduler и фоновые URLSession на iOS. Точного времени нет. Поэтому синхронизация запускается также:

  • при старте приложения и возврате на передний план;
  • при появлении сети;
  • по тихому push от сервера.

Постоянный фоновый поток система убьёт.

Конфликты

Два устройства изменили одну запись офлайн. Стратегии:

  • Last write wins по объекту или по полям: просто, но время устройства ненадёжно, а перезапись объекта теряет параллельные изменения разных полей.
  • Версии и оптимистичная блокировка: клиент отправляет версию, на основе которой менял; если на сервере новее, ответ 409, клиент скачивает актуальное и сливает.
  • Слияние по полям: менялись разные поля, конфликта нет.
  • Сервер всегда прав: для денег и остатков.
  • Ручное разрешение: показать обе версии.
  • CRDT (Automerge, Yjs) для совместного редактирования текста и списков: изменения сходятся при любом порядке применения.

Выбор по цене потери изменения: настройки переживут LWW, текст заметки нет.

Статус для пользователя

Пометки «отправляется», «не отправлено, повторить», индикатор офлайна, время последней синхронизации. Как часики у неотправленного сообщения в мессенджере.

Подводные камни

  • Миграция схемы базы при неотправленных операциях в outbox.
  • Первая синхронизация большого аккаунта: постранично, сначала нужное сейчас.
  • Отставшие дольше срока хранения надгробий клиенты: полная пересинхронизация.
  • Выход из аккаунта: что делать с неотправленными изменениями (предупредить).

Частые ошибки

  • Очередь отправки в памяти.
  • Параллельные синхронизаторы, отправляющие одно и то же.
  • Сравнение времени устройства с серверным.
  • UI, который показывает то кеш, то ответ сервера, и мигает.

Что спросят на собесе

  • Что такое offline-first и единый источник правды?
  • Как надёжно доставить изменения, сделанные без сети?
  • Что такое идемпотентность и зачем ключ на клиенте?
  • Как разрешать конфликты между устройствами?
  • Как синхронизировать большой набор данных эффективно?

Закрепите за 5 минут

После урока в тренажёре 3-5 проверочных вопросов, дальше модуль уходит в повторения.