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

Решения некоторых часто встречающихся задач с Volley

Наши серверные коллеги тоже люди, так что API у них получаются разные. Кто-то передаёт параметры POST-ом, у кого-то работает gzip-компрессия, кто-то возвращает ошибки статусами, а у кого-то для них отдельный протокол. Попробую собрать наиболее частые вопросы и предложить решения.

Сделаю так. Приведу код класса ExtendedRequest<T> — наследника Request<T>, в котором решены все назревающие вопросы, и объясню, где именно и как они решаются. Соответственно, дальнейшие реквесты будет иметь смысл наследовать именно от него.

ExtendedRequest.java

public abstract class ExtendedRequest<T> extends Request<T> {
    private static final String HEADER_ENCODING = "Content-Encoding";
    private static final String HEADER_USER_AGENT = "User-Agent";
    private static final String HEADER_ACCEPT_ENCODING = "Accept-Encoding";
    private static final String ENCODING_GZIP = "gzip";

    protected Map<String, String> mParams;

    private String mUserAgent;
    private boolean mGzipEnabled = true;

    protected final Response.Listener<T> mSuccessListener;

    public ExtendedRequest(int method, String url, Response.Listener<T> successListener, Response.ErrorListener errorListener) {
        super(method, url, errorListener);
        mSuccessListener = successListener;
    }

    @Override
    protected void deliverResponse(T response) {
        if (mSuccessListener != null) {
            mSuccessListener.onResponse(response);
        }
    }

    protected String getResponseString(NetworkResponse response) throws UnsupportedEncodingException {
        String responseString = null;
        String charset = HttpHeaderParser.parseCharset(response.headers);

        if (mGzipEnabled && isGzipped(response)) {
            try {
                byte[] data = decompressResponse(response.data);
                responseString = new String(data, charset);
            } catch (IOException e) {
                // it seems that result is not GZIP
            }
        }

        if (responseString == null) {
            responseString = new String(response.data, charset);
        }

        return responseString;
    }

    private boolean isGzipped(NetworkResponse response) {
        Map<String, String> headers = response.headers;
        return headers != null && !headers.isEmpty() && headers.containsKey(HEADER_ENCODING) &&
            headers.get(HEADER_ENCODING).equalsIgnoreCase(ENCODING_GZIP);
    }

    protected byte[] decompressResponse(byte [] compressed) throws IOException {
        ByteArrayOutputStream baos = null;
        try {
            int size;
            ByteArrayInputStream memstream = new ByteArrayInputStream(compressed);
            GZIPInputStream gzip = new GZIPInputStream(memstream);
            final int buffSize = 8192;
            byte[] tempBuffer = new byte[buffSize];
            baos = new ByteArrayOutputStream();
            while ((size = gzip.read(tempBuffer, 0, buffSize)) != -1) {
                baos.write(tempBuffer, 0, size);
            }
            return baos.toByteArray();
        } finally {
            if (baos != null) {
                baos.close();
            }
        }
    }

    /**
     * Sets parameters map
     * @param params Parameters map
     */
    public void setParams(Map<String, String> params) {
        mParams = params;
    }

    /**
     * Adds POST parameter
     * @param key Parameter name
     * @param value Parameter value
     */
    public void addParam(String key, Object value) {
        if (mParams == null) {
            mParams = new HashMap<String, String>();
        }
        mParams.put(key, String.valueOf(value));
    }

    @Override
    protected Map<String, String> getParams() throws AuthFailureError {
        return mParams;
    }

    @Override
    public Map<String, String> getHeaders() throws AuthFailureError {
        Map<String, String> headers = new HashMap<String, String>();

        // add user agent header
        if (TextUtils.isEmpty(mUserAgent)) {
            headers.put(HEADER_USER_AGENT, mUserAgent);
        }

        // add gzip header
        if (mGzipEnabled) {
            headers.put(HEADER_ACCEPT_ENCODING, ENCODING_GZIP);
        }

        return headers;
    }

    /**
     * Sets user agent to specify in request header
     * @param userAgent User agent string
     */
    public void setUserAgent(String userAgent) {
        mUserAgent = userAgent;
    }

    /** Disables GZIP compressing (enabled by default) */
    public void disableGzip() {
        mGzipEnabled = false;
    }

    /**
     * Sets request timeout
     * @param timeoutSec Timeout in seconds
     */
    public void setTimeout(int timeoutSec) {
        setRetryPolicy(new DefaultRetryPolicy(timeoutSec * 1000,
            DefaultRetryPolicy.DEFAULT_MAX_RETRIES,
            DefaultRetryPolicy.DEFAULT_BACKOFF_MULT));
    }
}

Итак, поехали

Как передавать параметры POST-запроса?

Действительно, как? Никаких setParameters у класса Request нет, да и в конструкторе тишина.

А оказывается, что для передачи параметров нужно переопределить у реквеста функцию getParams и вернуть мапу с нужными параметрами. Вроде:

public class SearchRequest extends Request<SomeResult> {
    

    public Map<String, String> getParams() {
        Map<String, String> params = new HashMap<String, String>();
        params.put("q", searchString);

        return params;
    }
}

Однако, видов запросов может быть много и писать такое для каждого может быть лениво. Поэтому в ExtendedRequest заведена специальная мапа и следующие методы:

  • addParam — чтобы добавлять параметры по одному.
  • setParams — чтобы инициализировать список параметров всей кучей.

Для разных ситуаций может пригодиться либо один, либо второй.

Как задать timeout запроса?

Таймауты по умолчанию в Volley маленькие — 2500 мс. Так что если сервер нетороплив, достаточно часто будет возникать Request Timeout.

В общем случае таймаут задаётся так:

Request<SomeClass> request = …;
request.setRetryPolicy(new DefaultRetryPolicy(<+>TIMEOUT_MS,
    DefaultRetryPolicy.DEFAULT_MAX_RETRIES,
    DefaultRetryPolicy.DEFAULT_BACKOFF_MULT));

По-моему несколько нетривиальный пассаж, запомнить его вряд ли получится. Так что место ему в ExtendedRequest под именем setTimeout. А ещё лично мне больше нравится указывать таймаут в секундах.

Как добавить в запрос User-Agent?

User-Agent, как известно, передаётся в заголовках HTTP-запроса, так что вопрос наш переформулируется:

Как добавить в запрос заголовки?

Тут логика та же, что и с параметрами: переопределяем getHeaders и возвращаем мапу с нужными заголовками. Так что можно по аналогии добавить локальное поле типа Map, объявить публичный метод addHeader и использовать его.

Но мне больше нравится другой подход. Так как заголовки у запросов в основном одни и те же (в основном, это только User-Agent и Referer), имеет смысл делать для каждого отдельный сеттер. Так что в ExtendedRequest нет никакого addHeader, но зато есть setUserAgent и переопределён метод getHeaders, в котором формируется мапа с заголовками, включая и юзер-агента. При необходимости можно добавить аналогичный сеттер для любого другого заголовка.

Как обработать GZIP-сжатие?

GZIP-сжатие – это хорошо, полезно и экономит пользователю трафик. Но создаёт программисту чуть-чуть дополнительной работы.

Для начала нужно попросить у сервера именно сжатый контент. Одни присылают gzip всегда, другие не умеют этого вовсе, третьи хотят соответствующий заголовок HTTP-запроса, а самые креативные завязываются на какой-нибудь GET- или POST-параметр.

Впрочем, соответствующий заголовок стоит добавлять всегда. Сервер, не поддерживающий gzip, просто не обратит на него внимания. В ExtendedRequest реализация очень простая: есть флаг mGzipEnabled, и, если он включён, в запрос добавляется заголовок Accept-Encoding=gzip. По умолчанию данный флаг имеет значение true, а выключить его можно с помощью метода disableGzip.

Это что касается отправки запроса. И это только полдела: надо ещё получить ответ, определить, действительно ли он gzip, при необходимости распаковать, и только после этого парсить. Вся эта логика реализована в методе getResponseString и сопутствующих приватных функциях. Метод объявлен, как protected. Использовать его нужно в parseNetworkResponse: строку ответа из экземпляра NetworkResponse стоит получать именно таким образом.

Итого

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

Помимо все перечисленных фичей в ExtendedRequest ещё реализована логика, связанная с success callback, про который шла речь в предыдущей статье. Ибо в большей части случаев результаты запроса возвращаются именно таким образом.

Ещё есть, над чем работать. К примеру, я пока не очень понимаю, как правильно сделать кэширование. В каком-то виде в Volley оно есть, но нет у меня к нему доверия.

Исходники

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