пятница, 17 января 2014 г.

REST под Android. Часть 1: паттерны Virgil Dobjanschi

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

Архитектура REST

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

REST-клиент под Android. Лекция Virgil Dobjanschi на Google I/O 2010

Реализация REST-клиента под Android обладает рядом особенностей. "По полочкам" все разложил, в свое время, Virgil Dobjanschi на Google I/O 2010, см. "Developing Android REST client applications" (видео, слайды, текст лекции).

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

  • Pattern А. Использовать Service API: Activity -> Service -> Content Provider. В данном варианте Activity работает с API Android Servcie. При необходимости послать REST-запрос Activity создает Service, Service асинхронно посылает запросы к REST-серверу и сохраняет результаты в Content Provider (sqlite). Activity получает уведомление о готовности данных и считывает результаты из Content Provider (sqlite).
  • Pattern B. Использовать ContentProvider API: Activity -> Content Provider -> Service. В этом случае Activity работает с API Content Provider, который выступает фасадом для сервиса. Данный подход основан на схожести Content Provider API и REST API: GET REST эквивалентен select-запросу к базе данных, POST REST эквивалентен insert, PUT REST ~ update, DELETE REST ~ delete. Результаты Activity так же загружает из sqlite.
  • Pattern C. Использовать Content Provider API + SyncAdapter: Activity -> Content Provider -> Sync Adapter. Вариация подхода "B", в котором вместо сервиса используется собственный Sync Adapter. Activity дает команду Content Provider, который переадресовывает ее в Sync Adapter. Sync Adapter вызывается из Sync Manager, но не сразу, а в "удобный" для системы момент. Т.о. возможны задержки в исполнении команд.
Основные проблемы связаны с тем, что время жизни Activity в Android совершенно непредсказуемо. В любой момент ваша Activity может быть приостановлена или даже удалена из памяти. Соответственно, какие-либо длительные операции в Activity делать нельзя, т.к. Activity запросто может "отвалиться" до завершения этих операций. В "лучшем" случае, будут потеряны результаты работы. В худшем - произойдет рассинхронизация данных на сервере и клиенте. Отсюда - необходимость все длительные операции запускать в сервисе. Сервис гораздо более живуч, чем Activity, его система без веских причин останавливать не будет.

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

  • Данные, полученные от REST-сервера, всегда сохраняются в sqlite. Напрямую в Activity они никогда не передаются. Вместо этого в Activity передается уведомление о том, что данные загружены в sqlite и их можно оттуда загрузить (вариант - Activity получает уведомление об обновлении данных в Content Provider через Content Observer).
  • При выполнении операций insert, delete, update данные в sqlite обновляются дважды: первый раз до отправки REST-запроса, второй раз - после получения результата. Первая операций выставляет информационные флаги, сигнализирующие о типе операции, проводимой над данными, и о статусе операции.
  • REST-методы следует всегда выполнять в отдельном потоке.
  • Следует использовать Apache HTTP client, а не Java URL connection.
  • Форматы данных в порядке предпочтения: какой-либо бинарный формат (например, AMF3), затем JSON, затем XML.
  • Желательно включать gzip. GZip на Android реализован "нативно", библиотека быстрая. В некоторых случаях можно получить коэффициент сжатия 5:1 и даже 10:1, в зависимости от количества получаемых данных. Использование GZip ускоряет загрузку данных и экономит батарею.
  • Если используете Sqlite - используйте транзакции.
  • Если программе требуется скачать 10-20 картинок, не стоит запускать 10-20 параллельных закачек. Запускайте 1-3, а остальные ставьте в очередь.
  • Activity регистрирует binder callback (т.е. ResultReceiver), для получения ответа от сервиса. Этот callback нужно обязательно удалить при вызове onPause у Activity, иначе можно налететь на ANR.
  • Длительные операции всегда следует запускать из сервиса. Сервис обязательно следует останавливать после того, как требуемые операции выполнены.
  • Не стоит позволять вашей базе данных расти бесконечно. При превышении лимита в 1 Мб работа приложения существенно замедлится (не актуально в Android версии 2.3 и выше.)
  • Необходимо минимизировать сетевой трафик.
  • Следует разбивать данные на страницы (конечно, если REST Api предоставляют такую возможность).
  • Для некритичной по времени синхронизации данных между клиентом и сервером рекомендуется использовать SyncAdapter.

Варианты реализации REST-клиентов на Android

Предложенные паттерны можно реализовать вручную, воспользовавшись библиотеками Android Asynchronous HTTP Client API или Volley (Google) (см. примеры в статье Experimenting a couple of Android REST APIs). Можно взять за основу готовые варианты реализации паттернов, например:

Однако, проще и удобнее будет воспользоваться одной из подходящих библиотек:

  • RoboSpice - мощная, модульная, хорошо документированная библиотека, регулярно обновляется. Pattern A + много полезных доп. функций. Детальное объяснение, как устроена библиотека: A User's Perspective on RoboSpice.
  • Datadroid, реализует pattern A, о чем явно написано в документации. Текущая версия 2.1.2
  • RESTDroid, pattern A
  • Mechanoid Ops. Mechanoid Library - это набор из четырех кодогенераторов (база данных sqlite, код для работы с длительными асинхронными запросами в отрыве от UI - Pattern A, работа с JSON, работа с Shared Preference). Текущая версия - 0.2.2 ALPHA
  • RESTProvider (не совсем ясно, развивается ли библиотека: последнее обновление - 2 года назад).
  • Retrofit. HTTP-запросы описываются через аннотации, синхронные и асинхронные вызовы REST-методов, данные могут передаваться в виде JSON, XML, Protobuf. Текущая версия 1.3, обновляется регулярно. С этой библиотекой на пару умеет работать RoboSpice.
  • PostmanLib.

Задачи, которые требуется решить при реализации REST-клиента

Перечислим, какие основные задачи придется решать при реализации REST-клиента на Android согласно паттернам Virgil Dobjanschi.
  • Управление сервисом: запуск, остановка.
  • Передача результатов из сервиса в активити.
  • Кэшировать результатов в sqlite.
  • Фиксирование статуса данных sqlite перед и после выполнения REST-запроса.
  • Запись информации о проводимых REST-операциях в sqlite.
  • Парсинг полученных данных.
  • Конструирование REST-запроса на основе URI и набора параметров.
  • Выполнение сетевых запросов к REST-серверу.
  • Чистка базы данных от устаревших данных.
  • В случае неудачи REST-запроса, пытаться повторить запрос (например, экспоненциально увеличивая время между запросами).
  • Возможность отложенного запуска REST-запроса через SyncAdapter.

Теперь посмотрим, что предлагают библиотеки. Я просмотрел исходные коды трех наиболее популярных библиотек - RoboSpice, Datadroid и RESTDroid, - и попробовал составить сравнительную таблицу их возможностей. Вот что получилось.

RoboSpice 1.4.8 Datadroid 2.1.2 RESTDroid 0.8.2
Тип паттерна A A A
Кэширование данных Кэш внешний (требуется реализовать абстрактный класс CacheManager.java). При запуске запроса можно указать, кэшировать его или нет, если кэшировать - то какое время считать результаты валидными. Среди расширений библиотеки есть ORMLite module, предназначенный для записи и считывания POJO в/из sqlite. Кэш встроенный (в RequestManager), данные хранятся в памяти в виде экземпляра LruCache. Для каждого типа запросов кэширование (вкл/выкл) настраивается отдельно. Не совсем ясно, можно ли задействовать вместо LruCache-кэша базу данных, т.к. LruCache не отключаемый. Кэш встроенный (CacheManager), результаты каждого запроса хранятся в отдельном файле. Валидность данных в кэше определяется временем создания файла. Использование кэша можно отключить, а вот запись в кэш, похоже, нельзя. Способ хранения данных можно менять путем реализации наследника PersistableFactory
Как идентифицируются типы REST-запросов. Идентификация на уровне экземпляров класса, if (this == other) ... int UUID
Слой ServiceHelper SpiceManager RequestManager WebService
Pre- и post- операции для REST-методов Встроенной поддержки нет. Можно реализовать самостоятельно в реализации наследника SpiceRequest Встроенной поддержки нет. Можно реализовать самостоятельно в реализации наследника Operation Логику пре- и пост-запросов можно изменить, создав наследника класса Processor и перекрыв в нем методы preRequestProcess, preGetRequest и т.д.
Встроенные средства парсинга результатов Функция loadDataFromNetwork в SpiceRequest<T> предусматривает реализацию парсинга. Для каждого типа запросов можно задать реализацию парсера, см. функцию getOperationForType в классе PoCRequestService в демонстрационном приложении. Предусмотрен парсинг результатов через parseToObject в Processor, парсер задается через фабрику классов.
Передача данных из сервиса в Activity Через RequestListener. Тип результата кастомизируется через generic-параметр. Listener передается в качестве параметра в spiceManager.execute, результаты приходят в распарсенном виде. Через RequestListener. Результаты загружаюстя в Bundle. Listener передается в качестве параметра в RequestManager.execute. Результаты приходят в виде bundle. Через RequestListeners можно получить код результата. В случае успеха, готовый распарсенный объект можно загрузить через метод getResource из RESTRequest
Реализация REST-запросов Работа с сетью кастомизируется наследниками SpiceRequest (функция loadDataFromNetwork). В библиотеку входит реализация SpiceRequest по умолчанию, на основе: java.net.URL для текстовых данных, HttpURLConnection для бинарных данных Для сетевой работы используется OkHTTP from Square или HttpURLConnection. Вначале идет попытка инициализировать okHttpClient через java.lang.reflect, в случае исключения используется стандартный HttpURLConnection. Для сетевой работы используется DefaultHttpClient (на Gingerbread и выше предпочтительнее использовать HttpURLConnection, а не DefaultHttpClient)
Автоматический повтор запроса в случае неудачи Есть. Алгоритм повтора настраивается через RetryPolicy. По умолчанию используется DefaultRetryPolicy, реализующий "exponential back off"-алгоритм. Нет. В случае ошибки при выполнении запроса "наверх" отправляется исключение с информацией о типе проблемы, см. RequestService Автоматический повтор через заданные интервалы времени, по умолчанию, 1 минута.
Поддержка GZip Реализуется путем кастомизации класса SpiceRequest, см. пример robospice-motivations. Встроена, настраивается через setGzipEnabled в NetworkConnection нет(?), см. HttpRequestHandler.java
Конструктор REST-запросов Все стандартные реализации SpiceRequest<T> принимают в качестве параметра конструктора уже готовую URL, которую необходимо конструировать самостоятельно. Класс Request позволяет задать параметры запроса. Класс NetworkConnectionImpl реализует сборку URL для REST-запроса. Класс RESTRequest принимает уже готовую URL, которую необходимо конструировать самостоятельно.
Уведомления в UI thread о ходе выполнения операции Встроена, см. SpiceNotificationService нет? нет?
Многопоточность при отправке REST-запросов Да. Размер пула потоков настраивается путем перекрытия функции getThreadCount в SpiceManager, по умолчанию 3. Да. Размер пула потоков настраивается путем перекрытия функции getMaximumNumberOfThreads в RequestService, по умолчанию 1. Да. Размер пула потоков настраивается константой в классе WebService, по умолчанию 10.
Встроенная поддержка SyncAdapter Нет. Нет. Нет.
Минимальная версия Android SDK 8 (Froyo / 2.2.x) 8 (Froyo / 2.2.x) 8 (Froyo / 2.2.x)
Примеры приложений На GitHub отдельный репозиторий RoboSpice-samples с примерами. Демонстрационное приложение DataDroidPoC распространяется вместе с исходниками. Демонстрационного примера нет. Страница документации RESTDroid guide в настоящий момент не доступна.
Наличие unit-тестов В описании библиотеки на git сказано про 160 тестов. нет нет

Выводы

  • RoboSpice - крайне гибкая библиотека. Хорошо подходит в качестве фундамента для реализации "собственного велосипеда". Кастомизация на основе generic и policy, общие решения, минимум ограничений для разработчика.
  • Datadroid и RESTDroid - неплохие попытки реализовать готовые REST-библиотеки в стиле "бери и пользуйся". Каждая - со своими преимуществами и ограничениями.

Mindmap, созданный в процессе работы над этой статьей, можно скачать здесь, в формате Freemind 1.0 (портативная версия: freemind-bin-1.0.0.zip).

3 комментария:

  1. Спасибо за статью! Прочитал с удовольствием! Жаль, что никто не использует Паттерн Б, на мой взгляд, он гораздо феншуйнее, придется свое видать писать.

    ОтветитьУдалить
  2. А кто нибудь находил реализация Pattern C. Как правльно работать и запускать AsyncAdpter когда ты делаешь query в БД.? До сих пор не нашел ответа. Кто нибудь знает его?

    ОтветитьУдалить
  3. А как реализовать отправку данных после перезагрузки телефона если она не удалась с первого раза? В случае Pattern A да и вообще.
    В данных реализациях сервис висит в памяти и с интервалами пытается отсуществить REST запрос. Но если ему это не удастся то соответсвующие флаги в SQLite так и останутся в виде STATE_POSTING/STATE_UPDATING/STATE_DELETING?

    ОтветитьУдалить