Droidutils — набор решений, которые ускоряют разработку приложений под Android

в 6:40, , рубрики: android, http, java.util.concurrent, json, mobile development, multithreading, Программирование, Разработка под android

При разработке приложений я заметил, что каждый раз, когда мне приходилось сталкиваться с решением похожих задач (реализовывать работу с 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

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js