Droidutils - набір рішень, які прискорюють розробку додатків під 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 сомрапу = new Company();
// передаємо наш об'єкт в конструктор HttpBody
HttpBody<Company> body = new HttpBody<Company>(сомрапу);

З хедерами все теж просто:

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)

Відмінність полягає в тому, що у кожного свій пул потоків, що досить зручно.

Висновок
Ось ми і дісталися до фінішу. Всім дякую за увагу.
Всі вихідні коди можна знайти на тут.
Здорова критика вітається.

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.