Писать парсеры всегда долго и неинтересно. Особенно для XML — большая куча однообразного неприятного кода. Бывает, что это надоедает и начинаешь пытаться упростить себе жизнь.
Один из способов оптимизировать время разработки — использование библиотек десериализации. API обычно используют форматы JSON и XML. Для первого есть годная библиотека Gson, для второго Simple. Рассмотрим обе.
Собственно, смысл у них обеих один и тот же: разобрать заданную строку в объектную модель. Работа обеих основана на Reflection, что не лучшим образом сказывается на производительности. Обе предоставляют аннотации для маппинга полей, успешно разбирают как объекты, так и списки.
Особенности работы библиотек — не тема данной статьи. Здесь мы будем рассматривать, как скрестить их с Volley, чтоб потом быстро запросы писать и горя не знать. Для этого напишем несколько реализаций нашего любимого класса Request
(вернее, написанного в прошлой части ExtendedRequest
).
Не забудьте самостоятельно подключить библиотеки Gson и/или Simple к проекту.
GsonRequest
Вот обёртка номер раз — для ответов в формате JSON. Переопределяем у неё parseNetworkResponse
и там пытаемся разобрать полученный ответ с помощью Gson.
GsonRequest.java
public class GsonRequest<T> extends ExtendedRequest<T> { private Class<T> mResponseType; public GsonRequest(Class<T> responseType, int method, String url, Response.Listener<T> listener, Response.ErrorListener errorListener) { super(method, url, listener, errorListener); mResponseType = responseType; } @Override protected Response<T> parseNetworkResponse(NetworkResponse response) { String jsonString; try { jsonString = getResponseString(response); } catch (UnsupportedEncodingException e) { return Response.error(new VolleyError(e)); } Gson gson = new Gson(); T result = gson.fromJson(jsonString, mResponseType); return Response.success(result, null); } }
Да, класс T
никак нельзя вычислить, так что его нужно передавать в параметрах конструктора. Меня тоже это огорчает.
Кода довольно мало, и ничего супер-сложного в нём нет. Рассмотрим пример использования.
Пример
Рассмотрим снова поиск на IMDB, но немного другой запрос. В прошлый раз мы писали поиск по фильмам, который возвращал список с краткой информацией, а сейчас будем получать всего один элемент, но зато в подробностях. Адрес у нас будет иметь вид:
http://www.omdbapi.com/?i=tt0436992
Ответ же будет примерно таким:
{ "Title": "Doctor Who", "Year": "2005", "Rated": "TV-PG", "Released": "26 Mar 2005", "Runtime": "1 h", "Genre": "Adventure, Drama, Family, Sci-Fi", "Director": "N/A", "Writer": "N/A", "Actors": "Matt Smith, David Tennant, Jenna Coleman, Karen Gillan", "Plot": "The further adventures of the time traveling alien adventurer and his companions.", "Poster": "http://ia.media-imdb.com/images/M/MV5BMTU1NDExNDg1MV5BMl5BanBnXkFtZTcwMjM0OTY5Ng@@._V1_SX300.jpg", "imdbRating": "8.7", "imdbVotes": "83,304", "imdbID": "tt0436992", "Type": "series", "Response": "True" }
Пишем объектную модель, подготовленную для Gson:
FilmDetails.java
public class FilmDetails { @SerializedName("Title") private String mTitle; @SerializedName("Year") private int mYear; @SerializedName("Genre") private String mGenre; @SerializedName("Actors") private String mActors; @SerializedName("Plot") private String mSummary; @SerializedName("imdbRating") private float mRating; … }
Отправляем запрос:
String url = "http://www.omdbapi.com/?i=" + id; GsonRequest<FilmDetails> request = new GsonRequest<FilmDetails>(FilmDetails.class, Request.Method.GET, url, new Response.Listener<FilmDetails>() { @Override public void onResponse(FilmDetails response) { // отображение результата } }, new Response.ErrorListener() { @Override public void onErrorResponse(VolleyError error) { // обработка ошибки } } ); mRequestQueue.add(request);
Вот и всё! И никаких парсеров!
SimpleXmlRequest
Пишем аналогичную обёртку.
SimpleXmlRequest.java
public class SimpleXmlRequest<T> extends ExtendedRequest<T> { private final Class<T> mResponseType; private Matcher mMatcher; public SimpleXmlRequest(Class<T> responseType, int method, String url, Response.Listener<T> listener, Response.ErrorListener errorListener) { super(method, url, listener, errorListener; mResponseType = responseType; } public void setMatcher(Matcher matcher) { mMatcher = matcher; } @Override protected Response<T> parseNetworkResponse(NetworkResponse response) { String responseString; try { responseString = getResponseString(response); } catch (UnsupportedEncodingException e) { return Response.error(new VolleyError(e)); } Serializer serializer = mMatcher == null ? new Persister() : new Persister(mMatcher); try { T result = serializer.read(mResponseType, responseString); return Response.success(result, HttpHeaderParser.parseCacheHeaders(response)); } catch (Exception e) { return Response.error(new VolleyError(e)); } } }
Насчёт Matcher-ов. Это нужно для того, чтобы разобрать объект такого класса, для которого аннотации не спасут (например, Calendar
). Про эти особенности можно почитать в специальной статье.
Пример
Публичный XML API для примера найти уже сложнее. Возьмём MediaWiki (пренеприятнейшие, кстати, что API, что документация).
Поисковый запрос выглядит так: http://ru.wikipedia.org/w/api.php?action=opensearch&search=Архангельск&format=xml. Ответ — примерно так (привожу сокращённый вариант):
<SearchSuggestion xmlns="http://opensearch.org/searchsuggest2" version="2.0"> <Query xml:space="preserve">Архангельск</Query> <Section> <Item> <Image source="http://upload.wikimedia.org/wikipedia/ru/thumb/…" width="50" height="36"/> <Text xml:space="preserve">Архангельск</Text> <Description xml:space="preserve">Арха́нгельск — город на севере европейской части России. Административный центр Архангельской области и Приморского муниципального района, образует городской округ Архангельск.</Description> <Url xml:space="preserve">http://ru.wikipedia.org/wiki/%D0%90%D1%80%D1%85%D0%B0%D0%BD%D0%B3%D0%B5%D0%BB%D1%8C%D1%81%D0%BA</Url> </Item> <Item> <Image source="http://upload.wikimedia.org/…" width="50" height="31"/> <Text xml:space="preserve">Архангельская область</Text> <Description xml:space="preserve">Арха́нгельская о́бласть — область на севере Европейской части России. В её состав входит Ненецкий автономный округ.</Description> <Url xml:space="preserve"> http://ru.wikipedia.org/wiki/%D0%90%D1%80%D1%85%D0%B0%D0%BD%D0%B3%D0%B5%D0%BB%D1%8C%D1%81%D0%BA%D0%B0%D1%8F_%D0%BE%D0%B1%D0%BB%D0%B0%D1%81%D1%82%D1%8C </Url> </Item> … </Section> </SearchSuggestion>
Объектная модель:
Подготавливать объектную модель для Simple немного сложнее. К счастью, у меня про это тоже есть статья.
SearchResult.java
@Root(name = "Item", strict = false) public class SearchResult { @Path(value = "Image") @Attribute(name = "source") private String mImageUrl; @Element(name = "Text") private String mTitle; @Element(name = "Description", required = false) private String mDescription; @Element(name = "Url") private String mUrl; … }
SearchSuggestion.java
@Root(strict = false) public class SearchSuggestion { @ElementList(inline = false, name = "Section") private List<SearchResult> mSearchResults; public List<SearchResult> getSearchResults() { return mSearchResults; } }
Отправка запроса:
String url = String.format("http://ru.wikipedia.org/w/api.php?action=opensearch&search=%s&format=xml", Uri.encode(getSearchString())); SimpleXmlRequest<SearchSuggestion> request = new SimpleXmlRequest<SearchSuggestion>(SearchSuggestion.class, Request.Method.GET, url, new Response.Listener<SearchSuggestion>() { @Override public void onResponse(SearchSuggestion response) { // отображение результата } }, new Response.ErrorListener() { @Override public void onErrorResponse(VolleyError error) { // обработка ошибки } }); mRequestQueue.add(request);
С разбором результата на этом всё. Дальше пишем логику, UI и всё такое прочее.
Обработка ошибок
Как мы уже говорили в первой части, сервера иногда склонны возвращать ошибки. Иногда ошибка обозначается статусом ответа (приходит не 200 OK
, а что-нибудь другое. Volley это ловит и вызывает соответствующий callback), но довольно часто они являются частью API. То есть при ошибке сервер присылает 200 OK
, но в ответе не желанные нам данные, а какое-нибудь ругательство вроде:
{error: 2, message:"Server did a boo boo"}
Но и по этому поводу есть у меня реквесты. Рассмотрим их вместе, ибо они почти одинаковые.
GsonWithErrorRequest
GsonWithErrorRequest.java
public class GsonWithErrorRequest<T, E extends ApiError> extends ExtendedRequest<T> { private Class<T> mResponseType; private Class<E> mApiErrorType; public GsonWithErrorRequest(Class<T> responseType, Class<E> errorType, int method, String url, Response.Listener<T> listener, Response.ErrorListener errorListener) { super(method, url, listener, errorListener); mResponseType = responseType; mApiErrorType = errorType; } @Override protected Response<T> parseNetworkResponse(NetworkResponse response) { String jsonString = null; try { jsonString = getResponseString(response); } catch (UnsupportedEncodingException e) { return Response.error(new VolleyError(e)); } Gson gson = new Gson(); try { E error = gson.fromJson(jsonString, mApiErrorType); if (error != null && error.isReasonable()) { return Response.error(error); } } catch (Exception ignored) { } T result = gson.fromJson(jsonString, mResponseType); return Response.success(result, HttpHeaderParser.parseCacheHeaders(response)); } }
SimpleXmlWithErrorRequest
SimpleXmlWithErrorRequest
public class SimpleXmlWithErrorRequest<T, E extends ApiError> extends ExtendedRequest<T> { private final Class<T> mResponseType; private final Class<E> mErrorType; private Matcher mMatcher; public SimpleXmlWithErrorRequest(Class<T> responseType, Class<E> errorType, int method, String url, Response.Listener<T> listener, Response.ErrorListener errorListener) { super(method, url, listener, errorListener); mResponseType = responseType; mErrorType = errorType; } public void setMatcher(Matcher matcher) { mMatcher = matcher; } @Override protected Response<T> parseNetworkResponse(NetworkResponse response) { String responseString; try { responseString = getResponseString(response); } catch (UnsupportedEncodingException e) { return Response.error(new VolleyError(e)); } Serializer serializer = mMatcher == null ? new Persister() : new Persister(mMatcher); // check if response contains error try { E error = serializer.read(mErrorType, responseString); if (error != null && error.isReasonable()) { return Response.error(error); } } catch (Exception ignored) { // In seems there is no error here } // no error – parse result try { T result = serializer.read(mResponseType, responseString); return Response.success(result, HttpHeaderParser.parseCacheHeaders(response)); } catch (Exception e) { return Response.error(new VolleyError(e)); } } }
Всё бы хорошо, но что это за ApiError
такой? А вот же он:
ApiError.java
public abstract class ApiError extends VolleyError { public abstract boolean isReasonable(); }
Это базовый класс для ошибки, возвращаемой API. Унаследован от VolleyError
, чтобы можно было возвращать его в Response.error
. Для конкретных реализаций мы будем указывать поля и аннотации к ним точно так же, как и остальным классам объектной модели, согласно установленному формату ошибки.
Метод isReasonable
очень нужен и вот почему. Десериализатор при желании из любой входной строки сможет соорудить любой объект, не выбросив никакого исключения, но просто с пустыми полями. Вот и ApiError
вполне способен образоваться даже из неошибочного ответа. Так что просто проверки на null недостаточно. Или другая история: в некоторых протоколах ошибку можно опознать только по какому-нибудь специальному id, который приходит всегда, но при успехе имеет значение 0, а при ошибке что-нибудь другое. Так что хорошо бы иметь возможность определить собственную логику проверки состоятельности ошибки.
Пример
Пример будет только для JSON-ответа, для XML ровно то же самое. Снова будем мучить наш поиск на IMDB, благо объектная модель готова, и ошибки приходят именно по описанной схеме. Например, если по запросу ничего не найдено, вернётся такое:
{"Response":"False","Error":"Movie not found!"}
Определяем класс для ошибки:
ImdbError.java
public class ImdbError extends ApiError { @SerializedName("Error") private String mErrorMessage; @Override public boolean isReasonable() { return !TextUtils.isEmpty(mErrorMessage); } @Override public String getMessage() { return mErrorMessage; } }
Создаём и запускаем запрос:
String url = "http://www.omdbapi.com/?t=" + Uri.encode(getSearchString()); GsonWithErrorRequest<FilmDetails, ImdbError> request = new GsonWithErrorRequest<FilmDetails, ImdbError>( FilmDetails.class, ImdbError.class, Request.Method.GET, url, new Response.Listener<FilmDetails>() { @Override public void onResponse(FilmDetails data) { // отображение результата } }, new Response.ErrorListener() { @Override public void onErrorResponse(VolleyError error) { // отображение ошибки } } ); mRequestQueue.add(request);
Ошибки отловлены, лишнего писать не надо, жизнь прекрасна.
(Кстати, запрос тут немного другой. В прошлый раз мы указывали параметр i
и искали по IMDB ID, а сейчас с помощью параметра t
ищем по названию)
Итак
Скрестив Volley с Gson или Simple, можно значительно сократить время разработки. Но есть и минусы:
- Дополнительные библиотеки увеличивают размер приложения. simple весит полмегабайта, gson — около 200 Кб
- Reflection! По сравнению с нативными парсерами производительность хуже на порядок. Если сервер склонен присылать большую кучу данных, лучше написать парсер старым дедовским способом
- Со сложно организованными протоколами также может не взлететь. К примеру, нередка ситуация, когда некое поле читается из нескольких ключей JSON. Вроде этого:
id = obj.has("id") ? obj.getLong("id") : obj.getLong("anotherId")
Даже такая простая вещь уже будет вызывать сложности при автоматическом маппинге.
На этом моё допиливание инфраструктуры для Volley пока приостановлено.
Исходники
Немного об организации кода.
ImdbSearchActivity
— пример наследования от классаRequest
(про что шла речь в первой части)ImdbFilmDetailsActivity
— пример использованияGsonRequest
. Запускается по тыку на результат поиска вImdbSearchActivity
WikiSearchActivity
— пример использованияSimpleXmlRequest
.ImdbSearchSingleItemActivity
— пример использованияGsonWithErrorRequest
.
Комментариев нет:
Отправить комментарий
Пожалуйста, пишите содержательные комментарии и соблюдайте вежливость.