Во многих приложениях, которые мне приходится разрабатывать, присутствует серверная часть. Так что из проекта в проект кочевали одни и те же куски копипасты для отправки/чтения запросов.
А потом появилась библиотека Volley. Теперь вместо своей копипасты можно тащить в проекты именно её. И это хорошо.
Мне уже довелось использовать Volley в своих проектах, можно и впечатлениями делиться. Речь пойдёт именно о части отправки запросов на сервер. О NetworkImageView
и прочих кэшах как-нибудь в другой раз.
- Чем мне не нравятся мануалы о Volley
- Решения некоторых часто встречающихся задач
- Volley и библиотеки десериализации
Начнём.
Чем мне не нравятся мануалы про Volley
Про Volley, что удивительно, не так много статей. Собственно, есть два основных источника знаний:
С их помощью можно получить представление о Volley, и, в частности, как отправлять запросы. Крайне рекомендуется к просмотру/прочтению.
А не нравится мне то, что такое повышенное внимание уделяется именно JsonObjectRequest
. Даже складывается впечатление, что только так и стоит работать: получать в onResponse
какой-нибудь JsonObject
, разбирать его и радостно отображать данные в нужных компонентах. Но есть пара проблем. Во-первых, onResponse выполняется в UI-потоке, и осуществлять там парсинг, особенно для достаточно больших JSON-объектов, — не самая лучшая идея. А во-вторых, на JSON тоже свет клином не сошёлся, многие сервера возвращают XML. Как быть с ним, и вовсе непонятно.
И что делать?
Мне кажется, классы JsonObjectRequest
и JsonObjectArrayRequest
были добавлены исключительно в качестве примера, и на практике их использовать не надо. Но можно обратить внимание, что они унаследованы соответственно от Request<JSONObject>
и Request<JSONArray>
. Да и в очередь запросов (то есть класс RequestQueue
) добавляются экземпляры именно Request<T>
(где T
— тип возвращаемого результата). Вот от него и надо наследоваться.
Рассмотрим конкретную задачу. Допустим, наше приложение должно искать фильмы на IMDB по куску названия. У нас есть серверная часть, которая на запрос http://www.omdbapi.com/?s=godfather возвращает JSON вот такого вида:
{ "Search": [ { "Title": "The Godfather", "Year": "1972", "imdbID": "tt0068646", "Type": "movie" }, { "Title": "The Godfather: Part II", "Year": "1974", "imdbID": "tt0071562", "Type": "movie" }, … ] }
Объектная модель
С данными удобнее работать, если они завёрнуты в объектную модель. В данном случае нужен всего один класс Film
Film.java
public class Film { public enum Type { movie, series, episode, other } private final String mId; private final Type mType; private final String mTitle; private final int mYear; public Film(JSONObject obj) throws JSONException { mId = obj.getString("imdbId"); mTitle = obj.getString("Title"); mYear = obj.getInt("Year"); Type type = Type.other; try { type = Type.valueOf(obj.getString("Type")); } catch (IllegalArgumentException ignored) { } mType = type; } }
Класс для отправки запроса
А теперь напишем класс, который будет отправлять запрос. Как было ранее сказано, он должен наследоваться от Request<T>
. Более того, так как нам должен прийти список фильмов, наследоваться надо от — от Request<List<Film>>
(или от Request<Film[]>
, тоже неплохо).
Наследникам Request<T>
необходимо реализовать два метода:
parseNetworkResponse
— разбор ответа. Отрабатывает в фоновом потоке, то есть там же, где и отправка запроса. Возвращает экземпляр классаResponse<T>
deliverResponse
— возвращение ответа, работает в UI-потоке. Здесь, очевидно, должен быть вызов некоего success callback-а
Кроме того, нужно переопределить конструктор.
Получится что-то вроде:
ImdbSearchRequest.java
public class ImdbSearchRequest extends Request<List<Film>> { private Response.Listener<List<Film>> mSuccessListener; public ImdbSearchRequest(String searchString, Response.Listener<List<Film>> successListener, Response.ErrorListener errorListener) { super(Method.GET, constructUrl(searchString), errorListener); mSuccessListener = successListener; } private static String constructUrl(String searchString) { return "http://www.omdbapi.com/?s=" + Uri.encode(searchString); } @Override protected Response<List<Film>> parseNetworkResponse(NetworkResponse response) { try { String data = new String(response.data, HttpHeaderParser.parseCharset(response.headers)); JSONObject rootObj = new JSONObject(data); if (rootObj.has("Error")) { return Response.error(new VolleyError(rootObj.getString("Error"))); } JSONArray resultsArray = rootObj.getJSONArray("Search"); List<Film> result = new ArrayList<Film>(resultsArray.length()); for (int i = 0; i < resultsArray.length(); ++i) { result.add(new Film(resultsArray.getJSONObject(i))); } return Response.success(result, null); } catch (UnsupportedEncodingException e) { return Response.error(new VolleyError(e)); } catch (JSONException e) { return Response.error(new VolleyError(e)); } } @Override protected void parseNetworkResponse(List<Film> response) { if (mSuccessListener != null) { mSuccessListener.onResponse(response); } } }
Разберёмся, что написали:
Конструктор
Для начала необходимо вызвать конструктор базового класса. Его параметры:
- method — HTTP-метод (GET, POST и экзотика)
- url — адрес, куда посылаем запрос.
- errorListener — callback для ошибки. Он есть в базовом классе
Для успешного же окончания запроса никакого callback-а в базовом классе не предусмотрено, так что в каждом запросе придется писать свой. Видимо, предполагаются и другие способы возвращения результата (отправка Intent-а, например). Но радует, что для callback-ов есть базовый класс Response.Listener<T>
.
parseNetworkResponse
Здесь идёт обычный разбор JSON. Но стоит обратить внимание на возвращаемый результат.
Метод возвращает экземпляр класса Response<T>
. Причём экземпляр создаётся с помощью статических методов error
и success
. Потом Volley сам разберётся, вызывать ли callback с ошибкой или возвращать разобранный ответ.
Кроме того, следует учитывать, что ошибка может прийти и в самом ответе. Наш сервер в ряде случаев может возвращать ответ вида:
{"Response":"False","Error":"Movie not found!"}
При разборе мы это учитываем и вначале проверяем, не содержит ли ответ ошибку.
parseNetworkResponse
Сюда мы попадаем, если запрос был успешно получен и разобран (то есть когда parseNetworkResponse
вернул Response.success(…)
).
Всё, что нужно сделать — вызвать наш callback. Если он есть.
Отправка запроса
Класс используется абсолютно так же, как и JsonObjectRequest
:
ImdbSearchActivity.java
public class ImdbSearchActivity extends ListActivity { private RequestQueue mRequestQueue; private ProgressDialog mProgressDialog; private EditText mSearchStringView; private ListView mFilmsListView; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_search); mRequestQueue = Volley.newRequestQueue(this); mProgressDialog = new ProgressDialog(this); mSearchStringView = (EditText) findViewById(R.id.search_string); … } public void searchImdb(View view) { mProgressDialog.show(); ImdbSearchRequest request = new ImdbSearchRequest(getSearchString(), new Response.Listener<List<Film>>() { @Override public void onResponse(List<Film> result) { setListAdapter(new FilmsAdapter(result)); mProgressDialog.dismiss(); } }, new Response.ErrorListener() { @Override public void onErrorResponse(VolleyError error) { Log.e(TAG, error.getMessage(), error); mProgressDialog.dismiss(); setListAdapter(null); Toast.makeText(ImdbSearchActivity.this, error.getMessage(), Toast.LENGTH_SHORT).show(); } } ); mRequestQueue.add(request); } public String getSearchString() { return mSearchStringView.getText().toString(); } … }
И onResponse
, и onErrorResponse
выполняются в UI-потоке, так что можно смело обновлять контролы прямо там.
FilmsAdapter
для краткости опущен. Посмотреть весь код класса можно на Github (имеются некоторые отличия, но суть та же). Если что, про адаптеры есть отдельная большая статья.
Результат:
Вот и всё!
Запрос отправили, ответ получили, в списке что-то отобразили. Парсинг работает в фоне, все счастливы.
А если вдруг будет API, возвращающее XML, с ним тоже ясно, что делать: запускаем в parseNetworkResponse
какой-нибудь XmlPullParser
и тоже запросто разбираем.
Проект целиком можно посмотреть на Github.
Комментариев нет:
Отправить комментарий