Мобильная разработка
REST, GraphQL и Offline-first архитектура
В метро на втором уровне сеть пропадает каждые две минуты. Пользователь в Telegram пишет сообщение, нажимает 'Отправить', и сообщение мгновенно появляется в чате с серым кружком (отправляется). Через минуту, когда сеть появится на станции, кружок становится зелёной галочкой. Без хорошей offline-first архитектуры пользователь увидел бы ошибку, потерял текст и закрыл приложение. С правильной - даже не заметил, что был офлайн. Эта разница - между приложением, которое люди используют, и тем, которое они удаляют.
- **Notion mobile** - канонический offline-first; редактирование документов работает без сети, синхронизация в фоне
- **Linear** - mobile-приложение полностью synchronizes через WebSocket + outbox; ощущается мгновенным даже на медленной сети
- **Figma** - real-time collaborative editing через CRDT (Yjs-подобный custom CRDT) для одновременной работы команды
Кэширование: REST vs GraphQL
Мобильный пользователь открывает Instagram в метро. Сеть нестабильна, latency 800ms, пакеты теряются. Без кэша приложение показывает белый экран и пользователь его закрывает. С правильным кэшем - приложение мгновенно показывает последние посты из предыдущей сессии, фоном пытается обновить ленту, и пользователь спокойно листает. Разница - в архитектуре сетевого слоя.
REST использует HTTP-кэш на уровне URL: ответ от GET /users/42 кэшируется по URL + заголовкам, обновление одного поля профиля инвалидирует весь объект. GraphQL кэширует на уровне entities (через __typename + id): запрос меняет имя пользователя - кэш обновляет именно user.name везде, где он используется. Apollo Client и Relay делают это автоматически через normalized cache. Для REST аналог - RTK Query или TanStack Query, но требует ручной настройки invalidation.
Какое ключевое преимущество normalized cache (Apollo, Relay) над URL-based кэшированием REST?
Offline-first: outbox pattern и фоновая синхронизация
Notion mobile - канонический пример offline-first. Пользователь редактирует страницу в самолёте, изменения сохраняются мгновенно. Когда соединение появляется, изменения автоматически уходят на сервер. Технически это - **outbox pattern**: каждое изменение сначала записывается в локальную SQLite-таблицу operations с полем status='pending', UI обновляется на основе локального состояния (read-your-writes), фоновый воркер периодически отправляет pending operations и помечает их status='sent'.
Тонкости реализации: операции должны быть идемпотентными (повторная отправка не ломает данные) и иметь уникальный client_op_id для дедупликации на сервере. При обрыве соединения посередине отправки - повторяем, сервер возвращает 200 на уже применённую операцию благодаря client_op_id. SQLite-таблица operations - persistent across app restarts, что критично: пользователь может закрыть приложение и открыть через час, операции должны дойти.
Зачем в outbox pattern генерировать client_op_id на клиенте, а не получать его от сервера?
Оптимистичный UI и rollback
Лайк в Instagram. Пользователь нажимает сердечко, и оно сразу окрашивается в красный - до того, как сервер подтвердил запрос. Это **оптимистичный UI**: клиент предполагает, что операция пройдёт успешно, и обновляет состояние немедленно. Если сервер ответит ошибкой (например, пользователь забанен) - клиент откатывает изменение. Преимущество: UI ощущается мгновенным, особенно при latency 500ms+. Сложность: нужно правильно откатывать состояние, если что-то пошло не так.
Не каждая операция должна быть оптимистичной. Правило: оптимизм работает для операций с очень высокой вероятностью успеха (>95%). Для финансовых транзакций, оплат, удаления данных - наоборот, лучше показать спиннер и дождаться подтверждения. Для UX-операций (лайки, комментарии, реакции) - оптимизм обязателен. Apollo и React Query предоставляют API для оптимистичных обновлений с автоматическим rollback при ошибке.
Когда оптимистичный UI становится антипаттерном?
Разрешение конфликтов: LWW, CRDT, manual merge
Два пользователя одновременно редактируют одну и ту же страницу Notion. Алиса меняет заголовок на 'Plan A', Боб - на 'Plan B'. Кто выиграет? Это - **проблема разрешения конфликтов**. Три классических подхода: Last-Write-Wins (LWW, кто последний - тот прав, простой но теряет данные), Manual Merge (попросить пользователя выбрать, как в Git), CRDT (Conflict-free Replicated Data Types - математически гарантированное автоматическое слияние без потерь).
LWW работает для простых полей (заголовки, статусы), где потерять одно из изменений приемлемо. Manual merge - для важных данных типа документов (Google Docs показывает оба варианта). CRDT - для real-time collaborative editing: текст представляется как граф операций (insert character at position X by user Y), который сходится к одному состоянию у всех клиентов независимо от порядка применения операций. Yjs и Automerge - две популярные CRDT-библиотеки для JavaScript.
Offline-first - это просто 'сохранить ответ API в кэш и показать из кэша при отсутствии сети'
Полноценная offline-first архитектура включает четыре уровня: persistent operations outbox для исходящих мутаций, normalized cache для входящих данных, оптимистичный UI для мгновенных обновлений, и стратегия разрешения конфликтов (LWW, CRDT, manual). Простой кэш ответов не покрывает мутации
Кэш ответов решает только read-сценарий 'покажи, что было'. Реальная задача - 'продолжай работать без сети': писать, редактировать, удалять, а потом синхронизироваться без конфликтов и потерь. Это - архитектурная проблема, не библиотечная
Почему CRDT - правильное решение для real-time collaborative editing, а не LWW?
Ключевые идеи
- **REST vs GraphQL caching** - URL-based в REST требует ручной инвалидации, normalized cache в GraphQL обновляет всё автоматически
- **Outbox pattern** - persistent SQLite-таблица pending operations плюс фоновый воркер; основа надёжной синхронизации
- **Оптимистичный UI** - мгновенное обновление состояния с автоматическим rollback при ошибке; работает для high-success операций
- **Разрешение конфликтов** - LWW для простых случаев, CRDT для collaborative editing, manual merge для важных данных
Связанные темы
Возврат к мотивации: offline-first - это не одна технология, а композиция приёмов. Связь с предыдущими уроками:
- Локальное хранилище: SQLite и Realm — Persistent outbox таблица - частный случай локального хранилища, ключевой для offline-first
- Состояние приложения и Redux/MobX — Оптимистичный UI требует чёткой работы со state; reducers и actions удобно расширяются на outbox
- WebSocket и push-уведомления — Server push дополняет outbox: входящие изменения из сети попадают в normalized cache в реальном времени
Вопросы для размышления
- Если outbox pattern настолько надёжен, почему многие приложения по-прежнему показывают ошибку 'Нет соединения' вместо тихой синхронизации? Какие ограничения мешают повсеместному применению?
- Команда хочет реализовать collaborative editing для своего mobile-приложения. С чего стоит начать: с CRDT-библиотеки или с outbox pattern? Как принять решение?
- Возврат к мотивации: пользователь в метро отправил сообщение, сеть пропала, телефон выключился. Через час сеть появилась, но сообщение не доставилось. Какой компонент архитектуры дал сбой и как это починить?
Связанные уроки
- mob-13 — Local storage для offline-first требует понимания memory
- mob-10 — Clean Architecture определяет слои для REST/GraphQL
- mob-05 — State management для offline-first данных
- mob-18 — Offline-first тестируется в CI/CD pipeline
- net-21-http-basics — REST строится поверх HTTP протокола
- ds-02-cap-theorem — Offline-first - это CAP: consistency vs availability при offline
- net-15-tcp-basics