Сегодня о том, как можно делать поддержку оффлайн режима приложения. Такого, в котором данные можно просматривать и изменять, а потом все синхронизируется. Плюс к этому, с одними и теми же данными могут работать несколько клиентов.
Зачем оффлайн?
Причина очевидная: отсутсвие интернета или нестабильность интернета.
И менее очевидная: качественный UX. Нам не нужно заставлять пользователя ждать пока сервер что-то ответит, блокировать его ошибками про соединение и т.п.
На сегодня все. Завтра будет про то, как мы делаем поддержку оффлайна. А в приложении, котором вы занимаетесь есть оффлайн режим?
А еще, толстый клиент это интересно. Немножко сложнее, чем отправил запрос -> показал результат.
Кстати, удивлен результатами вчерашнего опроса.
Думал первый и второй варианты будут сильно популярнее, а там примерно поровну.
twitter.com/mobileunderhoo…
Во первых для оффлайна нужен single source of truth. В нашем случае это локальная sqlite база данных. Не нужно выбирать источник данных в зависимости от наличия коннекта.
Чтение: прочитали данные из базы, параллельно пошли в интернет, обновили данные в базе.
Запись: записали в базу -> прогнали синк через ворк менеджер
UI подписан на изменения в нужных таблицах, например на уровне ViewModel
Вопросы которые тут возникают:
Как сохранять изменения сущностей, чтобы их можно было отправить
Как делать так, чтобы наши изменения не затирались при апдейте данных с сервера
Как обрабатывать ошибки
Хранить изменения можно в отдельной таблице: истории операций.
Попробуем рассмотреть случай апдейта уже существующей сущности. С созданием все похоже, но там есть свои тонкости.
Тут есть 2 варианта.
Первый: на каждый апдейт сохранять данные только в истории операций. Начнем с него.
Заведем таблицу "Operations":
"entity_type" и "entity_id" — поля ссылки на основную сущность, которую изменяем
"field" — название поля, которое изменяем
"new_value" — новое значение для этого поля
При таком подходе у нас получаются иммутабельные сущности с точки зрения локальных изменений. Любое локальное обновление — это простая вставка записи в таблицу операций.
Плюс такого подхода: при обновлении данных с сервера мы спокойно обновляем таблицу сущности, независимо от того, есть у нас не синхронизированные локальные изменения или нет.
При каждом запросе на чтение сущностей, для показа пользователю придется применять историю операций. Это становится минусом, если есть много запросов на чтение с разным набором полей и джойнами.
Второй вариант.
Убираем поле value из таблицы с операциями. Запись в таблице операций становится признаком изменения. При локальном обновлении мы изменяем как значение в таблице сущности, так и добавляем запись в операцию, с указанием изменённого поля.
Запись локальных изменений становится чуть сложнее, появляется необходимость в транзакциях для обеспечения целостности.
А еще возникает сложность с обновлением данных с сервера. Нужно смотреть, что у нас есть в истории, и не обновлять эти поля при записи серверных изменений.
Но чтение в таком случае простое: таблица операций нас не интересует, мы просто читаем основную таблицу сущности.
У нас реализован последней вариант, так как замержить данные полученные с сервера с операциями проще, чем делать это каждый раз при запросе на выборку.
При синхронизации операции сортируются по времени, и группируются по идентификатору и типу, чтобы отправить одним запросом все последовательные операции по одной сущности.
Важно, чтобы бекэнд API умело принимать отдельные изменения, а не, например, целиком сущность. Иначе даже изменение отдельных полей разными пользователями будут конфликтовать.
Могут конечно быть конфликты в отдельных полях. Как их решать не очень понятно. Наша реализация тут простая: кто последний тот и прав. Это не пойдет для ситуаций, когда идет работа над большим текстом, но тут необходимо отдельное решение.
Ошибки.
Их нужно так же где-то сохранять и показывать пользователю.
В нашем приложении ошибки мы так же трекаем в таблице операций. По ним строим по экран с логом, есть возможность повторить каждую операцию.
Плюс к этому можно добавить автоматические повторы. Но тут важно не забыть поставить ограничение на число таких повторов, и поставить backoff по времени.
Впрочем, некоторые запросы вообще нет смысла повторять. Если сервер ответил Bad Request, то вряд ли получится добиться иного.
Напротив, всякие ситуации с недостатком разрешений на редактирование часто решаемы.
Пожалуй все. Мы тут рассмотрели только случай обновления, а есть еще создание, проблемы со связными сущности, файловые вложения. Но это сложно уместить в один фид.
В заключении оставлю ссылку на серию статей разработчиков из Trello на эту же тему. tech.trello.com/sync-architect…
Я ее читал уже после того, как мы сделали свою реализацию. Как это ни странно, у нас одинаковые проблемы :) А вот решали мы их уже по разному.
Саша Аскеров