Офлайн-режим и синхронизация в мобильных приложениях: как сделать правильно

Зачем приложению вообще работать офлайн
В идеальном мире у пользователя всегда есть стабильный интернет. На практике сотрудник заходит в подвал склада, торговый агент едет между районами, монтажник работает в новостройке без покрытия, а курьер спускается в подземный паркинг. Если приложение в эти моменты превращается в белый экран с крутящимся индикатором — бизнес теряет данные и доверие людей.
Офлайн-режим — это не про "кэшировать пару экранов". Это про то, чтобы пользователь мог продолжать работать так, как будто связи нет вовсе: создавать записи, заполнять формы, отмечать выполненные задачи. А приложение само, тихо и надёжно, доставит всё на сервер, как только появится сеть. Для B2B-сценариев это часто не приятная опция, а обязательное требование: без него инструмент просто не используют в поле.
В реалиях Узбекистана это особенно заметно. Покрытие 4G в Ташкенте хорошее, но за пределами крупных городов связь неровная: между Самаркандом и кишлаками, на трассах, в горных районах Кашкадарьи или Сурхандарьи интернет то есть, то нет. Приложение, которое предполагает постоянный онлайн, в таких условиях бесполезно ровно там, где оно нужнее всего.
Что значит "офлайн-first" на самом деле
Есть две принципиально разные архитектуры. В первой приложение — это тонкий клиент: каждое действие сразу летит на сервер, и без ответа ничего не происходит. Во второй — офлайн-first: источником правды для интерфейса выступает локальная база данных на устройстве (SQLite, Realm, WatermelonDB и т. п.), а сервер синхронизируется с ней в фоне.
Ключевой выбор на старте проекта: делать ли приложение офлайн-first. Это решение влияет на всю архитектуру — структуру данных, API, модель синхронизации. Дёшево заложить его в начале и очень дорого переделывать потом. Если хотя бы часть пользователей работает в полях — закладывайте офлайн-first сразу.
В офлайн-first модели интерфейс всегда отзывчив: данные читаются и пишутся локально мгновенно, без ожидания сети. Это даёт и побочный приятный эффект — приложение кажется быстрым даже при хорошем интернете, потому что не ждёт round-trip до сервера на каждое нажатие.
Очередь операций: сердце синхронизации
Когда пользователь что-то делает офлайн, действие нельзя просто "запомнить экран". Нужно сохранить намерение — операцию: "создать клиента", "изменить статус заявки на выполнено", "добавить фото к заказу". Эти операции складываются в локальную очередь и помечаются как несинхронизированные.
Как только сеть появляется, фоновый процесс начинает отправлять операции по порядку. Здесь важны несколько вещей:
- Идемпотентность. Каждой операции присваивается уникальный идентификатор (UUID), сгенерированный на устройстве. Если ответ сервера потерялся, но операция на самом деле прошла, повторная отправка не создаст дубль — сервер узнаёт операцию по её ID.
- Порядок и зависимости. Нельзя отправить "добавить товар в заказ" раньше, чем создан сам заказ. Очередь должна уважать причинно-следственные связи.
- Ретраи с backoff. При сбое сети операция не отбрасывается, а откладывается с нарастающей паузой, чтобы не долбить сервер впустую.
- Устойчивость к перезапуску. Очередь живёт в постоянном хранилище. Если пользователь закрыл приложение или телефон разрядился — после включения синхронизация продолжится с того же места.
Частая ошибка: хранить очередь только в оперативной памяти. Приложение убивают системой при нехватке RAM, телефон перезагружается — и несохранённые операции исчезают вместе с работой сотрудника за полдня. Очередь обязана быть персистентной с первого дня.
Конфликты: что делать, когда правки расходятся
Самая сложная часть синхронизации — конфликты. Два сотрудника отредактировали одну заявку офлайн, потом оба вышли в сеть. Чьи изменения победят? Универсального правильного ответа нет, есть стратегии, и выбирать их надо осознанно под бизнес-логику.
Last-write-wins (последний победил): побеждает запись с более поздней меткой времени. Просто в реализации, но молча затирает чужую работу. Годится для некритичных полей вроде заметок.
Слияние по полям (field-level merge): если один менял адрес, а другой — телефон, объединяем оба изменения. Сложнее, но сохраняет максимум данных. Подходит для карточек клиентов и заказов.
Ручное разрешение: при настоящем конфликте показываем пользователю обе версии и просим выбрать. Дороже в UX, но обязательно там, где цена ошибки высока — финансы, медицина, договоры.
На практике хороший подход — комбинировать: большинство полей мержить автоматически, а по-настоящему спорные случаи эскалировать человеку. Важно вести версионирование записей (например, через номер ревизии или векторные часы), чтобы сервер понимал, на основе какой версии данных была сделана офлайн-правка.
Реальные кейсы из практики
Где офлайн-режим окупается сразу:
- Выездные сервисные бригады. Монтаж, ремонт, обслуживание оборудования. Мастер заполняет акт выполненных работ, прикладывает фото, ставит подпись клиента — всё это без сети в подвале или на стройке, а синхронизируется по дороге обратно.
- Торговые агенты и мерчандайзеры. Обход точек, приём заказов, сверка остатков на полках. Маршрут проходит через зоны без покрытия, но работа не останавливается ни на секунду.
- Логистика и доставка. Курьер отмечает статусы, фиксирует получателя, сканирует штрихкоды в подземных паркингах и на складах с толстыми стенами.
- Полевые опросы и инспекции. Аграрные обследования, проверки объектов, сбор данных в районах — там, где стабильного интернета не было и не будет.
- Складские операции. Инвентаризация в металлических ангарах, где Wi-Fi и сотовая связь почти не пробиваются.
Как мы это реализуем
Технически современный офлайн-стек выглядит так: локальная БД на устройстве как единственный источник правды для UI; слой репозиториев, который скрывает от интерфейса, откуда пришли данные; журнал изменений (changelog) с операциями и их статусами; фоновый синхронизатор, который запускается по событию появления сети и по расписанию.
На стороне сервера нужен API, спроектированный под синхронизацию: эндпоинты принимают пакеты операций с клиентскими ID, отдают дельту изменений с момента последней синхронизации (по курсору или временной метке), корректно сообщают о конфликтах. Полная перекачка всех данных при каждом запуске недопустима — синхронизировать нужно только то, что изменилось.
Отдельное внимание — индикации состояния. Пользователь должен видеть, что данные ещё не отправлены ("ожидает синхронизации"), что синхронизация идёт, и что всё ушло на сервер. Без этой обратной связи люди не доверяют приложению и начинают дублировать работу на бумаге.
Вывод
Офлайн-режим и синхронизация — это не дополнительная фича, которую можно "прикрутить потом", а архитектурное решение, влияющее на весь проект. Грамотно спроектированная очередь операций, продуманная стратегия конфликтов и честная индикация состояния превращают мобильное приложение в надёжный рабочий инструмент, который не подводит в самый ответственный момент. В условиях неровного покрытия в регионах Узбекистана это часто определяет, будут вообще пользоваться вашим продуктом или нет. Если вы планируете приложение для выездных команд, логистики или полевых работ — давайте обсудим ваш проект: в OneDev мы поможем заложить офлайн-first архитектуру с самого начала, чтобы потом не переделывать всё с нуля.
Любое приложение нужно делать офлайн-first?
Чем офлайн-режим отличается от обычного кэширования?
Что произойдёт с данными, если телефон выключится во время работы офлайн?
Как избежать дублей при повторной отправке операций?
Как решается, чьи изменения важнее при конфликте?
Насколько офлайн-режим удорожает разработку?
Нужна похожая система или хотите обсудить проект?
Опишите задачу — предложим архитектуру, технический подход и план работ. Часто для старта достаточно короткого звонка.
Обсудить проект