Offline-first Android приложения

Offline-first Android applications

31 may 2020

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

  • Логин/Регистрация
  • Просмотр списка (новости, посты, товары)
  • Просмотр деталей (новость, пост, товар)
  • Просмотр/Редактирование профиля

Большая часть работы сводится к однотипным экранам и их вариациям в различных приложениях. Неотъемлемой частью таких приложений является работа с сервером: получение, отправка, редактирование данных и так далее. Естественно, осуществление таких операций требует наличия интернет-соединения.

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

О Проекте

Приложение является клиентом к разрабатываемой нами платформе Doma.ai — нацеленной на автоматизацию, оптимизацию и облегчение ведения бизнеса в сфере жилищно-коммунальных услуг. Пользователи этого приложения — сотрудники компаний сферы ЖКХ.

Задача приложения

Основной задачей приложения является реализация потока выполнения заявок:

  • Создание
  • Редактирование
  • Выполнение (последовательное изменение статуса в процессе работы над заявкой)
  • Комментирование
  • Прикладывание документов

Почему offline-first?

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

Собственно, из-за этих двух факторов единственным подходящим решением является offline-first работа приложения.

Какие проблемы нас ждали?

Первое с чем пришлось столкнуться — как реализовать периодическую отправку данных?

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

В итоге выбор пал на JobScheduler и JobService — в JobService производится синхронизация, а JobScheduler позволяет запланировать выполнение раз в 15 минут. Данная связка использовалась для синхронизации в те моменты, когда приложение свернуто. Для синхронизации в процессе работы с приложением(на переднем плане) используется Observable.interval, который раз в 5 минут запускал выполнение JobService. Переключение между тем или иным способом синхронизации происходило в ApplicationActivity, приложение построено по принципу SingleActivity, а следовательно уход ApplicationActivity в фон — означает уход всего приложения в фон.

Второе — реализация бизнес-логики сервера на клиенте. Что это значит?

Так как данные изменяются локально, а лишь затем, спустя время отправляются на сервер, необходимо быть уверенными в том, что такое изменения возможно, поэтому многие вещи пришлось дублировать в мобильном приложение. К примеру, «может ли этот пользователь писать сообщения в чат с жителями?», «На какой статус возможен переход из текущего?», «Можно ли показывать текущему пользователю номер телефона жителя?» и т.п. Какой-то серебряной пули для решения этой проблемы нет, что-то получилось частично вынести на сервер — дерево переходов по статусам подгружается при синхронизации с сервера, что-то решается локально — по роли пользователя определяется может ли он писать/звонить жителям. Также из-за того что бизнес-логика дублируется на клиенте, приходится заметно чаще выпускать обновления, так как если вдруг в процессе работы с заявкой появились какие-то логические изменения — их также необходимо добавить и в приложение.

Третье — гарантия целостности данных.

Так как мобильное приложение позволяет провести в офлайне весь жизненный цикл заявки — от создания до выполнения, пришлось ввести приоритетность отправки данных:

  1. Запросы создания заявки;
  2. Запросы изменения заявки;
  3. Запросы загрузки файлов на сервер;
  4. Запросы создания документов и комментариев;
  5. Запросы изменения статуса

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

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

Заключение

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

Работа над подобными приложения — это увлекательный процесс, который заставляет экспериментировать, думать и планировать, так как без четкого алгоритма того, как должна работать синхронизация — разработка рискует превратиться в бесконечный процесс исправления ошибок.

Nowadays most of the developed mobile applications are clients for some services: online store, social network, news site, etc. Development of such applications often consists of developing the following functionality:

  • Login/Registration
  • View the list (news, posts, goods)
  • Viewing details (news, post, product)
  • View/Edit Profile

Most of the work comes down to similar screens and their variations in different applications. An integral part of such applications is working with the server: receiving, sending, editing data and so on. Naturally, performing such operations requires an Internet connection.

And here, at one moment we have the project, one of which requirements is working capacity in the absence of the Internet. This post is about how it was implemented, and what we had to face.

About project

The application is a client of the Doma.ai platform that we are developing - aimed at automating, optimizing and facilitating business in the field of housing and communal services. Users of this application are employees of companies in the field of housing and communal services.

Application purpose

The main task of the application is to implement the request execution flow:

  • Creation
  • Editing
  • Execution (consecutive change of status while working on the request).
  • Commenting
  • Attachment of documents

Why offline-first?

Since the application is mostly used by home maintenance staff, they often have to be in rooms where communication is not available, if not absent, so these data is needed locally without having to download them from outside. Plus, an employee can/should upload photos, write comments and change the status of the request while they are working.

Actually due to these two factors the only suitable solution is offline-first operation of the application.

What kind of problems did we face?

The first thing we faced was how to implement periodic data sending?

The first idea was to create a foreground service that would send and receive data once in N-minutes. This approach solved the problem, but led to serious battery consumption (the devices in this mode of use died in few hours).

As a result, the choice fell on JobScheduler and JobService - in JobService synchronization is performed, and JobScheduler allows you to schedule the execution once every 15 minutes. This bunch was used for synchronization when the application was minimized. For synchronization while working with the application (in the foreground), the Observable.interval is used, which starts the JobService execution every 5 minutes. Switching between different synchronization methods took place in ApplicationActivity, the application is built on the principle of SingleActivity, and therefore leaving ApplicationActivity in the background means leaving the entire application in the background.

The second thing is implementation of server business logic on the client. What does it mean?

Since the data is changed locally and only then, after a while, is sent to the server, you need to be sure that such changes are possible, so many things had to be duplicated in the mobile application. For example, "can this user write messages in chat with residents?", "What status is possible to move from the current one?", "Can the current user see the resident's phone number?", etc. There is no silver bullet to solve this problem, something has been partially brought to the server - the transition tree by status is loaded during synchronization from the server, something is solved locally - the role of the user determines whether the user can write/call residents. Also, due to the fact that business logic is duplicated on the client, we have to release updates much more often, because if there are any logical changes in the process of working with the application - they must also be added to the application.

The third thing is the guarantee of data integrity.

Since the mobile application allows you to spend offline the entire life cycle of the request - from creation to execution, you had to enter the priority of sending data:

  1. Request creation
  2. Request for modification of application
  3. File upload requests to the server
  4. Request for creation of documents and comments
  5. Status change requests

Why is that? Let's start with the end, moving to some statuses requires attached documents, so you have to attach those documents first. Documents in turn require links to files uploaded to the server. Modifying a request requires the ID of the request in the system, and finally creating a request returns us the ID of the request.

In addition, there may be situations when during offline operation of the application, the application has been modified by another user who is entitled to do it. Sometimes these changes may not be compatible with local changes. This problem was solved in the following way: we roll up the changes to the task until we come across an error, in case of an error we cancel all subsequent changes and notify the user about it. To rollback the request, we keep an intermediate copy that reflects the states before each change is made, if successful - the copy is deleted, if unsuccessful - the request is replaced by an intermediate copy.

Conclusion

Apart from the above mentioned problems, offline-first applications are no different from usual ones. In fact, the local database acts as a server and everything works in the same way, but in addition to this there is another layer that synchronizes the local database with the remote server.

Working on such applications is a fascinating process that makes you experiment, think and plan, because without a clear algorithm of how synchronization should work - development becomes an endless process of error correction.