🔥

Тред (Саша Аскеров)


Сегодня о том, как можно делать поддержку оффлайн режима приложения. Такого, в котором данные можно просматривать и изменять, а потом все синхронизируется. Плюс к этому, с одними и теми же данными могут работать несколько клиентов.

Зачем оффлайн? Причина очевидная: отсутсвие интернета или нестабильность интернета. И менее очевидная: качественный 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… Я ее читал уже после того, как мы сделали свою реализацию. Как это ни странно, у нас одинаковые проблемы :) А вот решали мы их уже по разному.

Саша АскеровСаша Аскеров