[:ru]Вторая лекция курса по архитектуре клиент-серверных android-приложений, в которой мы рассмотрим такие понятия, как REST-архитектура, ContentProvider, Паттерны A, B и C  и разные подходы по реализации взаимодействия с сетью.

Введение

На прошлой лекции мы рассмотрели способы обработки пересоздания Activity и проблемы сохранения данных при пересоздании. Но здесь есть и другой фактор – ведь Activity может не пересоздаваться при выполнении запроса, а вообще уничтожиться. А это грозит некоторыми проблемами. Во-первых, у вас может произойти рассинхронизация данных на сервере и клиенте. Клиент выполнит запрос, сервер примет его и вернет результат с новыми данными, которые клиент уже не сможет получить (в связи с тем, что приложение было закрыто). Когда клиент откроет приложение в следующий раз, к примеру, в офлайне, он увидит старую информацию, которую не ожидает увидеть. Во-вторых, возможно, что для завершения операции вам нужно получить один ответ, обработать его и выполнить новый запрос. Бесспорно, что это ужасная ситуация и так делать никогда не нужно, но бэкенды бывают разные, и не всегда они идеальны. В таком случае ситуация еще хуже, так как данные вашего приложения могут зависнуть посередине процесса.

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

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

Service (сервис) – это компонент, который предназначен для выполнения определенных операций в фоне и который не содержит никаких UI-элементов. Огромным плюсом сервисов является то, что они продолжат работать даже тогда, когда пользователь закроет все Activity приложения.

Простейшая реализация сервиса выглядит следующим образом:

 

После этого сервис нужно зарегистрировать в манифесте:

 

И его можно запустить следующим образом:

 

Intent, который передается в качестве параметра методу startService, в итоге попадает параметром в метод onStartCommand, где вы можете получить все необходимые переданные параметры.

Метод onBind служит для создания другого типа сервисов, которые в контексте рассматриваемой темы нас не интересует. Можно только сказать, что это позволяет привязать сервис к другому компоненту приложения (к примеру, к Activity) и осуществлять различную фоновую работу, в том числе и между процессами. Но поскольку привязанный сервис умирает вместе с компонентом, к которому он привязан, для решения текущей проблемы он нам никак не поможет. Более подробно о его применениях можно посмотреть в документации.

Несмотря на тот факт, что сервис предназначен для работы в фоне, сам класс Service не содержит средств для обеспечения асинхронности выполнения задач, и метод onStartCommand работает в главном потоке приложения. Асинхронность должен обеспечить разработчик, воспользовавшись любыми известными ему средствами. Есть и альтернатива – IntentService, который является простой однопоточной реализацией сервиса. К примеру, для загрузки данных и выполнения сетевых запросов IntentService можно использовать следующим образом:

 

Здесь нужно еще сделать небольшое примечание – IntentService однопоточный и обрабатывает все поступающие к нему запросы последовательно.

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

 

А именно на то, что этот метод имеет тип void. То есть данные о погоде мы загрузили, но как именно передать эту информацию не совсем понятно. В этом и заключается основная проблема сервисов – так как они никак не связаны с UI-классами, они не могут передать данные в них.

Одно из стандартных решений в таком случае – это использование различных Bus, к примеру, EventBus, Otto или даже шины на RxJava. Но такие способы ненадежны, поскольку работа с Bus требует подписки и отписки в UI-классах согласно методам жизненного цикла. К тому же их использование делает код неочевидным.

К сожалению, для этой проблемы нет простого и хорошего решения. И именно поэтому мы рассмотрим паттерны, которые были представлены на конференции Google I/O в 2010 году.

Для понимания этих паттернов нам понадобится сначала подробнее познакомиться с понятиями REST, а также изучить API ContentProvider, поскольку эти паттерны полностью базируются на них.

 

REST

Вероятно, каждый слышал такие слова как REST API, RESTful сервисы и другие смежные понятия. Что под этими понятиями подразумевается? В первую очередь, REST – это не какой-то жестко зафиксированный формат работы с веб-сервисами, а скорее набор принципов, который определяет, как должны взаимодействовать между собой веб-стандарты / компоненты, такие как HTTP, URI, и как они должны использоваться. Эти принципы предназначены для того, чтобы строить архитектуру веб-сервисов.

Из этого набора принципов можно выделить несколько ключевых:

  1. Каждая сущность должна иметь уникальный идентификатор – URI.
  2. Сущности должны быть связаны между собой.
  3. Для чтения и изменения данных должны использоваться стандартные методы.
  4. Должна быть поддержка нескольких типов ресурсов.
  5. Взаимодействие должно осуществляться без состояния.

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

Принцип 1 говорит о том, что каждый объект должен иметь свой уникальный идентификатор, более того – адрес URI, по которому к нему можно обратиться. URI (от Uniform Resources Identifier) – уникальный идентификатор ресурса. Как мы увидим далее, этот термин широко используется и в Android. Уникальный идентификатор нужен для того, чтобы обращаться к этому объекту и чтобы связывать его с другими объектами.

Принцип 3 сообщает об использовании стандартных методов для вызова удаленных процедур (и изменения данных). Эти методы хорошо известным всем:HTTP методы

  • GET – получение данных без их изменения. Это наиболее популярный и легкий метод. Он только возвращает данные, а не изменяет их, поэтому на клиенте вам не нужно заботиться о том, что вы можете повредить данные.
  • POST – метод, подразумевающий вставку новых записей.
  • PUT – метод, подразумевающий изменение существующих записей.
  • PATCH – метод, подразумевающий изменение идентификатора существующих записей.
  • DELETE – метод, подразумевающий удаление записей.

Знание об этих методах очень важно для нашего дальнейшего изучения. И для нас достаточно информации о REST, давайте только упростим его итоговое понимание и дадим свое представление. REST API – это набор удаленных вызовов стандартных методов, возвращающих данные в определенном формате. С таким определением и будем дальше работать.

ContentProvider

В этой лекции мы также рассмотрим очень удобный способ для работы с данными в Android, а именно ContentProvider. ContentProvider – это класс, предоставляющий унифицированный интерфейс для доступа к данным приложения. Этот класс позволяет вам использовать единый источник данных в вашем приложении.

ContentProvider также позволяет передавать данные между приложениями. К примеру, таким образом осуществляется работа с телефонными контактами, смс и другими системными данными. К большому сожалению, на этот счет в документации есть серьезная ошибка. В документации по ContentProvider сказано, что “You don’t need to develop your own provider if you don’t intend to share your data with other applications.”, то есть мы не должны реализовывать свой ContentProvider, если мы собираемся использовать его только внутри своего приложения. Такая ошибка стоит дорого, многие разработчики после прочтения верят в то, что ContentProvider им никогда не понадобится, а это не так. Так какие же преимущества есть у этого класса, и зачем он может нам понадобиться?

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

Во-вторых, вам не нужно управлять жизненным циклом объекта для доступа к данным (к примеру, экземпляра SQLiteDatabase). Ведь в случае прямого использования таких объектов возникает немало вопросов: где хранить этот объект? Когда закрывать базу данных? Когда уничтожать этот объект? ContentProvider позволяет вам не беспокоиться о таких вещах. К тому же он позволяет получать доступ к данным из любого места, где доступен контекст приложения (экземпляр Context).

И в-третьих, ContentProvider полностью соответствует концепциям REST, которые мы рассмотрели. Это хорошо тем, что позволяет использовать его для наших целей (спойлер – ContentProvider может выступать в роли фасада для запросов к серверу), и это будет подробнее рассмотрено далее. А пока перейдем к работе с ContentProvider.

Поскольку ContentProvider соответствует принципам REST, то для каждой сущности у него есть свой URI. У ContentProvider есть базовый URI, который определяется создателем приложения и регистрируется в манифесте. Это выглядит примерно следующим образом:

 

Для обращения к данным через ContentProvider нам требуется URI для этих данных (по URI можно обращаться как к группе данных, так и к отдельному объекту). У ContentProvider приложения всегда определен базовый URI, который формируется из authorities в манифесте и префикса content://. Поэтому в нашем случае базовый URI будет такого вида:

 

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

 

Если мы обратимся к данным в ContentProvider по этому URI, то получим все экземпляры, сохраненные в этой группе.

И наконец, если нам нужно URI для отдельного объекта, то оно будет выглядеть следующим образом:

 

Где 4 – это номер добавленного экземпляра.

Работа с URI очень важна. API ContentProvider позволяет следить за изменениями в данных по конкретному URI, что чрезвычайно удобно, так как позволяет легко организовать автоматическое обновление данных в UI-классах.

Но в общем здесь нет ничего сложного и нового, это стандартные средства формирования URI (а на самом деле URL – это частный случай URI, так что такой принцип всем прекрасно знаком). Поэтому закончим с URI и перейдем к реализации своего ContentProvider.

Примечание: в дальнейшем в рамках этой лекции мы будем работать со специальной библиотекой, которая основана на ContentProvider, поддерживает табличную модель и позволяет выполнять все нужные функции, в том числе подписываться на уведомления об изменениях в таблице. Это скрывает некоторые излишние детали реализации, позволяя целиком сосредоточиться на сути лекции. Описание и код библиотеки доступны по ссылке выше. Все примеры далее будут основаны на этой библиотеке.

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

 

Что делает код в этом методе? Во-первых, он создает базовый URI для дальнейшего обращения к данным в ContentProvider. Во-вторых, он создает схему данных в ContentProvider (в простейшем случае – добавляет таблицы в базу данных). И наконец, в этом методе создается экземпляр SQLiteOpenHelper, через который и будет организован доступ к хранилищу данных.

Следующим методом является метод getType. Этот метод должен вернуть тип данных, который содержится по переданному в качестве параметра URI. Обычно этот метод используют для сопоставления имени таблицы и URI, чтобы потом выполнить запрос к базе данных. В нашем случае реализация внешне выглядит просто, а углубляться мы не будем (это всегда можно сделать, посмотрев код библиотеки):

 

И далее начинаются методы, которые непосредственно предназначены для работы с данными. Это методы, которые полностью соответствуют HTTP методам, что мы и будем использовать в дальнейшем. А пока посмотрим реализацию этих методов. В большинстве случаев эти методы только переадресовывают вызов базе данных SQLite. В первую очередь это метод query (который соответствует HTTP методу GET):

 

Этот метод в начале проверяет, существует ли вызываемая таблица, и, если да, то вызывает метод query у объекта SQLiteDatabase, который на основании всех параметров строит SQL-запрос и возвращает данные.

Следующим идет метод insert, предназначенный для добавления элемента в ContentProvider и соответствующий HTTP методу POST:

 

Обратите внимание на следующий вызов:

 

База данных SQLite ничего не знает о таких штуках как URI, она привыкла работать с идентификаторами в другом виде, к примеру, в виде id. К счастью, встроенные средства Android позволяют с легкостью объединить использование ContentProvider и SQLite.

Следующим необходимым для переопределения методом является метод delete и служащий для удаления данных из ContentProvider. И, кстати говоря, это единственный метод ContentProvider, HTTP аналог которого имеет такое же название. Реализация этого метода аналогична всем остальным:

 

И последний метод, который нужно переопределить для реализации ContentProvider, это метод update (в терминологии HTTP методов – PUT или PATCH):

 

Кроме этих методов, достаточно часто переопределяется и метод bulkInsert (который в отличие от предыдущих методов не является обязательным к реализации). Этот метод предназначен для вставки массива элементов, и его реализация по умолчанию вызывает метод insert для каждого элемента, что неэффективно. Поэтому этот метод обычно переопределяют так, чтобы выполнить все вставки в рамках одной транзакции:

 

Вот и все! Мы создали свой ContentProvider, добавили его в манифесте и теперь можем обращаться к нему в качестве интерфейса для работы с данными. Здесь есть небольшая тонкость – мы создавали объект ContentProvider, но обращаться к данным нужно через объект ContentResolver, который можно получить через метод getContentResolver в класс Context:

Нельзя не сказать и следующее замечание. Большинство современных библиотек для работы с базой данных предоставляют вам намного более удобный интерфейс для работы, нежели SQL-запросы к базе. При этом они либо скрывают под своей реализацией работу с ContentProvider, либо напрямую работают с экземплярами баз данных (и в таком случае они наверняка не поддерживают уведомления об изменение таблиц или тем более конкретных записей). Поэтому глубоко изучать принципы работы ContentProvider нам сейчас не нужно, а хватит такого понимания связи ContentProvider и REST архитектуры и принципов работы основных методов.

 

Паттерны A/B/C

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

Идея очень простая – посредником между сервисом и UI-классами выступит ContentProvider, так как его идеология и методы полностью совпадают с архитектурой REST. Доступ к данным в ContentProvider можно получить из любого места, где доступен контекст приложения, то есть и из сервиса, и из UI-классов. А вместе с возможностью отслеживать изменения конкретных сущностей или таблиц по URI ContentProvider позволяет обеспечить невероятно качественный и удобный (по сравнению с другими способами взаимодействия между сервисами и UI-классами) уровень взаимодействия этих компонентов.

Паттерн A

Паттерн А

В общем виде архитектуру паттерна A можно описать следующей последовательностью действий:

  1. UI-класс запускает сервис для выполнения запроса и подписывается на изменение данных в таблице.
  2. Сервис выполняет запрос, сохраняет данные в базу и уведомляет об изменении таблицы.
  3. UI-класс получает уведомление об изменение данных в таблице, считывает новые данные и отображает их пользователю.

Эту последовательность действий можно представить в виде следующей диаграммы:

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

 

Когда сервис загрузит данные, подписавшийся класс получит уведомление об этом, которое сможет обработать:

 

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

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

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

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

В первую очередь мы должны завести класс, который будет отвечать за статус и информацию о каждом запросе. Это позволит нам, и отслеживать статус, и обрабатывать ошибки. Этот класс должен содержать идентификатор запроса, статус запроса и тело ошибки (которое будет пустым в случае успешного запроса):

 

Создадим таблицу для этого класса. После этого в UI-классах всегда будем подписываться на изменения именно этой таблицы со статусом (в идеале, конечно, это должна быть подписка на отдельный элемент, но это сложнее реализовать).

После этого переработаем сервис для запросов следующим образом (в связи с тем, что код значительно увеличился, будем приводить его по частям):

 

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

После этого мы переводим запрос в статус IN_PROGRESS и уведомляем подписчиков (это нужно для того, чтобы UI-классы могли отобразить процесс загрузки).

Продолжим рассмотрение метода:

 

Здесь все просто. Мы получаем результат с сервера, сохраняем их в базе данных и в зависимости от успеха / неудачи при получении данных переводим статус либо в SUCCESS, либо в ERROR. И уведомляем подписчиков о завершении запроса.

UI-часть может обрабатывать эти уведомления следующим образом (здесь используются средства RxJava для обеспечения асинхронности при работе с базой данных. RxJava будет рассматриваться в следующей лекции, а пока код детально прокомментируем):

 

При получении уведомления об изменении данных в таблице, мы считываем статус запроса из базы. После этого мы выбираем действия в зависимости от этого статуса:

  • Если статус IN_PROGRESS, то показываем процесс загрузки
  • Если статус ERROR, то полученная ошибка прокидывается дальше и отображается.
  • Если статус SUCCESS, то мы считываем информацию о городе и передаем ее подписчику, который покажет эту информацию.

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

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

Подводя промежуточный итог, можно сказать, что паттерн A имеет 2 главных преимущества:

  • Гарантированное выполнение всех запросов независимо от жизненного цикла UI-классов.
  • Поддержка единого источника данных в приложении.

Но у него, разумеется, есть и проблемы, однако все из них можно решить. Правда, это может сильно усложнить архитектуру приложения.

Паттерн B

Паттерн B

У паттерна A, который мы рассмотрели, разумеется, есть и недостатки. Это и большое количество кода, и необходимость использовать работу с данными на основе ContentProvider, и задача обработки жизненного цикла, и другие. Поэтому предлагаются и другие варианты реализации клиент-серверного взаимодействия, которые лишены части этих недостатков (хотя откровенно говоря, они обладают другими недостатками).

Паттерн B в плане идейных блоков очень похож на паттерн A, в нем точно также используются Service и ContentProvider, но вот порядок их использования диаметрально противоположен. Если в паттерне A ContentProvider использовался как вспомогательный слой для взаимодействия между сервисом, выполняющим сетевые запросы, и UI-классами, то в паттерне B UI-классы работают исключительно и всегда с API ContentProvider. И уже другие классы синхронизируют данные из ContentProvider с данными с сервера. Поэтому для данного паттерна API ContentProvider является в общем случае локальной версией серверной части со всеми вытекающими последствиями.

Схема паттерна B выглядит следующим образом:

В этом паттерне есть невероятно существенный плюс – вся работа с “сетью” осуществляется локально, а это дает нам уверенность, что любой запрос выполнится если не мгновенно, то очень быстро. Поэтому мы избавляемся от долгих операций и ожидания, а также от многих проблем при обработке жизненного цикла. Кроме того, наше приложение будет работать безо всяких задержек, что создаст очень хорошее впечатление у пользователя.

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

Во-вторых, чтобы хоть как-то обезопасить себя от слишком частых ошибок на стороне сервера, нужно перенести хотя бы часть серверной логики в приложение. А это как минимум означает дублирование кода и логики (двойная возможность ошибиться), да и в большинстве случаев серверная часть – это огромная система, которую крайне нерационально переносить на клиентскую сторону.

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

Паттерн С

И последний рассматриваемый паттерн – это паттерн C, который по факту является модификацией предыдущего паттерна и очень на него похож. Вся разница заключается в том, что для синхронизации используется не сервис, а класс SyncAdapter. Как можно догадаться из названия, SyncAdapter предназначен для синхронизации данных. Он имеет ряд преимуществ перед сервисами:

  • Экономия заряда батареи. Когда пользователь запускает приложение, вы начинаете дергать запросы в сеть, система вынуждена использовать радиомодуль на полную мощность, что увеличивает расход батареи. При использовании SyncAdapter система сама запускает синхронизацию в удобный для себя момент. Кратко говоря, система с определенной периодичностью включает радиомодуль и синхронизирует данные во всех приложениях, которые используют SyncAdapter. Это намного эффективнее в плане расхода заряда батареи, чем если бы каждое приложение само решало, когда нужно использовать сеть.
  • Контроль за состоянием. Это подразумевает как контроль за состоянием сети и автоматическую синхронизацию при появлении интернета, так и повторную синхронизацию, если не удалось синхронизировать данные в прошлый раз.
  • Планирование синхронизации. Можно настроить параметры и расписание для синхронизации, которые будут учтены системным планировщиком.
  • Свой аккаунт для приложения и возможность для пользователя синхронизировать данные вручную в системных настройках приложения.

SyncAdapter – это удобный способ для синхронизации данных. Но его не так часто используют по нескольким причинам:

  • Не всем приложениям нужна периодическая синхронизация данных. Более того, она не нужна подавляющему большинству приложений. Большинство приложений работают, если можно так выразиться, в сеансовом режиме, то есть обновляют данные и шлют запросы к серверу только во время активной работы. А вместе с пуш-сообщениями, модель данных без синхронизации покрывает требования очень большого числа приложений. Исключениями могут быть, к примеру, приложения для работы с финансами или погодные приложения.
  • Неудобное взаимодействие с данными. К сожалению, это так. Обновление данных через SyncAdapter с последующим чтением их из ContentProvider требует гораздо больше усилий, чем прямое обновление данных в UI-классах.

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

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

Практика

  1. Скачайте Проект SimpleWeatherPatternA. Описание задачи в файле ru.gdgkazan.simpleweather.screen.weatherlist.WeatherListActivity
  2. Нужно получить список городов и сохранить его в базу http://openweathermap.org/help/city_list.txt
  3. Получить информацию о погоде во всех городах одним запросом http://openweathermap.org/current#severalid
  4. Вся работа должна выполняться в сервисе (его нужно доработать)
  5. Реализуйте обновление данных через SwipeRefreshLayout
  6. Реализуйте обработку пересоздания Activity

Ссылки на исходный код ваших решений вы можете оставлять в комментариях. Делитесь вашими решениями с сообществом, получайте обратную связь и конструктивную критику. Лучшие решения будут опубликованы на нашем канале и сайте fandroid.info с указанием авторства победителей!

Ссылки и полезные ресурсы

  1. Приложения из репозитория:
    1. SimpleWeatherPatternA – реализация паттерна A для загрузки погоды и практическое задание.
    2. WeatherSyncAdapter – шаблон для практического задания по SyncAdapter.
  2. Документация по сервисам, в том числе и по bound services.
  3. Статья про REST. Еще одна статья про REST.
  4. Документация по ContentProvider.
  5. Статья по созданию своего ContentProvider на базе SQLite.
  6. Пример библиотеки, которая использует API ContentProvider с тестами.
  7. Непосредственно изложение паттернов A/B/C в оригинале.
  8. Хорошая статья с подробным разбором паттернов и библиотек, которые их реализуют.
  9. Статья про паттерн B.
  10. Реализация своего SyncAdapter.
  11. Статья про использование SyncAdapter для обновления данных.

Продолжение: Знакомство с RxJava

[:en]The second lecture on the architecture of client-server android-applications, in which we will consider such concepts as REST-architecture, ContentProvider, Patterns A, B and C and different approaches to implement interaction with the network.

Introduction

At the last lecture, we looked at ways to handle the re-creation of the Activity and the problem of saving data during re-creation. But there is another factor here, because the Activity can not be recreated when the query is executed, but in general it is destroyed. And it threatens with some problems. First of all, you may have an out-of-sync on the server and the client. The client will execute the request, the server will accept it and return the result with new data, which the client can no longer receive (due to the fact that the application was closed). When the client opens the application the next time, for example, offline, he will see the old information that he does not expect to see. Secondly, it is possible that to complete the operation you need to receive one response, process it and execute a new request. Undoubtedly, this is a terrible situation and it’s never necessary to do that, but backends are different, and they are not always ideal. In this case, the situation is even worse, since the data of your application may hang in the middle of the process.

We must say at once that the problems described above do not apply to all applications. Moreover, they do not apply to most applications. But at the same time to know how to solve such problems is necessary.

There is an obvious, but quite correct solution to such problems — to use for queries a component that is not destroyed by the system, even if the user closes the application. Fortunately, there is such a component, and this is Service.

Service is a component that is designed to perform certain operations in the background and that does not contain any UI elements. A huge plus of services is that they will continue to work even when the user closes all Activity applications.

The simplest implementation of the service is as follows:

 

After that, the service needs to be registered in the manifest:

 

And you can run it like this:

 

Intent, which is passed as a parameter to the startService method, eventually gets a parameter to the onStartCommand method, where you can get all the required parameters passed.

The onBind method serves to create a different type of services, which we are not interested in in the context of the topic under consideration. One can only say that this allows you to tie the service to another component of the application (for example, to Activity) and perform various background work, including between processes. But since the tied service dies along with the component to which it is attached, it will not help us to solve the current problem. More details about its applications can be found in the documentation.

Despite the fact that the service is designed to work in the background, the Service class itself does not contain the tools to ensure asynchronous execution of tasks, and the onStartCommand method works in the main application thread. Asynchrony should be provided by the developer, using any means known to him. There is also an alternative — IntentService, which is a simple single-threaded implementation of the service. For example, to load data and execute network requests, IntentService can be used as follows:

 

Here you also need to make a small note — IntentService is single-threaded and processes all incoming requests sequentially.

It would seem that once the service is such a powerful component that is not destroyed when the configuration is changed or even when the application is closed, why is it not used for network requests always? And here we are faced with the main problem, which answers this question. Pay attention again to the onHandleIntent method:

 

Namely, the fact that this method is of type void. That is, we uploaded weather data, but how to convey this information is not entirely clear. This is the main problem of services — since they are not related to UI classes, they can not transfer data to them.

One of the standard solutions in this case is the use of various Bus, for example, EventBus, Otto or even a bus on RxJava. But such methods are unreliable, since working with Bus requires subscription and unsubscription in UI classes according to the methods of the life cycle. In addition, their use makes the code unobvious.

Unfortunately, there is no simple and good solution for this problem. And that’s why we’ll look at the patterns that were presented at the Google I / O conference in 2010.

In order to understand these patterns, we will first need to become more familiar with the concepts of REST, as well as explore the ContentProvider API, because these patterns are completely based on them.

 

REST

Probably everyone has heard such words as REST API, RESTful services and other related concepts. What is meant by these concepts? First of all, REST is not some hard-coded format for working with web services, but rather a set of principles that defines how Web standards / components such as HTTP, URI, and how they should be used should interact . These principles are designed to build the architecture of Web services.

From this set of principles, there are several key ones:

  1. Each entity must have a unique identifier – URI.
  2. Entities must be related.
  3. Standard methods should be used to read and modify data.
  4. There should be support for several types of resources.
  5. Interaction should be carried out without a state.

Most of these principles are needed only when implementing web services, of course. For the development of the client part, in particular for the mobile application, we need the 1 st and 3 rd principle.

Principle 1 says that each object must have its own unique identifier, moreover, the URI address where it can be accessed. URI (from Uniform Resources Identifier) ​​is a unique resource identifier. As we will see later, this term is widely used in Android. A unique identifier is needed to access this object and to associate it with other objects.

Principle 3 reports the use of standard methods for calling remote procedures (and changing data). These methods are well known to all:

HTTP методы

  • GET – Receiving data without changing it. This is the most popular and easy method. It only returns data, and does not change it, so on the client you do not need to worry about that you can damage the data.
  • POST is a method that imply the insertion of new records.
  • PUT is a method that involves changing existing records.
  • PATCH — a method that involves changing the identifier of existing records.
  • DELETE — a method that involves deleting records.

Knowledge of these methods is very important for our further study. And for us enough information about REST, let’s just simplify his final understanding and give his presentation. The REST API is a set of remote calls to standard methods that return data in a specific format. With this definition, we will continue to work.

ContentProvider

In this lecture we will also consider a very convenient way to work with data in Android, namely ContentProvider. ContentProvider is a class that provides a unified interface for accessing application data. This class allows you to use a single data source in your application.

ContentProvider also allows you to transfer data between applications. For example, this way you work with phone contacts, sms and other system data. Unfortunately, there is a serious error in this regard. The documentation for ContentProvider says that «we do not need to develop your own provider if you do not intend to share your data with other applications.», That is, we should not implement our ContentProvider if we are going to use it only inside Your application. Such an error is expensive, many developers after reading believe that the ContentProvider will never need them, and this is not so. So what are the advantages of this class, and why should we need it?

First, as already mentioned, ContentProvider provides a unified interface for accessing data. The unified here means both independent of the implementation (and this will help you very much in cases where you decide to change the implementation of your storage), and covering all the necessary cases.

Secondly, you do not need to manage the object lifecycle to access the data (for example, the SQLiteDatabase instance). After all, in the case of direct use of such objects, there are many questions: where to store this object? When to close the database? When to destroy this object? ContentProvider allows you not to worry about such things. In addition, it allows you to access data from any location where the context of the application is available (Context instance).

And thirdly, ContentProvider fully complies with the concepts of REST, which we examined. This is good because it allows us to use it for our purposes (the spoiler — ContentProvider can act as a front for requests to the server), and this will be discussed in more detail later. In the meantime, let’s move on to working with ContentProvider.

Because ContentProvider conforms to the principles of REST, for each entity it has its own URI. ContentProvider has a base URI, which is defined by the application creator and registered in the manifest. It looks something like this:

 

To access data through the ContentProvider, we need a URI for this data (by the URI, you can access both the data group and the individual object). The application’s ContentProvider always defines a base URI that is formed from authorities in the manifest and the content: // prefix. Therefore, in our case, the base URI will be:

 

Next, suppose we want to create a group in which weather information will be stored (within the database it will be a table). Then the URI for this group should look like this:

 

If we access data in the ContentProvider using this URI, we get all the instances stored in this group.

Finally, if we need a URI for an individual object, it will look like this:

 

Where 4 is the number of the added instance.

Working with the URI is very important. The ContentProvider API allows you to monitor changes in data for a specific URI, which is extremely convenient, since it makes it easy to organize automatic updating of data in UI classes.

But in general, there is nothing complicated and new here, these are the standard means of forming a URI (and in fact a URL is a special case of a URI, so this principle is perfectly familiar to everyone). Therefore, we end with the URI and proceed to the implementation of our ContentProvider.

Note: In the future, within the framework of this lecture, we will work with a special library that is based on ContentProvider, supports a table model and allows you to perform all the necessary functions, including subscribing to notifications of changes in the table. This hides some unnecessary details of implementation, allowing you to concentrate entirely on the essence of the lecture. The description and code of the library are available at the link above. All examples will be based on this library.

To implement your ContentProvider, you need to create a class that will inherit from the ContentProvider and override all required methods. Consider these methods in order. First, it’s the onCreate method, in which you need to initialize all the fields that you need to work:

 

What does the code do in this method? First, it creates a basic URI for further access to the data in the ContentProvider. Secondly, it creates a data schema in the ContentProvider (in the simplest case, it adds tables to the database). Finally, in this method, an instance of SQLiteOpenHelper is created, through which access to the data store will be organized.

The next method is the getType method. This method must return the data type that is contained in the URI passed as the parameter. Typically, this method is used to map the table name and URI, so that you can then execute the query to the database. In our case, the implementation looks simple, but we will not go deeper (this can always be done by looking at the library code):

 

And then the methods that are directly intended for working with data begin. These are methods that completely correspond to HTTP methods, which we will use in the future. In the meantime, let’s see the implementation of these methods. In most cases, these methods only redirect the call to the SQLite database. First of all, this is the query method (which corresponds to the HTTP GET method):

 

This method first checks if the table is called, and if so, calls the query method of the SQLiteDatabase object, which, based on all parameters, builds an SQL query and returns the data.

Next is the insert method, which is used to add an element to the ContentProvider and the corresponding HTTP method to POST:

 

Pay attention to the following call:

 

The SQLite database does not know anything about such things as URI, it’s used to working with identifiers in a different form, for example, as an id. Fortunately, the built-in Android tools allow you to easily combine the use of ContentProvider and SQLite.

The next necessary method for overriding is the delete method and is used to delete data from the ContentProvider. And by the way, this is the only method of ContentProvider, HTTP analog has the same name. The implementation of this method is analogous to all the others:

 

And the last method that you need to override for implementing ContentProvider is the update method (in the terminology of the HTTP methods — PUT or PATCH):

 

In addition to these methods, the method of bulkInsert (which unlike previous methods is not mandatory for implementation) is often redefined. This method is for inserting an array of elements, and its default implementation calls the insert method for each element, which is inefficient. Therefore, this method is usually redefined to perform all inserts within a single transaction:

 

That’s all! We created our ContentProvider, added it to the manifest, and now we can access it as an interface for working with data. There is a small subtlety here — we created a ContentProvider object, but we need to access the data through the ContentResolver object, which can be obtained via the getContentResolver method in the Context class:

It is impossible not to say the following remark. Most modern libraries for working with a database provide you with a much more convenient interface to work than SQL queries to the database. At the same time, they either hide their work with ContentProvider under their implementation, or work directly with database instances (and in that case they certainly do not support notifications about changing the tables or even more specific records). Therefore, we do not need to deeply study the principles of ContentProvider work, but there is enough understanding of the relationship between ContentProvider and REST architecture and the principles of operation of the main methods.

 

Patterns A / B / C

After the previous section, it became clear how the principles of REST and ContentProvider are related to each other, it remains only to understand how they apply to our task.

The idea is very simple — ContentProvider will act as the intermediary between the service and UI-classes, as its ideology and methods completely coincide with the REST architecture. Access to the data in the ContentProvider can be obtained from any location where the context of the application is available, that is, from the service and from the UI classes. And, along with the ability to track changes to specific entities or tables through the URI ContentProvider, you can provide an incredibly high-quality and convenient (in comparison with other ways of interaction between services and UI-classes) the level of interaction of these components.

Паттерн A

Паттерн А

In general, the architecture of pattern A can be described by the following sequence of actions:

  1. The UI class starts the service to execute the query and subscribes to the data change in the table.
  2. The service executes the query, saves the data to the database, and notifies the table change.
  3. The UI class receives a notification about changing the data in the table, reads the new data and displays it to the user.

This sequence of actions can be represented in the form of the following diagram:

In this case, there are many intermediate actions, which will be discussed later. In the meantime, let’s implement such a simple approach. First of all, we modify the service for loading data, so that it stores data in the database and notifies subscribers about this:

 

When the service loads the data, the subscribed class will receive a notification about it that it can process:

 

Such a decision does not look particularly complicated, but in fact here it is necessary to solve a lot of questions and problems. First, we must correctly handle various life-cycle events and unsubscribe from notifications. That is, we did not leave here the problems that were considered in the framework of the previous lecture. Of course, these problems can be solved by the same means, but I would like that these problems were not at all.

Secondly, you need to be able to handle errors. In the example above, we looked, if there are no records in the database, then an error occurred. Of course, in any more serious example, this option does not work.

And thirdly, more precisely, developing the second point, we need to be able to track the status of each request to show the download process and hide it, and also to not re-run an already running query.

That is why the example above is naive and the simplest implementation of pattern A. In fact, in the original presentation of the pattern, everything is more complicated, but this example was needed to show why these difficulties are needed.

First of all, we need to create a class that will be responsible for the status and information about each request. This will allow us, and track status, and handle errors. This class must contain the request ID, request status and error body (which will be empty if the request is successful):

 

Create a table for this class. After that, in UI-classes we will always subscribe to changes of this table with the status (ideally, of course, it should be subscription to a separate element, but it is more difficult to implement).

After that we rework the service for queries as follows (due to the fact that the code has significantly increased, we will quote it in parts):

 

First, we get the current record of requesting weather information. If this request is IN_PROGRESS, that is, already in the process of execution, then we ignore this new call.

After that we translate the request into the IN_PROGRESS status and notify the subscribers (this is necessary for the UI classes to display the download process).

Let’s continue the consideration of the method:

 

Here everything is simple. We get the result from the server, store them in the database and depending on the success / failure when retrieving the data we translate the status either in SUCCESS or in ERROR. And notify subscribers about the completion of the request.

The UI part can process these notifications as follows (here RxJava tools are used to provide asynchrony when working with the database.) RxJava will be considered in the next lecture, but in the meantime we will comment on the code in detail)

 

When we receive a notification about changing the data in the table, we read the status of the request from the database. After that, we choose the actions depending on this status:

  • If the status is IN_PROGRESS, then we show the boot process
  • If the status is ERROR, then the received error is rolled up and displayed.
  • If the status is SUCCESS, then we read the information about the city and send it to the subscriber, who will show this information.

Thus, we can track the changes in the status of the request and correctly respond to these changes. Of course, as already mentioned, it will be ideal to track not all the table with queries, but each request separately.

But we still need to try to handle life cycle events so that we do not restart the service every time we recreate Activity. To do this, you can use the different approaches discussed in the first lecture, for example, loaders.

Summarizing the subtotal, we can say that the pattern A has 2 main advantages:

  • Guaranteed execution of all requests regardless of the life cycle of UI classes. [Wpanchor id = «4»]
  • Support for a single data source in the application.

But he, of course, has problems, but all of them can be solved. True, this can greatly complicate the architecture of the application.

Паттерн B

Pattern A, which we examined, of course, has its drawbacks. This is a lot of code, and the need to use data-based work on the basis of ContentProvider, and the task of processing the life cycle, and others. Therefore, other variants of client-server interaction are offered, which lack some of these drawbacks (although frankly, they have other drawbacks).

Pattern B in terms of ideological blocks is very similar to pattern A, it also uses Service and ContentProvider, but the order of their use is diametrically opposed. If the A ContentProvider pattern was used as an auxiliary layer for interaction between the service performing network requests and UI classes, then in the B pattern, the UI classes work exclusively and always with the ContentProvider API. And already other classes synchronize the data from the ContentProvider with the data from the server. Therefore, for a given pattern, the ContentProvider API is generally a local version of the server part with all the ensuing consequences.

Pattern B pattern is as follows:

Паттерн B

In this pattern there is an incredibly significant plus — all work with the «network» is carried out locally, and this gives us the confidence that any query will be executed if not instantly, then very quickly. Therefore, we get rid of long operations and waiting, as well as many problems in the processing of the life cycle. In addition, our application will work without any delays, which will create a very good impression for the user.

But you have to pay for everything. And it is clear that for such a serious plus to pay will also be very serious. What is it? First, you will always risk getting a situation where your local data is not synchronized with the data on the server, which means that the operations that the user has performed can be canceled and the user does not even understand the reasons.

Secondly, in order to somehow protect yourself from too frequent errors on the server side, you need to transfer at least part of the server logic to the application. And this at least means duplication of code and logic (the double possibility to be mistaken), and in most cases the server part is a huge system that is extremely irrationally transferred to the client side. [Wpanchor id = «5»]

Unfortunately, we must admit that such a pattern, in connection with its drawbacks, has too great disadvantages to apply it in practice. Exceptions may be situations where you have a small and simple application without complex server logic and when you can be sure that all the logic on the client side will be enough. Such cases are rare, but in them the pattern B will give a tangible gain in the speed of the application, so it must be borne in mind.

Паттерн С

And the last pattern under consideration is pattern C, which in fact is a modification of the previous pattern and very similar to it. The difference is that the SyncAdapter class is not used for synchronization. As you can guess from the name, SyncAdapter is designed to synchronize data. It has a number of advantages over services:

  • Saves battery power. When the user starts the application, you start pulling requests to the network, the system is forced to use the radio module at full power, which increases the battery consumption. When using the SyncAdapter, the system starts the synchronization itself at a convenient time. Briefly, the system periodically switches on the radio module and synchronizes the data in all applications that use the SyncAdapter. This is much more efficient in terms of battery power consumption than if each application itself decided when to use the network.
  • Control over the state. This implies both monitoring the network status and automatic synchronization when the Internet appears, and re-synchronization, if it was not possible to synchronize the data last time.
  • Scheduling synchronization. You can configure the parameters and the schedule for synchronization, which will be taken into account by the system scheduler.
  • Your account for the application and the ability for the user to synchronize data manually in the system settings of the application.

SyncAdapter is a convenient way to synchronize data. But it is not often used for several reasons:

  • Not all applications need periodic data synchronization. Moreover, it is not needed by the vast majority of applications. Most applications work, so to speak, in a session mode, that is, they update the data and send requests to the server only during active work. And along with the push messages, the data model without synchronization covers the requirements of a very large number of applications. Exceptions can be, for example, applications for working with finances or weather applications.
  • Inconvenient interaction with data. Unfortunately this is the case. Updating data via SyncAdapter and then reading it from ContentProvider requires much more effort than directly updating data in UI classes.

Implementing your SyncAdapter is not difficult, so we will omit the consideration of this topic. Detailed information can be found on the links at the end of the lecture.

The concrete implementation of each of the patterns examined depends very much on the application that you are developing. The main thing is that you understand the main idea when organizing such a method of client-server interaction, because in this case it will be easy for you to refine these patterns for your application.

Practice

  1. Download the SimpleWeatherPatternA Project. Description of the task in the file ru.gdgkazan.simpleweather.screen.weatherlist.WeatherListActivity
  2. You need to get a list of cities and save it to the base http://openweathermap.org/help/city_list.txt
  3. Get information about the weather in all cities by one request http://openweathermap.org/current#severalid
  4. All work must be performed in the service (it needs to be finalized)
    Implement update of data via SwipeRefreshLayout
  5. Implement re-create Activity

Links to the source code of your solutions you can leave in the comments. Share your decisions with the community, get feedback and constructive criticism. The best solutions will be published on our channel and the website fandroid.info with the authorship of the winners!

Links and useful resources

  1. Applications from the repository:
    — SimpleWeatherPatternA is a realization of the pattern A for downloading weather and a practical task.
    — WeatherSyncAdapter — a template for a practical task on the SyncAdapter.
  2. Documentation for services, including for bound services.
  3. Article about REST.
  4. Documentation for ContentProvider.
  5. Article to create your ContentProvider on the basis of SQLite.
  6. An example library that uses the ContentProvider API with tests.
  7. Directly outline the A / B / C patterns in the original.
  8. A good article with a detailed analysis of the patterns and libraries that implement them.
  9. Article about the pattern B.
  10. Implementing your SyncAdapter.
  11. Article about using SyncAdapter for updating data.

Continued: Introduction to RxJava

[:]

4 thoughts on “[:ru]Лекция 2 Курса по архитектуре андроид-приложений. Паттерны A/B/C [:en]Lecture 2 Course on the architecture of android applications. Patterns A / B / C[:]

    • В комментариях под видео на канале посмотрите пример выполнения задания

Добавить комментарий