пятница, 29 ноября 2013 г.

О Volley немного по-другому

Во многих приложениях, которые мне приходится разрабатывать, присутствует серверная часть. Так что из проекта в проект кочевали одни и те же куски копипасты для отправки/чтения запросов.

А потом появилась библиотека Volley. Теперь вместо своей копипасты можно тащить в проекты именно её. И это хорошо.

Мне уже довелось использовать Volley в своих проектах, можно и впечатлениями делиться. Речь пойдёт именно о части отправки запросов на сервер. О NetworkImageView и прочих кэшах как-нибудь в другой раз.

Начнём.

Чем мне не нравятся мануалы про 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.

Комментариев нет: