Создание offline-first Android-приложения
«Offline-first» используется свободно, означая «работает без интернета», но реальная архитектура более конкретна.
Основная идея: локальные данные — источник истины
В offline-first приложении UI читает из локальной базы данных, не напрямую из сети. Задача сети — синхронизировать эту локальную базу данных в фоне — UI не нужно знать или заботиться о том, произошла ли только что синхронизация.
Это значимо другая форма, чем «кэшировать последний ответ API и показывать его, если офлайн» — это поддерживает чтение, запись и даже создание новых данных в офлайне, с синхронизацией, примиряющей всё, как только соединение возвращается.
Типичный паттерн
- UI читает из Room (или другой локальной базы данных) через Flow для реактивных обновлений.
- Слой репозитория решает, когда получать данные из сети, и записывает результаты в локальную базу данных — UI никогда не общается с сетью напрямую.
- Механизм синхронизации (WorkManager — стандартный современный выбор) работает в фоне, отправляя локальные изменения вверх и получая удалённые изменения вниз, независимо от того, открыто ли приложение у пользователя прямо сейчас.
class NotesRepository(private val dao: NoteDao, private val api: NotesApi) {
fun getNotes(): Flow<List<Note>> = dao.getAll()
suspend fun refresh() {
val remoteNotes = api.fetchNotes()
dao.insertAll(remoteNotes)
}
}UI непрерывно наблюдает за getNotes() — он обновляется автоматически, когда бы refresh() ни записывал новые данные, без необходимости для слоя UI знать, что вообще произошёл сетевой вызов.
Сложная часть: разрешение конфликтов
Если одна и та же запись может редактироваться и офлайн (на устройстве), и в другом месте (другое устройство, веб-версия, сервер напрямую), вам нужна явная стратегия того, что происходит, когда обе версии пытаются примириться:
- Последняя запись побеждает — проще всего, но может молча отбросить офлайн-правку пользователя.
- Слияние на уровне полей — сложнее, но избегает потери изменений, которые на самом деле не конфликтуют.
- Показать конфликт пользователю — уместно, когда молчаливый выбор победителя рискует потерять то, что важно пользователю.
Здесь нет универсально правильного выбора — это зависит от того, насколько потерянная правка реально важна для вашего конкретного приложения.
Почему встраивание этого позже дорого
Приложение, построенное в предположении, что UI читает напрямую из сетевых ответов, имеет это предположение встроенным в большую часть кода — экраны, view model, обработку ошибок, всё построено вокруг «сетевой вызов либо успешен, либо показывает ошибку». Преобразование этого в «UI всегда читает локальные данные, которые могут быть, а могут и не быть свежесинхронизированы» затрагивает большинство тех же мест. Это именно то решение, которое стоит принять явно в начале — см. наш гайд по архитектуре Android насчёт того, как это вписывается в более широкую картину ранних решений.
Планируете приложение, которому нужно надёжно работать без соединения, или пытаетесь встроить офлайн-поддержку в существующее? Свяжитесь со мной — эти две ситуации требуют по-настоящему разных подходов.
Частые вопросы
Offline-first — то же самое, что кэширование ответов API?
Связано, но уже — кэширование просто хранит прошлые ответы для повторного использования. Offline-first означает, что приложение относится к локальным данным как к источнику истины, из которого читает UI, с сетевой синхронизацией как фоновой заботой, что также поддерживает запись/создание данных в офлайне.
Можно ли добавить offline-first к существующему приложению позже?
Это возможно, но значимо сложнее, чем проектировать это с самого начала — встраивание модели «локальная база данных как источник истины» в приложение, построенное в предположении, что UI читает напрямую из сетевых вызовов, — существенное архитектурное изменение, а не постепенное.
Что происходит, если одни и те же данные редактируются и офлайн, и на сервере?
Это сложная часть — разрешение конфликтов нуждается в явной стратегии (последняя запись побеждает, логика слияния, или показ конфликта пользователю), и стоит проектировать это намеренно, а не оставлять как второстепенное, поскольку именно здесь offline-first приложения чаще всего ведут себя неожиданно.