При разработке приложений я заметил, что каждый раз, когда мне приходилось сталкиваться с решением похожих задач (реализовывать работу с http, json, multithreading и т.п.), приходилось делать одну и туже роботу, причем на это уходило много времени. Поначалу это было не критично, но в больших проектах занимало слишком много времени. Чтобы сэкономить свое и ваше время, решил написать универсальное решение для этих задач, которым и хочу поделиться с сообществом.
Начнем с парсинга 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 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)
Отличие заключается в том, что у каждого свой пул потоков, что довольно удобно.
Заключение
Вот мы и добрались до финиша. Всем спасибо за внимание.
Все исходные коды можно найти здесь.
Здравая критика приветствуется.
Автор: mishamhoyan