При разработке приложений я заметил, что каждый раз, когда мне приходилось сталкиваться с решением похожих задач (реализовывать работу с http, json, multithreading и т.п.), приходилось делать одну и туже роботу, причем на это уходило много времени. Поначалу это было не критично, но в больших проектах занимало слишком много времени. Чтобы сэкономить свое и ваше время, решил написать универсальное решение для этих задач, которым и хочу поделиться с сообществом.
Droidutils предоставляет удобный класс для работы с JSON, который позволяет конвертировать данные в JSON и обратно в объект класса, реализующего структуру конкретного JSON. Давайте посмотрим на примере.
У нас есть JSON:
Теперь нам нужен класс, в который мы запишем данные. Каждое поле, в которое мы хотим записать определенные данные, нужно пометить аннотацией и указать ключ, по которому хранятся данные в JSON.
Все готово, теперь можем парсить JSON.
Возможно и обратное действие. Для этого нужно создать экземпляр класса и заполнить его данными (поля тоже нужно пометить аннотациями) и передать парсеру, в результате получим JSON строку:
Все просто. Но сейчас вы скажете, что есть куча разных и мощных фреймворков, которые все это уже умеют (например jackson). Я с вами согласен, но в большинстве случаев мы не используем всех мощностей данных фреймворков. В таких случаях зачем нам лишний балласт, если можно обойтись одним классом?
При разработке приложений, старайтесь избегать множества зависимостей. Не спешите подключать к проекту кучу библиотек только лишь потому, что вам лень своими ручками писать. Или потому что разработчик данной библиотеки вовсю кричит, что его разработка решает данную проблему. Я не говорю, что зависимости — это плохо, просто перед тем, как что-то внедрять в свой проект, лучше подумайте несколько раз, нужно ли вам это.
Основные причины, почему много зависимостей плохо:
— проект стает очень громоздким;
— ухудшается производительность;
— на поздних этапах разработки проект стает очень зависеть от сторонних библиотек, которые при необходимости тяжело выпилить из проекта;
Это говорит человек, который уже наступил на эти грабли и которому потом пришлось много всего переделывать. Это, как мы знаем, потеря драгоценного времени и денег.
Для того, что бы работать с Http в Android, мы можем использовать одно из двух стандартных решений: ApacheHttpClient или HttpURLConnection. Я выбрал HttpURLConnection, так как ребята из Google сами его используют и нам рекомендуют.
Теперь о достоинствах и недостатках:
— HttpURLConnection немного быстрее, но менее удобный (как по мне, так это только на первый взгляд);
— ApacheHttpClient гораздо удобнее по отношению к предыдущему, но медленнее, и в нем есть парочка багов;
Давайте представим, что мы разрабатываем приложение, которое тесно общается с сервером. У нас есть куча разных запросов, которые нужно посылать на сервер. Некоторые из них сами периодически ходят на сервер за обновлениями, а другие мы сами посылаем. И еще нам нужно некоторые данные кэшировать. Возьмем за пример новостную ленту. Представим, что у нас есть запрос, с помощью которого мы получаем новую информацию, назовем его «update_news_request».
Для построения Url есть удобный builder:
Тело запроса можно создать очень просто:
С хедерами все тоже пр��сто:
Теперь создадим Http запрос, для этого у нас есть удобный builder:
Вот мы и создали наш запрос. Для выполнения запросов нам нужен класс HttpExecutor:
Давайте разбираться. Конструктор HttpExecutor требует реализацию интерфейса HttpConnection. В нашем случае я использую реализацию HttpURLConnection (можно использовать и другую реализацию). Во второй строчке задается временное ограничение для конкретного запроса (здесь используется тот самый ключ, который был указан при создании запроса). То есть обращение к серверу будет происходить не чаще чем 30 сек (в нашем случае), все остальные попытки этого запроса будут обращаться в кэш или вовсе ничего не делать. Это удобно, когда нужно уменьшить нагрузку на сервер.
Теперь можно выполнить запрос:
Первый параметр — собственно объект нашего запроса, Второй параметр — это класс, в который будет записан результат запроса и Третий параметр — реализация интерфейса Cache, сюда мы и будем обращаться, если запрос будет делаться чаще, чем указано в лимите. При желании можно не использовать Cache. Все просто и удобно.
Для работы с потоками решил использовать java.util.concurrent. Этот пакет предоставляет нам кучу всяких удобных инструментов и потокобезопасных структур данных для работы с многопоточностью.
При коммуникации с сервером возникает ряд проблем, которые нужно решить.
Первая проблема, которую нужно решить, это сделать так, что бы два потока одновременно не выполняли один и тот же запрос на сервер.
Тут нам на помощь приходит Semaphore. Давайте посмотрим на код:
Итак, как же эта штука работает?
Когда поток выполняет запрос на сервер, мы отдаем этому потоку блокировку и сохраняем в Map наш Semaphore, где ключом является ключ нашего запроса «update_news_request». Пока первый поток выполняет запрос, приходит второй поток с таким же самым запросом и в этот момент он проверяет, хранится ли в Map по данному ключу Semaphore. Если такой есть, тогда он пытается взять в данного Semaphore блокировку, а так как первый поток уже забрал ее, второй поток останавливается и ждет, пока первый поток отпустит блокировку. Таким образом, два потока не смогут сделать одновременно один и тот же запрос.
Иногда нужно, что бы все запросы на сервер выполнялись только по очереди. Тогда нам просто не нужно указывать в запросе ключ и будет использоваться ключ по умолчанию один для всех.
Вторая важная проблема возникает, когда нужно сделать несколько запросов подряд.
Например, нужно залогинится в какой-то социальной сети, потом получить профайл пользователя, потом с необходимыми данными пройти регистрацию на нашем сервере. Таким образом у нас получается три запроса. В таких случаях не нужно делать вложенные callback-и. Например, вы из UI потока запускаете другой поток, который делает запрос на сервер, а потом дергает callback в UI потоке, который в свою очередь запускает еще один поток, который делает следующий запрос — и так далее. Этот подход создает в коде многоэтажные вложенности, которые сложно читать и дебажить. Создается много ненужного кода. Но самое главное, с точки зрения многопоточности это плохая практика создавать без надобности кучу потоков и постоянно дергать UI поток. В таких случаях лучше сделать эти три запроса синхронными в одном потоке и там же обработать всю информацию, а в UI поток отправить только результат.
Есть и удобное решение для того, чтобы делать что-то по таймеру. Например, ходить на сервер за обновлениями каждые 30 секунд:
Этот метод также возвращает нам реализацию интерфейса ScheduledFuture<?>, с помощью которого мы можем остановить роботу нашего таймера, а также запросить результат с помощью метода get(). Только нужно помнить, что этот метод блокирующий.
Еще в классе ThreadExecutor есть два удобных метода:
Отличие заключается в том, что у каждого свой пул потоков, что довольно удобно.
Вот мы и добрались до финиша. Всем спасибо за внимание.
Все исходные коды можно найти здесь.
Здравая критика приветствуется.
Начнем с парсинга JSON
Droidutils предоставляет удобный класс для работы с JSON, который позволяет конвертировать данные в JSON и обратно в объект класса, реализующего структуру конкретного JSON. Давайте посмотрим на примере.
У нас есть JSON:
{ "example":{ "test":"Hello World" }, "company_name":"Google", "staff":[ { "Name":"David" }, { "Name":"Mike" } ], }
Теперь нам нужен класс, в который мы запишем данные. Каждое поле, в которое мы хотим записать определенные данные, нужно пометить аннотацией и указать ключ, по которому хранятся данные в JSON.
public class Company { // можно указывать конкретное поле из JSONObject @JsonKey("test") private String mTest; @JsonKey("company_name") private String mCompanyName; @JsonKey("staff") private LinkedList<Employee> mStaff; public class Employee { @JsonKey("Name") private String mName; } }
Все готово, теперь можем парсить JSON.
JsonConverter converter = new JsonConverter(); try { // Получаем объект нашего класса уже с данными из JSON Company company = converter.readJson(exampleJson, Company.class); } catch (Exception e) { e.printStackTrace(); }
Возможно и обратное действие. Для этого нужно создать экземпляр класса и заполнить его данными (поля тоже нужно пометить аннотациями) и передать парсеру, в результате получим JSON строку:
String json = converter.convertToJsonString(new Company());
Все просто. Но сейчас вы скажете, что есть куча разных и мощных фреймворков, которые все это уже умеют (например jackson). Я с вами согласен, но в большинстве случаев мы не используем всех мощностей данных фреймворков. В таких случаях зачем нам лишний балласт, если можно обойтись одним классом?
Маленькое отступление
При разработке приложений, старайтесь избегать множества зависимостей. Не спешите подключать к проекту кучу библиотек только лишь потому, что вам лень своими ручками писать. Или потому что разработчик данной библиотеки вовсю кричит, что его разработка решает данную проблему. Я не говорю, что зависимости — это плохо, просто перед тем, как что-то внедрять в свой проект, лучше подумайте несколько раз, нужно ли вам это.
Основные причины, почему много зависимостей плохо:
— проект стает очень громоздким;
— ухудшается производительность;
— на поздних этапах разработки проект стает очень зависеть от сторонних библиотек, которые при необходимости тяжело выпилить из проекта;
Это говорит человек, который уже наступил на эти грабли и которому потом пришлось много всего переделывать. Это, как мы знаем, потеря драгоценного времени и денег.
Работа с Http
Для того, что бы работать с Http в Android, мы можем использовать одно из двух стандартных решений: ApacheHttpClient или HttpURLConnection. Я выбрал HttpURLConnection, так как ребята из Google сами его используют и нам рекомендуют.
Теперь о достоинствах и недостатках:
— HttpURLConnection немного быстрее, но менее удобный (как по мне, так это только на первый взгляд);
— ApacheHttpClient гораздо удобнее по отношению к предыдущему, но медленнее, и в нем есть парочка багов;
Давайте представим, что мы разрабатываем приложение, которое тесно общается с сервером. У нас есть куча разных запросов, которые нужно посылать на сервер. Некоторые из них сами периодически ходят на сервер за обновлениями, а другие мы сами посылаем. И еще нам нужно некоторые данные кэшировать. Возьмем за пример новостную ленту. Представим, что у нас есть запрос, с помощью которого мы получаем новую информацию, назовем его «update_news_request».
Приступим к созданию запроса
Для построения Url есть удобный builder:
String url = new Url.Builder("http://base_url?") .addParameter("key1", "value1") .addParameter("key2", "value2") .build(); // На выходе получаем http://base_url?key1=value1&key2=value2
Тело запроса можно создать очень просто:
// создаем объект класса, который реализует структуру тела запроса // как в примере с JSON Company сompany = new Company(); // передаем наш объект в конструктор HttpBody HttpBody<Company> body = new HttpBody<Company>(сompany);
С хедерами все тоже пр��сто:
HttpHeaders headers = new HttpHeaders(); headers.add("header1", "value1"); HttpHeader header = new HttpHeader("header2", "value2"); headers.add(header);
Теперь создадим Http запрос, для этого у нас есть удобный builder:
HttpRequest updateNewsRequest= new HttpRequest.Builder() .setRequestKey("update_news_request") // указываем ключ, о нем чуть мы еще поговорим .setHttpMethod(HttpMethod.GET) // указываем тип запроса(по умолчанию HttpMethod.GET) .setUrl(url) .setHttpBody(body) .setHttpHeaders(header) .setReadTimeout(10000) // устанавливает максимальное время ожидания входного потока для чтения // по умолчанию 30 сек. .setConnectTimeout(10000) // максимальное время ожидания подключения(по умолчанию 30 сек.) .build();
Вот мы и создали наш запрос. Для выполнения запросов нам нужен класс HttpExecutor:
HttpURLConnectionClient httpURLConnectionClient = new HttpURLConnectionClient(); httpURLConnectionClient.setRequestLimit("update_news_request", 30000); httpExecutor = new HttpExecutor(httpURLConnectionClient);
Давайте разбираться. Конструктор HttpExecutor требует реализацию интерфейса HttpConnection. В нашем случае я использую реализацию HttpURLConnection (можно использовать и другую реализацию). Во второй строчке задается временное ограничение для конкретного запроса (здесь используется тот самый ключ, который был указан при создании запроса). То есть обращение к серверу будет происходить не чаще чем 30 сек (в нашем случае), все остальные попытки этого запроса будут обращаться в кэш или вовсе ничего не делать. Это удобно, когда нужно уменьшить нагрузку на сервер.
Теперь можно выполнить запрос:
RequestResponse response = httpExecutor.execute(request, RequestResponse.class, new Cache<RequestResponse>() { @Override public RequestResponse syncCache(RequestResponse data, String requestKey) { // пишем в кеш и возвращаем данные уже с кеша // так мы избежим проблем синхронизации данных на сервере и в кеше return data; } @Override public RequestResponse readFromCache(String requestKey) { RequestResponse response = new RequestResponse(); response.hello = "hello from cache"; return response; } });
Первый параметр — собственно объект нашего запроса, Второй параметр — это класс, в который будет записан результат запроса и Третий параметр — реализация интерфейса Cache, сюда мы и будем обращаться, если запрос будет делаться чаще, чем указано в лимите. При желании можно не использовать Cache. Все просто и удобно.
Работа с потоками
Для работы с потоками решил использовать java.util.concurrent. Этот пакет предоставляет нам кучу всяких удобных инструментов и потокобезопасных структур данных для работы с многопоточностью.
При коммуникации с сервером возникает ряд проблем, которые нужно решить.
Первая проблема, которую нужно решить, это сделать так, что бы два потока одновременно не выполняли один и тот же запрос на сервер.
Тут нам на помощь приходит Semaphore. Давайте посмотрим на код:
public class CustomSemaphore { private Map<String, Semaphore> mRunningTask; public CustomSemaphore(){ mRunningTask = new HashMap<String, Semaphore>(); } public synchronized void acquire(String taskTag) throws InterruptedException { Semaphore semaphore = null; if (!mRunningTask.containsKey(taskTag)) { semaphore = new Semaphore(1); } else { semaphore = mRunningTask.get(taskTag); } semaphore.acquire(); mRunningTask.put(taskTag, semaphore); } public void release(String taskTag) throws InterruptedException { if (mRunningTask.containsKey(taskTag)) { mRunningTask.remove(taskTag).release(); } } }
Итак, как же эта штука работает?
Когда поток выполняет запрос на сервер, мы отдаем этому потоку блокировку и сохраняем в Map наш Semaphore, где ключом является ключ нашего запроса «update_news_request». Пока первый поток выполняет запрос, приходит второй поток с таким же самым запросом и в этот момент он проверяет, хранится ли в Map по данному ключу Semaphore. Если такой есть, тогда он пытается взять в данного Semaphore блокировку, а так как первый поток уже забрал ее, второй поток останавливается и ждет, пока первый поток отпустит блокировку. Таким образом, два потока не смогут сделать одновременно один и тот же запрос.
Иногда нужно, что бы все запросы на сервер выполнялись только по очереди. Тогда нам просто не нужно указывать в запросе ключ и будет использоваться ключ по умолчанию один для всех.
Вторая важная проблема возникает, когда нужно сделать несколько запросов подряд.
Например, нужно залогинится в какой-то социальной сети, потом получить профайл пользователя, потом с необходимыми данными пройти регистрацию на нашем сервере. Таким образом у нас получается три запроса. В таких случаях не нужно делать вложенные callback-и. Например, вы из UI потока запускаете другой поток, который делает запрос на сервер, а потом дергает callback в UI потоке, который в свою очередь запускает еще один поток, который делает следующий запрос — и так далее. Этот подход создает в коде многоэтажные вложенности, которые сложно читать и дебажить. Создается много ненужного кода. Но самое главное, с точки зрения многопоточности это плохая практика создавать без надобности кучу потоков и постоянно дергать UI поток. В таких случаях лучше сделать эти три запроса синхронными в одном потоке и там же обработать всю информацию, а в UI поток отправить только результат.
Есть и удобное решение для того, чтобы делать что-то по таймеру. Например, ходить на сервер за обновлениями каждые 30 секунд:
ScheduledFuture<?> scheduledFuture = ThreadExecutor.doTaskWithInterval(new Runnable() { @Override public void run() { // ходим на сервер } }, 0, 30, TimeUnit.SECONDS);
Этот метод также возвращает нам реализацию интерфейса ScheduledFuture<?>, с помощью которого мы можем остановить роботу нашего таймера, а также запросить результат с помощью метода get(). Только нужно помнить, что этот метод блокирующий.
Еще в классе ThreadExecutor есть два удобных метода:
doNetworkTaskAsync(final Callable<V> task, final ExecutorListener<V> listener) doBackgroundTaskAsync(final Callable<V> task, final ExecutorListener<V> listener)
Отличие заключается в том, что у каждого свой пул потоков, что довольно удобно.
Заключение
Вот мы и добрались до финиша. Всем спасибо за внимание.
Все исходные коды можно найти здесь.
Здравая критика приветствуется.
