Главная » Хабрахабр » Оффлайн-режим на iOS и особенности его реализации на Realm

Оффлайн-режим на iOS и особенности его реализации на Realm

Автор: Екатерина Семашко, Strong Junior iOS Developer, DataArt

Цель приложения — возможность шаринга дисконтных карт между сотрудниками компании и их друзьями. Немного о проекте: мобильное приложение для платформы iOS, написанное на языке Swift.

Для хранения локальных данных выбрали Realm, для работы с сервером — Alamofire, для аутентификации использовался Google Sign-In, для загрузки изображений — PINRemoteImage. Одной из целей проекта было изучить и попробовать на практике популярные технологии и библиотеки.

Основные функции приложения:

  • добавление карты, ее редактирование и удаление;
  • просмотр чужих карт;
  • поиск карт по названию магазина/имени пользователя;
  • добавление карт в список избранных для быстрого доступа к ним.

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

Информация об изменениях, сделанных пользователем, сохранялась и при появлении интернет-соединения синхронизировалась. Позже мы решили расширить оффлайн, добавив режим записи. О реализации такого read-write оффлайн-режима и пойдет речь.

Что необходимо для полноценного оффлайн-режима в мобильном приложении? Нам нужно убрать зависимость пользователя от качества интернет-соединения, в частности:

  1. Убрать зависимость ответов пользователю на его действия в UI от сервера. В первую очередь запрос будет взаимодействовать с локальным хранилищем, потом уже будет отправляться на сервер.
  2. Помечать и хранить локальные изменения.
  3. Реализовать механизм синхронизации — при появлении интернет-соединения нужно отправлять изменения на сервер.
  4. Отображать пользователю какие изменения синхронизированы, какие нет.

Offline-first-подход

Первым делом мне пришлось изменить существующий механизм взаимодействия с сервером и базой данных. Цель была в том, чтобы пользователь не чувствовал зависимости от наличия или отсутствия интернета. В первую очередь он должен взаимодействовать с локальным хранилищем данных, а запросы на сервер должны идти в фоновом режиме.

Механизм работы с данными был следующим: сначала шел запрос на сервер через класс NetworkManager, мы ждали результата, после этого данные сохранялись в базу через класс Repository. В предыдущей версии присутствовала сильная связь между слоем хранения данных и сетевым слоем. Потом результат отдавался на UI, как представлено на схеме.

Для реализации offline-first-подхода я разделила слой хранения данных и сетевой слой, введя новый класс Flow, который управлял порядком вызова NetworkManager и Repository. Теперь данные сначала сохраняются в базу через класс Repository, потом результат отдается на UI, и пользователь продолжает работу с приложением. В фоновом режиме идет запрос на сервер, после ответа обновляются информация в базе данных и UI.

Работа с идентификаторами объектов

При новой архитектуре появилось несколько новых задач, одна из которых — работа с id объектов. Раньше мы получали их от сервера при создании объекта. Но теперь объект создавался локально, соответственно, необходимо было генерировать id и после синхронизации обновлять их на актуальные. Тут я столкнулась с первым ограничением Realm: после создания объекта менять его первичный ключ нельзя.

Но при этом терялись преимущества использования первичного ключа: индексирование Realm, что ускоряет fetch объекта, возможность обновления объекта с флагом create (создать объект, если его не существует), соблюдение требования уникальности объекта. Первым вариантом было отказаться от первичного ключа в объекте, сделать id обычным полем.

В результате рабочим решением было иметь два идентификатора, один из них серверный, optional поле, а второй — локальный, который и был бы первичным ключом. Я хотела сохранить первичный ключ, но он не мог быть id объекта с сервера.

Т. В итоге локальный id генерируется на клиенте при создании объекта локально, а в случае, когда объект приходил с сервера, он равен серверному id. в приложении single source of truth является база данных, при получении данных с сервера объекту проставляется актуальный локальный идентификатор, и работа идет только с ним. к. При отправке данных на сервер передается серверный идентификатор.

Хранение несинхронизированных изменений

Изменения объектов, которые еще не были отправлены на сервер, необходимо хранить локально. Это можно реализовать следующими способами:

  1. добавлением полей к существующим объектам;
  2. хранением несинхронизированных объектов в отдельных таблицах;
  3. хранением отдельных изменений полей в каком-то формате.

Автообновление интерфейса идет с помощью auto-updating results выборок, где я подписываюсь на обновления запросов. Я не использую объекты Realm напрямую в своих классах, а делаю их маппинг на свои, чтобы избежать проблем с многопоточностью. Только первый подход работал с моей текущей архитектурой, поэтому выбор пал на добавление полей к существующим объектам.

Больше всего изменений претерпел объект карты:

  • synced — есть ли данные на сервере;
  • deleted — true, если карта удалена только локально, необходима синхронизация.

Идентификаторы, о которых шла речь в предыдущей части:

  • localId — первичный ключ сущности в приложении, либо равный серверному id, либо сгенерированный локально;
  • serverId — id с сервера.

Отдельно стоит упомянуть хранение изображений. В сущности Attachment к полю serverURL изображения на сервере добавилось поле diskURL, хранящее адрес локального несинхронизированного изображения. При синхронизации изображения локальное удалялось, чтобы не засорять память устройства.

Синхронизация с сервером

Для синхронизации с сервером была добавлена работа с Reachability, чтобы при появлении интернета запускался механизм синхронизации.

Потом на сервер идет запрос о получении актуального слепка данных, в результате на клиенте отсеиваются изменения, которые не нужно посылать (например изменение объекта, который на сервере уже удалили). Сначала проверяется, есть ли в базе данных изменения, которые нужно отправить. Оставшиеся изменения формируют очередь запросов на сервер.

Но к тому времени бэкенд-разработчик уже был занят в другом проекте, и помогал нам только в свободное время, поэтому для каждого типа изменения создается свой запрос. Для отправки изменений можно было реализовать bulk updates, посылая изменения массивом, или же сделать большой запрос на синхронизацию всех данных.

Некоторые операции зависят друг от друга, например, мы не можем загрузить изображение карты до создания карты, поэтому я добавила dependency операции изображения на операцию карты. Я реализовала очередь через OperationQueue и обернула каждый запрос в асинхронную Operation. Также операции загрузки изображений на сервер был поставлен меньший приоритет, чем всем остальным, и добавляла в очередь я их тоже последними из-за их тяжеловесности.

Но когда мы дошли до этого в ходе реализации, поняли, что случай, когда пользователь меняет одни и те же свои данные на разных устройствах, очень редкий. При планировании оффлайн-режима большим вопросом было разрешение конфликтов с сервером при синхронизации. При синхронизации приоритет всегда отдается неотправленным изменениям на клиенте, они не перетираются. Значит нам достаточно реализовать механизм last writer wins.

И потом если он так и будет висеть несинхронизированным после мерджа, пользователь сам решит, оставить его или удалить. Обработка ошибок пока в зачаточном состоянии, при неудачной синхронизации объект будет добавлен в очередь изменений при следующем появлении интернета.

Дополнительные workaround при работе с Realm

При работе с Realm столкнулись еще с несколькими проблемами. Возможно, этот опыт также будет кому-то полезен.

Мы столкнулись с ситуацией, когда названия в нижнем регистре идут после названий в верхнем регистре, например: Магнит, Пятерочка, Лента. При сортировке по строке порядок идет согласно порядку символов в UTF-8, поддержки регистронезависимого поиска нет. Если список очень большой, все названия в нижнем регистре будут внизу, что очень неприятно.

Для сохранения порядка сортировки независимо от регистра пришлось вводить новое поле lowercasedName, обновлять его при обновлении name и сортировать по нему.

Также новое поле добавили для сортировки по наличию карты в избранном, так как по сути для этого нужен подзапрос по связям объекта.

Но, увы, он работает только с латиницей. При поиске в Realm есть метод CONTAINS[c] %@ для регистронезависимого поиска. Но позже это оказалось нам на руку для исключения специальных символов при поиске. Для русских брендов тоже пришлось создавать отдельные поля и вести поиск по ним.

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

Несмотря на некоторые сложности, вы можете использовать для его реализации Realm, получая при этом все преимущества в виде live updates, zero-copy architecture и удобного API.

Так что нет никакой причины лишать ваших пользователей возможности доступа к данным в любой момент, вне зависимости от качества соединения.


Оставить комментарий

Ваш email нигде не будет показан
Обязательные для заполнения поля помечены *

*

x

Ещё Hi-Tech Интересное!

[Перевод] Интервью с Дэвидом Гобелем

Дэвид любезно согласился дать LEAF очень интересное интервью. Дэвид Гобель – изобретатель, филантроп, футурист и ярый сторонник технологий омоложения; вместе с Обри де Греем он известен как один из основателей Methuselah Foundation и как автор концепции Longevity Escape Velocity (LEV), ...

10 долларов на хостинг: 20 лет назад и сегодня

Всё кругом дорожает, а технологии дешевеют. Когда-то компьютер или мобильный телефон могли позволить себе единицы, сейчас эти устройства есть в каждой российской семье. Цена мегабайта за последние 20 лет упала в несколько тысяч раз. Ещё один пример — хостинг. В ...