Писать парсеры всегда долго и неинтересно. Особенно для 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. Запускается по тыку на результат поиска вImdbSearchActivityWikiSearchActivity— пример использованияSimpleXmlRequest.ImdbSearchSingleItemActivity— пример использованияGsonWithErrorRequest.
Комментариев нет:
Отправить комментарий