Перейти к содержанию
Innovialabs

Offline-first приложения: локальное хранение и синхронизация данных

Продвинутое4 мин

Коротко

В offline-first приложении интерфейс читает и пишет только в локальную базу, а синхронизация с сервером идёт в фоне через очередь изменений. Сложность не в хранении, а в разрешении конфликтов и повторной отправке без дублей.

Что значит offline-first

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

Такой подход нужен, когда люди работают при плохой связи: курьеры, торговые представители, полевые сотрудники, склады, приложения для заметок и задач. Для каталога, который просто показывает данные с сервера, обычно достаточно кеша.

Локальная база: что выбрать

РешениеПлатформаКогда подходит
SQLiteВездеПолный контроль над схемой и запросами; через обёртки вроде GRDB, SQLDelight, drift, sqflite
RoomAndroidСтандарт для Android: SQLite с проверкой запросов при компиляции и миграциями
Core Data / SwiftDataiOSНативный стек Apple, интеграция с SwiftUI и CloudKit
RealmiOS, Android, Flutter, React NativeОбъектная модель без SQL. MongoDB объявила о прекращении поддержки Atlas Device Sync, поэтому синхронизацию для Realm придётся строить самим

Во Flutter часто берут drift (на базе SQLite), в React Native — SQLite-обёртки или WatermelonDB.

Независимо от базы:

  • планируйте миграции схемы с первого релиза — у пользователей будут старые версии данных;
  • храните у каждой записи серверный ID, локальный ID, время изменения и статус синхронизации;
  • удаление делайте мягким (tombstone), иначе сервер не узнает, что запись удалена офлайн.

Стратегии синхронизации

Получение изменений (pull):

  • Дельта-синхронизация: клиент передаёт курсор или метку последней синхронизации, сервер отдаёт только изменения после неё. Основной вариант.
  • Полная перезагрузка: для маленьких справочников или как запасной путь при повреждении данных.
  • Push-уведомление как сигнал: сервер сообщает «есть изменения», приложение делает pull.

Отправка изменений (push) — через очередь исходящих изменений (outbox): каждое изменение сначала сохраняется в базу вместе с записью в очереди, а фоновый обработчик отправляет очередь по порядку.

CREATE TABLE outbox (
  id TEXT PRIMARY KEY,        -- ключ идемпотентности (UUID)
  entity TEXT NOT NULL,
  operation TEXT NOT NULL,    -- create | update | delete
  payload TEXT NOT NULL,      -- JSON
  attempts INTEGER NOT NULL DEFAULT 0,
  created_at INTEGER NOT NULL
);

Очередь запросов без дублей

  • Запись в данные и в outbox — в одной транзакции.
  • Каждый запрос несёт ключ идемпотентности: если ответ потерялся и клиент отправил повторно, сервер не создаст дубль.
  • Повторы с экспоненциальной задержкой и лимитом попыток; после лимита — показать пользователю, что изменение не отправлено.
  • Фоновая отправка через WorkManager на Android и BGTaskScheduler на iOS; система сама решает, когда запустить задачу, поэтому отправляйте очередь и при открытии приложения.
  • Учитывайте зависимости: заказ нельзя отправить раньше клиента, созданного офлайн. Отправляйте по порядку или передавайте локальные ID и сопоставляйте их на сервере.

Разрешение конфликтов

Конфликт — когда одну запись изменили на двух устройствах до синхронизации.

СтратегияКак работаетРиск
Last-write-winsПобеждает последнее изменение по времениТихая потеря данных; часы устройств могут врать
Версии записейКлиент отправляет версию, сервер отклоняет устаревшуюНужно решить, что делать после отказа
Слияние по полямИзменённые поля объединяются, конфликт только при правке одного поляСложнее реализовать
CRDTСтруктуры данных, которые сливаются без конфликтовСложность, подходит для совместного редактирования
Решает пользовательПоказываем обе версииПодходит для важных документов, утомляет при частых конфликтах

Для большинства бизнес-приложений рабочая схема — версии записей плюс слияние по полям, а last-write-wins только для неважных данных. Время для сравнения лучше брать с сервера.

Как тестировать плохую сеть

  • Network Link Conditioner на iOS и настройки сети в эмуляторе Android для медленной связи и потерь пакетов.
  • Прокси вроде Charles или Proxyman для ограничения скорости и подмены ответов.
  • Ручные сценарии: авиарежим посреди отправки, закрытие приложения с непустой очередью, два устройства правят одну запись, обновление приложения с несинхронизированными данными.
  • Автотесты слоя синхронизации с подменой сети, которая отвечает ошибкой, таймаутом или дублирует ответ.

Частые вопросы

Нет. Это заметно усложняет разработку и бэкенд. Делайте так, когда работа без сети — реальный сценарий пользователей, а в остальных случаях достаточно кеша и понятных сообщений об ошибке.

Поможем с этим

Если хотите не разбираться самим, а получить результат — этим мы занимаемся каждый день.

Обсудить проект

Читайте также

Есть идея или задача? Расскажите нам

Ответим в течение рабочего дня и предложим решение. Консультация бесплатная.