В одном из проектов появилась необходимость написать REST-приложение под Android. Каким путем пойти, какие Android-особенности учесть, на какие библиотеки опереться в работе? Пришлось провести исследование. Результатами хочу поделиться.
Архитектура REST
Не буду останавливаться на принципах REST-архитектуры. Приведу лишь список книг и статей, где эти принципы изложены.
- A Brief Introduction to REST (2007) - замечательная статья Стефана Тилкова (Stefan Tilkov) на InfoQ, в которой кратко и точно описана REST-архитектура. Если вы не знакомы с REST, начните с нее.
- InfoQ - compilation of REST resources - сборник статей, вышедших на InfoQ, посвященных REST.
- Книга "RESTful Web Services" (2007) by Leonard Richardson, Sam Ruby. Первая книга по REST, с описанием основных принципов и преимуществ по сравнению с Web Services.
- Книга "RESTful Web APIs" (2013) by Leonard Richardson, Mike Amundsen, Sam Ruby. Новая книга, тех же авторов. Здесь акцент сделан не на основы REST, а на принципы разработки правильных REST API. Возможно скоро появится перевод это книги на русский.
- Книга "RESTful Web Services Cookbook" (2010) by Subbu Allamaraju. В интернете пишут, что в этой книге очень хорошо описана концепция REST.
- Книга "REST in Practice" (2010) by Jim Webber, Savas Parastatidis, Ian Robinson
- Диссертация Роя Филдинга (Roy Fielding), глава 5 (2000). Именно здесь было впервые введено понятие REST-архитектуры. Принципы REST изложены в общем виде, читать тяжеловато.
- REST APIs must be hypertext-driven (2008). Статья Роя Филдинга, посвященная типичным ошибкам при реализации REST-архитектуры.
- REST Anti-Patterns (2008). Статья Стефана Тилкова, посвященная типичным ошибкам при реализации 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).
Спасибо за статью! Прочитал с удовольствием! Жаль, что никто не использует Паттерн Б, на мой взгляд, он гораздо феншуйнее, придется свое видать писать.
ОтветитьУдалитьА кто нибудь находил реализация Pattern C. Как правльно работать и запускать AsyncAdpter когда ты делаешь query в БД.? До сих пор не нашел ответа. Кто нибудь знает его?
ОтветитьУдалитьА как реализовать отправку данных после перезагрузки телефона если она не удалась с первого раза? В случае Pattern A да и вообще.
ОтветитьУдалитьВ данных реализациях сервис висит в памяти и с интервалами пытается отсуществить REST запрос. Но если ему это не удастся то соответсвующие флаги в SQLite так и останутся в виде STATE_POSTING/STATE_UPDATING/STATE_DELETING?