9. RxJava у середовищі Android
9.1. Introduction
Тут ми розглянемо додаток, який вже розглядався в кількох документах:
- [Android pour les développeurs JEE : un modèle asynchrone pour clients Android] (розділ 4);
- [Introduction à la programmation de tablettes Android par l'exemple] (розділ 9);
- [Introduction à la programmation de tablettes Android par l'exemple - version 2] (параграф 1.11);
У ній розглядається клієнт-серверний додаток, у якому сервер асинхронно надає випадкові числа, які відображає клієнт на Android:
- у документі 1 клієнт на базі Android використовує нестандартну технологію;
- у документі 2 клієнт Android використовує стандартну технологію Android для асинхронних операцій;
- у документі 3 клієнт Android використовує ту саму технологію, що й у документі 2, але спрощену завдяки використанню анотацій із бібліотеки Android Annotations;
Клієнт Android виглядає так:
![]() |
Рівень [DAO] взаємодіє із сервером, який генерує випадкові числа, що відображаються на планшеті Android. Цей сервер має таку дворівневу архітектуру:
![]() |
Клієнти надсилають запити до певних URL шару [web / JSON] і отримують текстову відповідь у форматі JSON (JavaScript Object Notation).
Ми розбимо аналіз додатка на два етапи:
Веб-сервер / jSON
- його рівень [métier];
- його сервіс [web / JSON], реалізований за допомогою Spring MVC;
Android-клієнт
- його рівень [DAO];
- його активність;
- його подання;
9.2. Веб-сервіс / jSON
Примітка: веб-сервіс / jSON реалізовано за допомогою технології Spring MVC. Читач, який не знайомий з цією технологією, може:
- просто прочитати розділ 9.2.1, де пояснюється, як запустити сервер і як здійснювати запити до нього;
- ознайомитися з документом [Spring MVC et Thymeleaf par l'exemple], зокрема з розділом 4, де наведено основні анотації, що використовуються в коді;
9.2.1. Проєкт IntelliJ IDEA
Веб-сервіс / jSON має таку архітектуру:
![]() |
Ця архітектура реалізована в наступному проєкті IntelliJ IDEA [1]:
![]() | ![]() |
Сервер запускається за допомогою [2-3]. Після цього на консолі відображаються такі записи журналу:
- рядок 12: вказує, що служба доступна на порту 8080;
- рядок 10: єдиний URL веб-сервісу / jSON, доступний через операцію HTTP GET. Його параметри такі:
- [a,b]: інтервал генерації випадкових чисел;
- [minCount, maxCount]: генерується count випадкових чисел, де count — це випадкове число в інтервалі [minCount, maxCount];
- [minDelay, maxDelay]: сервіс очікує delay мілісекунд, перш ніж повернути запитувані числа, де delay — випадкове число в [minDelay, maxDelay];
У браузері запитаємо це URL:
![]() |
Ми запитали:
- випадкові числа в інтервалі [100, 200];
- n випадкових чисел з n в інтервалі [10, 20];
- час очікування x мілісекунд з x в інтервалі [300, 400];
У відповіді:
- aleas: список згенерованих випадкових чисел;
- delay: час очікування в мілісекундах, який сервер зафіксував;
- error: код помилки — 0, якщо помилки немає;
- message: повідомлення про помилку — null, якщо помилки немає;
9.2.2. Залежності Gradle проекту
![]() |
Проєкт [serveur] — це проєкт Gradle, налаштований за допомогою такого файлу [build.gradle] [1]:
// згенеровано за допомогою http://start.spring.io/ (травень 2016 р.)
buildscript {
ext {
springBootVersion = '1.3.5.RELEASE'
}
repositories {
mavenCentral()
}
dependencies {
classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
}
}
apply plugin: 'java'
apply plugin: 'spring-boot'
jar {
baseName = 'serveur'
version = '0.0.1-SNAPSHOT'
}
sourceCompatibility = 1.8
targetCompatibility = 1.8
repositories {
mavenCentral()
}
dependencies {
compile('org.springframework.boot:spring-boot-starter-web')
}
- рядок 1: коментар, що вказує, як було згенеровано цей файл конфігурації;
- рядки 4 та 10: залежність від фреймворку [Spring Boot], гілки екосистеми Spring. Цей фреймворк [http://projects.spring.io/spring-boot/] забезпечує мінімальну конфігурацію Spring. На основі архівів, що містяться у Classpath проекту, [Spring Boot] виводить ймовірну або можливу конфігурацію для нього. Отже, якщо Hibernate знаходиться у Classpath проекту, то [Spring Boot] зробить висновок, що використовуваною реалізацією JPA буде Hibernate, і налаштує Spring відповідно. Розробнику більше не потрібно це робити. Йому залишається лише виконати ті налаштування, які [Spring Boot] не зробив за замовчуванням, або ті, які [Spring Boot] зробив за замовчуванням, але які потребують уточнення. У будь-якому випадку останнє слово залишається за конфігурацією, виконаною розробником;
- рядки 14–15: два плагіни Gradle, необхідні для використання вмісту цього файлу Gradle;
- рядки 17–20: визначають характеристики архіву, що генерується для цього проєкту;
- рядки 22–23: для сумісності з Java 8;
- рядки 25–27: залежності будуть шукатися у глобальному репозиторії Maven або у локальному репозиторії на комп’ютері;
- рядок 30: визначає залежність від артефакту [spring-boot-starter-web]. Цей артефакт містить усі архіви, необхідні для проекту Spring MVC. Серед них є архів сервера Tomcat. Саме він буде використовуватися для розгортання веб-додатку. Слід зауважити, що версія залежності не вказана. Буде використана та версія, яка вказана в імпортованому проєкті [spring-boot];
Щоб оновити проект, потрібно примусово завантажити залежності [1-3]:
![]() | ![]() ![]() |
Давайте розглянемо [4] — залежності, що додаються цим файлом [build.gradle]:
![]() |
Їх дуже багато. Spring Boot для веб-додатків включив залежності, які, ймовірно, знадобляться веб-додатку Spring MVC. Це означає, що деякі з них можуть виявитися непотрібними. Spring Boot ідеально підходить для навчального посібника:
- він надає залежності, які нам, ймовірно, знадобляться;
- ми побачимо, що він значно спрощує налаштування проекту Spring MVC;
- він містить вбудований сервер Tomcat [1], що позбавляє нас необхідності розгортати додаток на зовнішньому веб-сервері;
- він дозволяє згенерувати виконуваний файл JAR, що містить усі вищезазначені залежності. Цей файл JAR можна переносити з однієї платформи на іншу без повторної конфігурації.
На сайті екосистеми Spring [http://spring.io/guides] можна знайти безліч прикладів використання Spring Boot. Тепер, коли ми знаємо залежності проєкту, можемо перейти до коду.
9.2.3. Рівень [métier]
![]() |
![]() |
Рівень [métier] матиме такий інтерфейс [IMetier]:
package dvp.rxjava.server.metier;
public interface IMetier {
// випадкові числа в інтервалі [a,b]
// n чисел генерується, де n — саме випадкове число в інтервалі [minCount, maxCount]
// числа генеруються після затримки в delay мілісекунд,
// де [delay] — це випадкове число в інтервалі [minDelay, maxDelay]
public AleasMetier getAleas(int a, int b, int minCount, int maxCount, int minDelay, int maxDelay);
}
Цей інтерфейс майже ідентичний тому, що розглядався в середовищі Swing у розділі 8.4. У рядку 8 метод [getAleas] повертає такий тип [AleasMetier]:
package dvp.rxjava.server.metier;
import java.util.List;
public class AleasMetier {
// поля
private int delay;
private List<Integer> aleas;
// конструктори
public AleasMetier(){
}
public AleasMetier(int delay, List<Integer> aleas){
this.delay=delay;
this.aleas=aleas;
}
public AleasMetier(AleasMetier aleasMetier){
this.delay=aleasMetier.delay;
this.aleas=aleasMetier.aleas;
}
// методи getter та setter
...
}
Код класу [Metier], що реалізує інтерфейс [IMetier], має такий вигляд:
package dvp.rxjava.server.metier;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.*;
@Service
public class Metier implements IMetier {
@Autowired
private ObjectMapper mapper;
@Override
public AleasMetier getAleas(int a, int b, int minCount, int maxCount, int minDelay, int maxDelay) {
// випадкові числа в інтервалі [a,b]
// n чисел генерується, де n — саме випадкове число в інтервалі [minCount, maxCount]
// числа генеруються після затримки в delay мілісекунд,
// де [delay] — це випадкове число в інтервалі [minDelay, maxDelay]
// деякі перевірки
List<String> messages = new ArrayList<>();
int erreur = 0;
if (a < 0) {
messages.add("Le nombre a de l'intervalle [a,b] de génération doit être supérieur à 0");
erreur |= 2;
}
if (a >= b) {
messages.add("Dans l'intervalle [a,b] de génération, on doit avoir a< b");
erreur |= 4;
}
if (minCount < 0) {
messages.add("Le nombre min de l'intervalle [min,count] du nombre de valeurs générées doit être supérieur à 0");
erreur |= 16;
}
if (minCount > maxCount) {
messages.add("Dans l'intervalle [min,count] du nombre de valeurs générées, on doit avoir min<= max");
erreur |= 32;
}
if (minDelay < 0) {
messages.add("Le nombre min de l'intervalle [min,count] du délai d'attente doit être supérieur à 0");
erreur |= 64;
}
if (minCount > maxCount) {
messages.add("Dans l'intervalle [min,count] du délai d'attente, on doit avoir min<= max");
erreur |= 128;
}
if (maxDelay > 5000) {
messages.add("L'attente en millisecondes avant la génération des nombres doit être dans l'intervalle [0,5000]");
erreur |= 256;
}
// помилки?
if (!messages.isEmpty()) {
throw new AleasException(String.join(" [---] ", messages), erreur);
}
// генератор випадкових чисел
Random random = new Random();
// очікування?
int delay = minDelay + random.nextInt(maxDelay - minDelay + 1);
if (delay > 0) {
try {
Thread.sleep(delay);
} catch (InterruptedException e) {
String message = null;
try {
message = mapper.writeValueAsString(Arrays.asList(String.format("[%s : %s]", e.getClass().getName(), e.getMessage())));
} catch (JsonProcessingException e1) {
throw new AleasException(e1,512);
}
throw new AleasException(message, 1024);
}
}
// генерація результату
int count = minCount + random.nextInt(maxCount - minCount + 1);
List<Integer> nombres = new ArrayList<Integer>();
for (int i = 0; i < count; i++) {
nombres.add(a + random.nextInt(b - a + 1));
}
// повернення результату
return new AleasMetier(delay,nombres);
}
}
Ми не коментуємо цей клас: він аналогічний тому, що розглядався в середовищі Swing у розділі 8.4. Зазначимо лише наступне:
- рядок 10: анотація Spring [@Service], яка забезпечить, що Spring створить єдиний екземпляр класу (синглтон) і зробить його посилання доступним для інших компонентів Spring. Для досягнення того самого ефекту тут можна було б використати й інші анотації Spring;
- рядки 13–14: вводиться маппер jSON. Spring — це контейнер об’єктів. Цей контейнер ініціалізується під час запуску веб-додатку, після чого створюються екземпляри об’єктів, визначених у конфігураційному файлі; за замовчуванням створюється лише один екземпляр (синглтон). Сінглтон Spring може містити посилання на інші об’єкти Spring. Саме так і відбувається в даному випадку: сінглтон [metier] (рядки 10–11) матиме посилання на сінглтон [mapper] (рядки 13–14). Це називається ін’єкцією залежності. Існує два способи ін’єкції синглтона в інший синглтон:
- за типом: це можливо, якщо синглтон, який потрібно ввести, є єдиним об’єктом Spring, що має цей тип. Саме так відбувається у випадку введення в рядках 13–14 (тип ObjectMapper);
- за його ім’ям, якщо кілька об’єктів Spring мають однаковий тип. У такому разі потрібно додати анотацію @Qualifier(«nomDuSingleton»), щоб вказати ім’я синглтона;
Клас [Metier] генерує винятки типу [AleaException]:
package android.exemples.server.metier;
public class AleaException extends RuntimeException {
// код помилки
private int code;
// конструктори
public AleaException() {
}
public AleaException(String detailMessage, int code) {
super(detailMessage);
this.code = code;
}
public AleaException(Throwable throwable, int code) {
super(throwable);
this.code = code;
}
public AleaException(String detailMessage, Throwable throwable, int code) {
super(detailMessage, throwable);
this.code = code;
}
// методи getter та setter
public int getCode() {
return code;
}
public void setCode(int code) {
this.code = code;
}
}
- рядок 3: [AleasException] успадковує клас [RuntimeException]. Отже, це неконтрольоване виключення (немає обов’язку обробляти його за допомогою try/catch);
- рядок 6: до класу [RuntimeException] додається код помилки;
9.2.4. Веб-сервіс / JSON
![]() |
![]() |
Веб-сервіс / JSON реалізовано за допомогою Spring MVC. Spring MVC реалізує архітектурну модель, відому як MVC (Модель – Вигляд – Контролер), наступним чином:
![]() |
Обробка запиту клієнта відбувається наступним чином:
- запит — запитувані URL мають вигляд http://machine:port/contexte/Action/param1/param2/....?p1=v1&p2=v2&... [Dispatcher Servlet] — це клас Spring, який обробляє вхідні URL. Вона «направляє» URL до дії, яка має її обробити. Ці дії — це методи певних класів, що називаються [Contrôleurs]. Літера «C» у MVC тут є рядком [Dispatcher Servlet, Contrôleur, Action]. Якщо для обробки вхідного запиту URL не було налаштовано жодної дії, сервлет [Dispatcher Servlet] відповість, що запитуваний URL не знайдено (помилка 404 NOT FOUND);
- обробка
- обрана дія може використовувати параметри parami, які їй передала сервлета [Dispatcher Servlet]. Вони можуть походити з кількох джерел:
- з шляху [/param1/param2/...] сервлета URL,
- з параметрів [p1=v1&p2=v2] сервлета URL,
- з параметрів, надісланих браузером разом із запитом;
- під час обробки запиту користувача дії може знадобитися рівень [metier] [2b]. Після обробки запиту клієнта це може викликати різні відповіді. Класичним прикладом є:
- сторінка з повідомленням про помилку, якщо запит не вдалося обробити належним чином
- сторінка підтвердження в іншому випадку
- дія вимагає відображення певного виду [3]. Цей вид відображатиме дані, які називаються моделлю виду. Це «M» у MVC. Дія створить цю модель M [2c] і вимагатиме відображення виду V [3];
- відповідь — обраний вигляд V використовує модель M, створену дією, для ініціалізації динамічних частин відповіді HTML, яку він повинен надіслати клієнту, а потім надсилає цю відповідь.
Для веб-сервісу / JSON попередня архітектура дещо змінена:
![]() |
- у [4a] модель, яка є класом Java, перетворюється на рядок JSON за допомогою бібліотеки JSON;
- у [4b] цей рядок JSON надсилається до браузера;
Повернемося до рівня [web] нашого додатка:
![]() |
У нашому додатку є лише один контролер:
![]() |
Веб-сервіс / JSON надішле своїм клієнтам відповідь типу [AleasResponse] такого змісту:
package dvp.rxjava.server.web;
import dvp.rxjava.server.metier.AleasMetier;
public class AleasResponse extends AleasMetier {
// код помилки
private int erreur;
// повідомлення про помилку
private String message;
// конструктори
public AleasResponse() {
}
public AleasResponse(int erreur, String message, AleasMetier aleasMetier) {
super(aleasMetier);
this.erreur = erreur;
this.message = message;
}
// методи getter та setter
public void setAleasMetier(AleasMetier aleasMetier) {
this.setDelay(aleasMetier.getDelay());
this.setAleas(aleasMetier.getAleas());
}
...
}
- рядок 5: клас [AleasResponse] успадковує клас [AleasMetier] і, отже, переймає всі його атрибути (aleas, delay);
- рядок 8: код помилки (0, якщо помилки немає);
- рядок 10: якщо erreur!=0 — повідомлення про помилку, null — якщо помилки немає;
Контролер [AleasController] має такий вигляд:
package dvp.rxjava.server.web;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import dvp.rxjava.server.metier.AleasException;
import dvp.rxjava.server.metier.IMetier;
@Controller
public class AleasController {
// бізнес-шар
@Autowired
private IMetier metier;
@Autowired
private ObjectMapper mapper;
// випадкові числа в [a,b]
// n чисел генерується, де n належить до інтервалу [minCount, maxCount]
// числа генеруються після очікування тривалістю delay мілісекунд,
// де [delay] — випадкове число в проміжку [minDelay, maxDelay]
@RequestMapping(value = "/{a}/{b}/{minCount}/{maxCount}/{minDelay}/{maxDelay}", method = RequestMethod.GET, produces = "application/json")
@ResponseBody
public String getAleas(@PathVariable("a") int a, @PathVariable("b") int b, @PathVariable("minCount") int minCount,
@PathVariable("maxCount") int maxCount, @PathVariable("minDelay") int minDelay,
@PathVariable("maxDelay") int maxDelay) throws JsonProcessingException {
// готується відповідь
AleasResponse response = new AleasResponse();
// використовується бізнес-шар для генерації випадкових чисел
try {
response.setAleasMetier(metier.getAleas(a, b, minCount, maxCount, minDelay, maxDelay));
} catch (AleasException e) {
// випадок помилки (код і повідомлення)
response.setErreur(e.getCode());
response.setMessage(e.getMessage());
}
// повертається відповідь jSON
return mapper.writeValueAsString(response);
}
}
- рядок 16: анотація [@Controller] робить клас [AleasController] синглтоном Spring. Крім того, вона вказує, що клас містить методи, які оброблятимуть запити для певних URL веб-додатку. Тут є лише одна, у рядку 29;
- рядки 20–21: анотація [@Autowired] вимагає від Spring ввести в поле компонент типу [IMetier]. Це буде попередній клас [Metier]. Оскільки ми додали до нього анотацію [@Service], він обробляється як компонент Spring;
- рядки 22–23: анотація [@Autowired] вимагає від Spring ввести в поле компонент типу [ObjectMapper]. Ми незабаром визначимо цей компонент;
- рядок 31: метод [getAleas] генерує випадкові числа. Його назва не має значення. Під час його виконання параметри в рядках 31–33 були ініціалізовані Spring MVC. Ми розглянемо, як саме. Крім того, якщо він виконується, то це тому, що веб-сервер отримав запит HTTP GET для URL із рядка 29 (атрибут method);
- рядок 30: анотація [@ResponseBody] вказує, що результат методу має бути надісланий клієнту без змін. У цьому випадку ми надішлемо йому рядок символів, який буде рядком jSON типу [AleasResponse];
- рядок 29: оброблюваний URL має вигляд /{a}/{b}/{minCount}/{maxCount}/{minDelay}/{maxDelay}, де {x} позначає змінну. Ці різні змінні присвоюються параметрам методу в рядках 32–33. Це здійснюється за допомогою анотації @PathVariable("x"). Слід зауважити, що значення {x} є компонентами URL і, отже, мають тип String. Перетворення String у тип параметрів методу може завершитися невдачею. У такому разі Spring MVC генерує виняток. Підсумуємо: якщо за допомогою браузера я запитую URL /100/200/10/20/300/400, метод getAleas у рядку 31 виконається з параметрами a=100 (рядок 31), b=200 (рядок 31), minCount=10 (рядок 31), maxCount=20 (рядок 32), minDelay=300 (рядок 32), maxDelay=400 (рядок 33);
- рядок 39: від шару [métier] запитується список випадкових чисел. Нагадаємо, що метод [metier].getAleas може викликати виняток;
- рядки 42–43: випадок помилки;
- рядок 46: відповідь типу [AleasResponse] повертається у вигляді рядка jSON;
9.2.5. Налаштування проекту Spring
![]() |
Існують різні способи налаштування Spring:
- за допомогою файлів XML;
- за допомогою коду Java;
- за допомогою поєднання обох;
Ми вирішили налаштувати наш веб-додаток за допомогою коду Java. Цю конфігурацію забезпечує наведений вище клас [Config]:
package dvp.rxjava.server.config;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.embedded.EmbeddedServletContainerFactory;
import org.springframework.boot.context.embedded.ServletRegistrationBean;
import org.springframework.boot.context.embedded.tomcat.TomcatEmbeddedServletContainerFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.servlet.DispatcherServlet;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
@ComponentScan(basePackages = { "dvp.rxjava.server.metier", "dvp.rxjava.server.web" })
@EnableWebMvc
public class Config {
// -------------------------------- конфігурація рівня [web]
@Autowired
private ApplicationContext context;
@Bean
public DispatcherServlet dispatcherServlet() {
DispatcherServlet servlet = new DispatcherServlet((WebApplicationContext) context);
return servlet;
}
@Bean
public ServletRegistrationBean servletRegistrationBean(DispatcherServlet dispatcherServlet) {
return new ServletRegistrationBean(dispatcherServlet, "/*");
}
@Bean
public EmbeddedServletContainerFactory embeddedServletContainerFactory() {
return new TomcatEmbeddedServletContainerFactory("", 8080);
}
// маппер jSON
@Bean
public ObjectMapper jsonMapper() {
return new ObjectMapper();
}
}
- рядок 15: ми вказуємо Spring, у яких пакетах він знайде об’єкти для інстанціювання. Він знайде два:
- клас [Metier], анотований [@Service];
- клас [AleasController], анотований [@Controller];
- рядок 16: анотація [@EnableWebMvc] ініціює автоматичні налаштування для фреймворку Spring MVC;
- рядки 19–20: ін'єкція контексту Spring (контейнер об'єктів Spring). Ця ін'єкція необхідна, оскільки об'єкт у рядках 22–26 її потребує;
- файл конфігурації Spring може визначати нові об’єкти Spring за допомогою методів, анотованих [@Bean]. Результат виконання методу стає об’єктом Spring;
- рядки 22–26: визначення сервлету фреймворку Spring MVC, який маршрутизує запити HTTP до відповідного контролера та методу. [DispatcherServlet] — це клас Spring;
- рядки 28–31: вказується, що цей сервлет обробляє всі запити URL;
- рядки 33–36: саме наявність цього біна активує сервер Tomcat, що міститься в архівах проєкту. Він очікуватиме запитів на порту 8080;
- рядки 39–42: мапер jSON. Саме він був ін'єктований в об'єкти Spring [Metier] та [AleasController];
9.2.6. Запуск веб-сервера
![]() |
Проект запускається з наступного виконуваного класу [Application]:
package android.exemples.server.boot;
import android.exemples.server.config.Config;
import org.springframework.boot.SpringApplication;
public class Application {
public static void main(String[] args) {
// запуск додатка
SpringApplication.run(Config.class, args);
}
}
- рядок 6: клас [Application] є виконуваним класом (рядки 7–10);
- рядок 9: статичний метод [SpringApplication.run] є методом класу [spring Boot] (рядок 4), який запускає додаток. Його першим параметром є Java-клас, що налаштовує проект. У даному випадку це клас [Config], який ми щойно описали. Другим параметром є масив аргументів, що передається методу [main] (рядок 7). У даному випадку аргументів не буде;
Щодо власне виконання, читачеві пропонується повернутися до розділу 9.2.1.
9.3. Клієнт для Android
Примітка: наведений нижче проект для Android є досить складним. Він вимагає ґрунтовних знань про Android, які можна знайти, наприклад, у [Introduction à la programmation de tablettes Android avec Android Studio ].
Активність
Види
Шар
[DAO]
Користувач
Сервер
Клієнт матиме два компоненти:
- рівень [Présentation] (види + активність);
- рівень [DAO], який звертається до служби [web / JSON], яку ми розглядали раніше.
9.3.1. RxAndroid
Для асинхронної взаємодії з сервером випадкових чисел клієнт Android використовуватиме бібліотеку RxAndroid. Вона розширює можливості RxJava для середовища Android. Як і у випадку з додатком Swing, ми будемо використовувати лише одне розширення, яке надає RxAndroid, а саме — планувальник [AndroidSchedulers.mainThread()]. Графічний інтерфейс Android підпорядковується тим самим законам, що й інтерфейс Swing:
- події обробляються в єдиному потоці, який називається циклом подій (event loop) або потоком інтерфейсу користувача (UI);
- коли подія запускає асинхронні дії, їхні результати потрібно отримувати в потоці інтерфейсу користувача, якщо вони мають бути використані для оновлення інтерфейсу;
Клієнт Android:
- відправить кілька асинхронних запитів до сервера випадкових чисел. Ці запити будуть виконуватися на стороні клієнта за допомогою потоків планувальника [Schedulers.io()];
- ці асинхронні запити повернуть спостережувані величини, які будуть об’єднані в одну (merge);
- цей об’єкт спостереження буде відстежуватися на стороні клієнта в планувальнику [AndroidSchedulers.mainThread()], запущеному планувальником RxAndroid;
9.3.2. Проєкт IntelliJ IDEA
Проєкт Android має назву [client]:
![]() | ![]() ![]() |
Запускати його потрібно за допомогою [2].
Примітка: запуск значною мірою залежить від налаштувань використовуваної версії IntelliJ IDEA. Цілком імовірно, що наведений вище запуск [2] не спрацює з першого разу на комп’ютері, відмінному від мого. Правильна настройка IntelliJ IDEA для виконання цього проєкту може виявитися складним завданням для початківців. Ось кілька моментів, на які слід звернути увагу:
- у [3] перейдіть до структури проєкту;
![]() | ![]() |
- у [4-5], JDK та SDK для Android, які встановлені на моєму комп’ютері. Зауважте, що JDK 1.8 не є обов’язковим. Android не підтримує деякі функції Java 8, зокрема лямбди. Тому для створення екземплярів функціональних інтерфейсів ми будемо використовувати анонімні класи. У цьому випадку достатньо версії JDK 1.6. Однак проект у тому вигляді, в якому він розповсюджується, налаштований на версію JDK 1.8;
Файл [build.gradle] [6], який налаштовує проект Android, має такий вигляд:
buildscript {
repositories {
mavenCentral()
mavenLocal()
}
dependencies {
// замінити на поточну версію плагіна Android
classpath 'com.android.tools.build:gradle:1.5.0'
}
}
apply plugin: 'com.android.application'
dependencies {
compile 'com.android.support:appcompat-v7:23.1.1'
compile 'com.android.support:design:23.1.1'
compile fileTree(dir: 'libs', include: ['*.jar'])
compile 'org.springframework.android:spring-android-rest-template:1.0.1.RELEASE'
compile 'org.codehaus.jackson:jackson-mapper-asl:1.9.9'
compile 'io.reactivex:rxandroid:1.1.0'
}
repositories {
jcenter()
}
android {
compileSdkVersion 23
buildToolsVersion "23.0.3"
defaultConfig {
applicationId "android.aleas"
minSdkVersion 15
targetSdkVersion 23
versionCode 1
versionName "1.0"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_6
targetCompatibility JavaVersion.VERSION_1_6
}
packagingOptions {
exclude 'META-INF/ASL2.0'
exclude 'META-INF/NOTICE'
exclude 'META-INF/LICENSE'
exclude 'META-INF/NOTICE.txt'
exclude 'META-INF/LICENSE.txt'
exclude 'META-INF/notice.txt'
exclude 'META-INF/license.txt'
}
}
З огляду на наявні файли SDK для Android, версії у рядках 8, 24–25 та 29, можливо, доведеться змінити.
Щоб встановити нові SDK для Android, скористайтеся SDK Manager наступним чином [1]:
![]() ![]() | ![]() |
Проект налаштовано для:
- SDK, API, 23, [2];
- SDK Build-tools 23.0.3 [3];
- SDK Tool 25.1.3 [4]
Нарешті, перевірте шлях до SDK Android у файлі [local.properties] [4], рядок 11 нижче:
## Цей файл автоматично згенеровано Android Studio.
# Не змінюйте цей файл -- YOUR CHANGES WILL BE ERASED!
#
# Цей файл *NOT* необхідно внести до системи контролю версій,
# оскільки він містить інформацію, що стосується саме вашої локальної конфігурації.
#
# Розташування файлу SDK. Він використовується лише Gradle.
# Щоб налаштувати файл під час використання системи контролю версій, ознайомтеся з
# примітку у заголовку.
#Четвер, 7 квітня 14:51:14 CEST 2016
sdk.dir=C\:\\Users\\st\\AppData\\Local\\Android\\sdk
9.3.3. Запуск проєкту в IntelliJ IDEA
Після створення правильного середовища для проєкту його можна запустити наступним чином:
![]() | ![]() | ![]() |
- у [1] запускається емулятор Android Genymotion;
- у [2] виконується конфігурація запуску [app];
- у [3] — для створення конфігурації запуску;
![]() |
- у [1, 3] конфігурація отримала назву [app];
- в [2], вона відповідає виконанню модуля з назвою [app];
- у [4] вимагається, щоб під час виконання IDE пропонував нам пристрій для виконання. Тут це завжди буде емулятор Genymotion;
- у [5] вказано, що цей пристрій слід зберігати для всіх запусків цієї конфігурації;
Виконання проєкту на емуляторі Genymotion починається з такого початкового вікна:

Щоб дізнатися, що вказати в полі [1], відкрийте командне вікно DOS і введіть таку команду [ipconfig]:
C:\Program Files\Console2>ipconfig
Configuration IP de Windows
Carte Ethernet Ethernet :
Statut du média. . . . . . . . . . . . : Média déconnecté
Suffixe DNS propre à la connexion. . . : ad.univ-angers.fr
Carte réseau sans fil Connexion au réseau local* 3 :
Statut du média. . . . . . . . . . . . : Média déconnecté
Suffixe DNS propre à la connexion. . . :
Carte Ethernet VirtualBox Host-Only Network :
Suffixe DNS propre à la connexion. . . :
Adresse IPv6 de liaison locale. . . . .: fe80::8076:36e6:3b38:5e98%16
Adresse IPv4. . . . . . . . . . . . . .: 192.168.56.2
Masque de sous-réseau. . . . . . . . . : 255.255.255.0
Passerelle par défaut. . . . . . . . . :
Carte Ethernet Ethernet 2 :
Suffixe DNS propre à la connexion. . . :
Adresse IPv6 de liaison locale. . . . .: fe80::d0d9:e01f:ddde:1f4b%14
Adresse IPv4. . . . . . . . . . . . . .: 192.168.95.1
Masque de sous-réseau. . . . . . . . . : 255.255.255.0
Passerelle par défaut. . . . . . . . . :
Carte réseau sans fil Wi-Fi :
Suffixe DNS propre à la connexion. . . :
Adresse IPv6 de liaison locale. . . . .: fe80::54b3:afe5:e199:2206%10
Adresse IPv4. . . . . . . . . . . . . .: 192.168.0.13
Masque de sous-réseau. . . . . . . . . : 255.255.255.0
Passerelle par défaut. . . . . . . . . : fe80::523d:e5ff:fe0c:4ad9 192.168.0.1
Введіть у [1] одну з адрес IP вашого комп’ютера (рядки 20, 28, 32). Якщо у вас встановлено брандмауер Windows, ймовірно, доведеться його вимкнути, щоб емулятор Android міг підключитися до сервера випадкових чисел.
Виконання асинхронних запитів із зазначеними вище даними дає такі результати:

Кожен запит генерує відповідь jSON, що містить такі поля:
- aleas: випадкові числа, згенеровані сервером;
- idClient: номер запиту;
- on: потік виконання запиту на стороні клієнта;
- requestAt: час запиту;
- responseAt: час отримання відповіді;
- delay: час очікування, який сервер дотримав перед відправленням відповіді;
- error: код помилки — 0, якщо помилки немає;
- message: повідомлення про помилку — null, якщо помилки немає;
- observedAt: час спостереження за відповіддю;
- observedOn: потік, у якому було отримано відповідь. Тут це завжди буде [main], що позначає потік інтерфейсу користувача;
Оскільки запити є асинхронними, а час очікування, встановлений для сервера, є випадковим, відповіді надходять у довільному порядку.
9.3.4. Залежності проекту в Gradle
Проєкту потрібні залежності, які ми вказуємо у файлі [app / build.gradle]:
![]() |
dependencies {
compile 'com.android.support:appcompat-v7:23.1.1'
compile 'com.android.support:design:23.1.1'
compile fileTree(dir: 'libs', include: ['*.jar'])
compile 'org.springframework.android:spring-android-rest-template:1.0.1.RELEASE'
compile 'org.codehaus.jackson:jackson-mapper-asl:1.9.9'
compile 'io.reactivex:rxandroid:1.1.0'
}
- залежності в рядках 2–3 є стандартними залежностями для проекту Android із SDK 23;
- залежність рядка 5 вказує на об’єкт Spring [RestTemplate], який керує взаємодією шару [DAO] із сервером;
- залежність у рядку 6 призводить до завантаження бібліотек JSON та [Jackson], які використовуються додатком;
- залежність у рядку 7 призводить до завантаження бібліотеки RxAndroid (а разом з нею — бібліотеки RxJava), яку рівень Ui використовує для взаємодії з рівнем [DAO];
9.3.5. Маніфест додатка для Android
![]() |
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="android.aleas">
<uses-permission android:name="android.permission.INTERNET"/>
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity
android:name="android.aleas.activity.MainActivity"
android:label="@string/app_name"
android:theme="@style/AppTheme.NoActionBar">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
</application>
</manifest>
- рядок 5: доступ до Інтернету має бути дозволений;
9.3.6. Рівень [DAO]
![]() |
![]() | ![]() |
9.3.6.1. Інтерфейс [IDao] шару [DAO]
Інтерфейс шару [DAO] буде таким:
package android.aleas.dao;
import android.aleas.fragments.Request;
import rx.Observable;
public interface IDao {
// випадкові числа в інтервалі [a,b]
// n чисел генерується, де n — саме випадкове число в інтервалі [minCount, maxCount]
// числа генеруються після затримки в delay мілісекунд,
//, де [delay] — це випадкове число в інтервалі [minDelay, maxDelay]
public Observable<AleasDaoResponse> getAleas(final Request request);
// URL веб-сервісу
public void setUrlServiceWebJson(String url);
// максимальний час очікування (мс) відповіді сервера на запит про підключення
// максимальний час очікування (мс) відповіді сервера на запит
public void setClientTimeouts(int connectTimeout, int readTimeOut);
}
- рядок 12: метод шару [DAO], який асинхронно видає випадкові числа;
- рядок 15: для вказівки реалізації [DAO] на URL служби генерації випадкових чисел;
- рядок 19: для встановлення в реалізації [DAO] максимальних часів очікування, щоб уникнути надто тривалого очікування, коли сервер не відповідає;
Метод [getAleas] отримує всі свої параметри в наступному об’єкті [Request]:
package android.aleas.fragments;
public class Request {
// номер запиту
int id;
// введення користувача
private int nbRequests;
private int a;
private int b;
private int minCount;
private int maxCount;
private int minDelay;
private int maxDelay;
// конструктори
public Request() {
}
public Request(int id, int nbRequests, int a, int b, int minCount, int maxCount, int minDelay, int maxDelay) {
this.id = id;
this.nbRequests = nbRequests;
this.a = a;
this.b = b;
this.minCount = minCount;
this.maxCount = maxCount;
this.minDelay = minDelay;
this.maxDelay = maxDelay;
}
// методи getter та setter
...
}
Тут можна впізнати більшість параметрів об’єкта URL сервера, до якого потрібно звернутися.
Метод [getAleas] повертає тип Observable<AleasDaoResponse>, де клас [AleasDaoResponse] має такий вигляд:
package android.aleas.dao;
import java.util.List;
public class AleasDaoResponse {
// код помилки
private int erreur;
// повідомлення про помилку
private String message;
// час очікування сервера
private int delay;
// випадкові числа, що видаються сервером
private List<Integer> aleas;
// стан клієнта
private ClientState clientState;
// конструктори
public AleasDaoResponse() {
}
public AleasDaoResponse(int erreur, String message, int delay, List<Integer> aleas, ClientState clientState) {
this.erreur = erreur;
this.message = message;
this.delay = delay;
this.aleas = aleas;
this.clientState = clientState;
}
// методи getter та setter
...
}
Тип [ClientState] має такий вигляд:
package android.aleas.dao;
import org.codehaus.jackson.map.annotate.JsonFilter;
import java.text.SimpleDateFormat;
import java.util.Calendar;
public class ClientState {
// ім'я потоку виконання
private String on;
// час запиту
private String requestAt;
// час відповіді
private String responseAt;
// ідентифікатор клієнта
private int idClient;
// конструктор
public ClientState() {
on = Thread.currentThread().getName();
requestAt = getTimeStamp();
}
public ClientState(int idClient) {
this();
this.idClient = idClient;
}
// приватні методи
private String getTimeStamp() {
return new SimpleDateFormat("hh:mm:ss:SSS").format(Calendar.getInstance().getTime());
}
// методи getter та setter
...
}
- рядок 11: потік виконання шару [DAO];
- рядок 13: час запиту;
- рядок 15: час відповіді;
- рядок 17: номер запиту;
Поля [on, requestAt, idClient] ініціалізуються клієнтом на початку запиту. Поле [responseAt] ініціалізується, коли клієнт отримує відповідь від сервера.
9.3.6.2. Реалізація рівня [DAO]
![]() |
Інтерфейс [IDao] реалізовано за допомогою наступного класу [Dao]:
package android.aleas.dao;
import android.aleas.fragments.Request;
import android.util.Log;
import org.codehaus.jackson.map.ObjectMapper;
import org.codehaus.jackson.map.ser.impl.SimpleBeanPropertyFilter;
import org.codehaus.jackson.map.ser.impl.SimpleFilterProvider;
import org.codehaus.jackson.type.TypeReference;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.http.converter.StringHttpMessageConverter;
import org.springframework.web.client.RestTemplate;
import rx.Observable;
import rx.Subscriber;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
public class Dao implements IDao {
// клієнт REST
private RestTemplate restTemplate;
// URL сервіс
private String urlServiceWebJson;
// маппер jSON
private ObjectMapper mapper;
// конструктори
public Dao() {
// маппер jSON
mapper = new ObjectMapper();
}
@Override
public Observable<AleasDaoResponse> getAleas(final Request request) {
...
}
@Override
public void setUrlServiceWebJson(String urlServiceWebJson) {
// визначається URL сервісу REST
this.urlServiceWebJson = urlServiceWebJson;
}
@Override
public void setClientTimeouts(int connectTimeout, int readTimeOut) {
...
}
}
- рядок 22: об’єкт [RestTemplate], який забезпечуватиме взаємодію з сервером випадкових чисел;
- рядок 24: URL служби генерації — встановлюється методом [setUrlServiceWebJson] у рядку 41;
- рядок 27: мапер jSON, який буде використовуватися для десеріалізації рядка jSON, надісланого сервером випадкових чисел;
- рядки 30–33: конструктор класу;
- рядок 32: створюється мапер jSON з рядка 27;
Метод [setClientTimeouts] має такий вигляд:
// клієнт REST
private RestTemplate restTemplate;
...
@Override
public void setClientTimeouts(int connectTimeout, int readTimeOut) {
// встановлюємо тайм-аут для запитів клієнта REST
HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory();
factory.setReadTimeout(readTimeOut);
factory.setConnectTimeout(connectTimeout);
restTemplate = new RestTemplate(factory);
restTemplate.getMessageConverters().add(new StringHttpMessageConverter());
}
- взаємодія клієнта з веб-сервером / JSON забезпечується об’єктом [RestTemplate] із рядка 2. Наразі ми його ще не ініціалізували. Це робить метод [setClientTimeouts];
- рядок 8: клас [HttpComponentsClientHttpRequestFactory] надається залежністю [spring-android-rest-template]. Він дозволить нам встановити максимальні часові межі очікування відповіді від сервера (рядки 9–10);
- рядок 11: ми створюємо об’єкт типу [RestTemplate], який слугуватиме носієм комунікації з веб-сервісом. Як параметр ми передаємо йому об’єкт [factory], який щойно було створено;
- рядок 12: діалог «клієнт-сервер» може набувати різних форм. Обмін даними відбувається у вигляді текстових рядків, і ми повинні вказати об’єкту типу [RestTemplate], що він має робити з цим текстовим рядком. Для цього ми надаємо йому конвертери — класи, здатні обробляти текстові рядки. Вибір конвертера зазвичай здійснюється за допомогою заголовків HTTP, які супроводжують текстовий рядок. Згідно з цими заголовками об’єкт [RestTemplate] вибере серед своїх конвертерів той, який найкраще підходить для даної ситуації. У даному випадку ми матимемо лише один конвертер — конвертер String --> String, завдяки чому тип String, отриманий від сервера, не зазнаватиме жодних перетворень.
Метод [getAleas] є найскладнішим:
@Override
public Observable<AleasDaoResponse> getAleas(final Request request) {
Log.d("rxjava", String.format("service [DAO] pour client n° %s%n", request.getId()));
// виконання сервісу
return Observable.create(new Observable.OnSubscribe<AleasDaoResponse>() {
@Override
public void call(Subscriber<? super AleasDaoResponse> subscriber) {
try {
// URL сервісу: /{a}/{b}/{minCount}/{maxCount}/{minDelay}/{maxDelay}
String urlService = String.format("%s/%s/%s/%s/%s/%s/%s",
urlServiceWebJson, request.getA(), request.getB(), request.getMinCount(),
request.getMaxCount(), request.getMinDelay(), request.getMaxDelay());
// інформація про клієнта
ClientState clientState = new ClientState(request.getId());
// синхронний HTTP-запит
String response = executeRestService("get", urlService, null);
// десеріалізація відповіді jSON від сервера
AleasServerResponse aleasServerResponse = mapper.readValue(
response,
new TypeReference<AleasServerResponse>() {
});
// помилка?
int erreur = aleasServerResponse.getErreur();
if (erreur != 0) {
// виняток передається далі
subscriber.onError(new AleasException(aleasServerResponse.getMessage(), erreur));
} else {
// записується час отримання
clientState.setResponseAt();
// результат передається абоненту
subscriber.onNext(
new AleasDaoResponse(aleasServerResponse.getErreur(), aleasServerResponse.getMessage(),
aleasServerResponse.getDelay(), aleasServerResponse.getAleas(), clientState));
}
} catch (Exception ex) {
// передача винятку абоненту
subscriber.onError(ex);
} finally {
// повідомляється про завершення спостережуваного об’єкта
// під час виконання помічається, що цей метод не має жодного ефекту, якщо метод [onError] був викликаний раніше — відповідно до теорії — отже, цю інструкцію можна розмістити виключно в блоці try
subscriber.onCompleted();
}
}
});
}
- рядок 2: слід пам’ятати, що потрібно створити тип [Observable<AleasResponse>];
- рядок 3: рядок журналу на консолі Android;
- рядок 5: об’єкт [RestTemplate] забезпечує синхронний діалог із сервером. Це означає, що потік виконання, який надсилає запит, блокується до отримання відповіді. У прикладі зі Swing ми бачили, як перетворити синхронну дію на асинхронну за допомогою методу [Observable.create]. Саме цим шляхом ми й ідемо тут;
- рядок 7: метод [call] інтерфейсу [Observable.OnSubscribe<AleasDaoResponse>] з рядка 5. Саме цей метод викликається, коли спостерігач підписується на об’єкт спостереження;
- рядки 10–12: створення об’єкта URL служби генерації випадкових чисел;
- рядок 14: ініціалізація об’єкта [ClientState]. Тут потрібно зафіксувати час запиту;
- рядок 16: синхронний запит HTTP. Отримуємо відповідь jSON. Метод [executeRestService] очікує три параметри:
- метод HTTP, який слід використовувати для запиту до сервісу;
- URL сервісу;
- об’єкт для відправлення типу Object, null, якщо метод HTTP не є POST;
- 18-21: десеріалізація отриманого рядка jSON у тип [AleasServerResponse]. Цей тип має такий вигляд:
package android.aleas.dao;
import java.util.List;
public class AleasServerResponse {
// код помилки
private int erreur;
// повідомлення про помилку
private String message;
// час очікування сервера
private int delay;
// випадкові числа
private List<Integer> aleas;
// гетери та сеттери
...
}
- рядок 23: отримуємо код помилки, надісланий сервером;
- рядки 24–26: у разі помилки передаємо виняток абоненту;
- рядок 29: оновлюється [clientState], який стане частиною відповіді, надісланої абоненту;
- рядки 31–33: надсилання відповіді абоненту. Вона має формат [AleasDaoResponse];
- рядки 35–37: обробляють усі випадки помилок без розрізнення. Найімовірнішою помилкою є мережева помилка;
- рядок 41: повідомлення про завершення передачі;
9.3.7. Види додатка
![]() |
![]() |
Додаток має два такі види:
Вигляд запиту

Вигляд відповіді

9.3.7.1. Клас [MyFragment]
Є два фрагменти:
- [RequestFragment] для запиту;
- [ResponseFragment] — для відповіді;
Обидва фрагменти розширюють наступний клас [MyFragment]:
package android.aleas.fragments;
import android.aleas.activity.MainActivity;
import android.aleas.activity.Session;
import android.support.v4.app.Fragment;
public abstract class MyFragment extends Fragment {
// ------------- дані, спільні для фрагментів
protected MainActivity activity;
protected Session session;
public abstract void onRefresh();
}
- рядок 7: клас [MyFragment] розширює клас Android [Fragment];
- рядки 10–11: дані, спільні для всіх фрагментів;
- рядок 10: кожен фрагмент знає про єдину активність додатка;
- рядок 11: для взаємодії між собою фрагменти використовують сесію;
- рядок 13: перед відображенням фрагмента йому буде запропоновано оновити вміст із сесії. Цей метод оголошено абстрактним, оскільки його реалізують дочірні класи. З цієї причини сам клас оголошено абстрактним (рядок 7);
Клас [Session] містить дані, якими обмінюються різні фрагменти додатка. Його код такий:
![]() |
package android.aleas.activity;
import android.aleas.fragments.Request;
import android.widget.ArrayAdapter;
public class Session {
// активність додатка
private MainActivity activity;
// кількість запитів
private int nbRequests;
// характеристики запитів
private int a;
private int b;
private int minCount;
private int maxCount;
private int minDelay;
private int maxDelay;
// URL веб-сервіс / jSON
private String urlWebJson;
// операція розпочалася
private boolean onAir;
// те саме, але трохи пізніше
private boolean operationStarted;
// назва прикладу, обраного користувачем зі списку прикладів
private String exampleName;
// його номер у списку фрагментів
private int examplePosition;
// адаптер спінера прикладів у вікні запиту
private ArrayAdapter<CharSequence> spinnerExemplesAdapter;
// методи
public void setInfos(int nbRequests, int a, int b, int minCount, int maxCount, int minDelay, int maxDelay, String urlWebJson, String exampleName, int examplePosition) {
this.nbRequests = nbRequests;
this.a = a;
this.b = b;
this.minCount = minCount;
this.maxCount = maxCount;
this.minDelay = minDelay;
this.maxDelay = maxDelay;
this.urlWebJson = urlWebJson;
this.exampleName = exampleName;
this.examplePosition = examplePosition;
}
public Request getRequest() {
return new Request(0, nbRequests, a, b, minCount, maxCount, minDelay, maxDelay);
}
// гетери та сеттери
...
}
Метод у рядку 46 дозволяє створити об’єкт [Request], що інкапсулює всю інформацію, надану користувачем у вікні запиту:
![]() |
package android.aleas.fragments;
public class Request {
// номер запиту
int id;
// введені дані користувача
private int nbRequests;
private int a;
private int b;
private int minCount;
private int maxCount;
private int minDelay;
private int maxDelay;
// конструктори
public Request() {
}
public Request(int id, int nbRequests, int a, int b, int minCount, int maxCount, int minDelay, int maxDelay) {
this.id = id;
this.nbRequests = nbRequests;
this.a = a;
this.b = b;
this.minCount = minCount;
this.maxCount = maxCount;
this.minDelay = minDelay;
this.maxDelay = maxDelay;
}
// геттери та сеттери
....
}
9.3.7.2. Фрагмент запиту [RequestFragment]
Фрагмент запиту має такі компоненти:

Додаток має єдиний вікно, яке складається з двох вкладок:
- [1] — вкладка запиту;
- [2]: вкладка відповіді;
Компоненти фрагмента [RequestFragment] такі:
№ | Тип | Назва | Роль |
3 | EditText | edtNbRequests | кількість запитів до служби генерації випадкових чисел |
4 | EditText | edtA, edtB | межі [a,b] інтервалу генерації чисел; |
5 | EditText | edtMinCount, edtMaxCount | сервіс генерує числа count, де count — випадкове число в інтервалі [minCount, maxCount] |
6 | EditText | edtMinDelay, edtMaxDelay | сервіс очікує delay мілісекунд, перш ніж згенерувати числа, де delay — випадкове число в інтервалі [minDelay, maxDelay] |
7 | EditText | edtUrlServiceRest | URL служби генерації випадкових чисел; |
8 | Спінер | spinnerExemples | випадаючий список прикладів. Кожен приклад ілюструє конкретний метод класу [Observable]; |
8 | Button | btnExecuter | кнопка, яка запускає виклики до служби генерації чисел; |
Повідомляється про помилки введення:

Компоненти з 1 по 6 — це компоненти [TextView] із такими назвами (у порядку): txtErrorRequests, txtErrorIntervalle, txtErrorCount, txtErrorDelay, txtMsgErreurUrlServiceWeb.
9.3.7.3. Фрагмент [ResponseFragment] відповіді
Фрагмент відповіді містить такі компоненти:

№ | Тип | Назва | Роль |
1 | TextView | infoReponses | кількість отриманих відповідей |
2 | ListView | listReponses | список каналів jSON, отриманих із сервера |
3 | Кнопка | btnAnnuler | для скасування запитів до сервера |
9.3.7.4. Активність Android [MainActivity]
![]() |
![]() |
Клас [MainActivity] відображає такий вигляд []:
<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/main_content"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
tools:context="android.arduinos.ui.activity.MainActivity">
<!-- панель додатка -->
<android.support.design.widget.AppBarLayout
android:id="@+id/appbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="@dimen/appbar_padding_top"
android:theme="@style/AppTheme.AppBarOverlay">
<!-- панель інструментів -->
<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
app:popupTheme="@style/AppTheme.PopupOverlay"
app:layout_scrollFlags="scroll|enterAlways">
<!-- зображення очікування -->
<ProgressBar
android:id="@+id/loadingPanel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:indeterminate="true"/>
</android.support.v7.widget.Toolbar>
<!-- контейнер вкладок -->
<android.support.design.widget.TabLayout
android:id="@+id/tabs"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
</android.support.design.widget.AppBarLayout>
<!-- контейнер переглядів -->
<android.aleas.activity.MyPager
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingLeft="20dp"
android:paddingRight="20dp"
android:layout_marginBottom="100dp"
app:layout_behavior="@string/appbar_scrolling_view_behavior"/>
</android.support.design.widget.CoordinatorLayout>
Компоненти цього виду такі:
рядки | Тип | Назва | Роль |
20–34 | Панель інструментів | панель інструментів | панель інструментів програми |
29–34 | ProgressBar | loadingPanel | зображення, що відображається під час обробки запиту користувача |
37–40 | TabLayout | вкладки | панель вкладок додатка |
44-51 | MyPager | контейнер | контейнер, у якому відображаються різні фрагменти додатка |
Клас [MyPager] має такий вигляд:
package android.aleas.activity;
import android.content.Context;
import android.support.v4.view.ViewPager;
import android.util.AttributeSet;
import android.view.MotionEvent;
public class MyPager extends ViewPager {
// елемент управління прокруткою
private boolean isSwipeEnabled;
// конструктори
public MyPager(Context context) {
super(context);
}
public MyPager(Context context, AttributeSet attrs) {
super(context, attrs);
}
// перевизначення методів
@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
// прокрутка дозволена?
if (isSwipeEnabled) {
return super.onInterceptTouchEvent(event);
} else {
return false;
}
}
@Override
public boolean onTouchEvent(MotionEvent event) {
// прокрутка дозволена?
if (isSwipeEnabled) {
return super.onTouchEvent(event);
} else {
return false;
}
}
// сеттер
public void setSwipeEnabled(boolean isSwipeEnabled) {
this.isSwipeEnabled = isSwipeEnabled;
}
}
- клас [MyPager] розширює стандартний клас Android [ViewPager]. Клас [MyPager] використовується замість класу [ViewPager] виключно для того, щоб заборонити прокрутку: за замовчуванням у класі [ViewPager] можна переходити з однієї вкладки на іншу за допомогою прокрутки (проведення пальцем вліво або вправо). У даному випадку ми не хочемо такої поведінки;
- рядок 11: логічна змінна, яка контролюватиме прокрутку (рядки 26 і 36);
- рядки 44–46: метод, що дозволяє ініціалізувати поле з рядка 11;
Скелет активності Android [MainActivity] має такий вигляд:
package android.aleas.activity;
import android.aleas.R;
import android.aleas.dao.AleasDaoResponse;
import android.aleas.dao.Dao;
import android.aleas.dao.IDao;
import android.aleas.fragments.MyFragment;
import android.aleas.fragments.Request;
import android.aleas.fragments.RequestFragment;
import android.os.Bundle;
import android.support.design.widget.TabLayout;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentPagerAdapter;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.Toolbar;
import android.view.View;
import android.widget.ArrayAdapter;
import android.widget.ProgressBar;
import rx.Observable;
public class MainActivity extends AppCompatActivity implements IDao {
// шар [DAO]
private IDao dao;
// сесія
private Session session;
// конструктор
public MainActivity() {
// батьківський елемент
super();
// сесія
session = new Session();
// DAO
dao = new Dao();
}
// методи отримання
public Session getSession() {
return session;
}
// реалізація IDao ----------------------------------------
@Override
public Observable<AleasDaoResponse> getAleas(Request request) {
return dao.getAleas(request);
}
@Override
public void setUrlServiceWebJson(String url) {
dao.setUrlServiceWebJson(url);
}
@Override
public void setClientTimeouts(int connectTimeout, int readTimeOut) {
dao.setClientTimeouts(connectTimeout, readTimeOut);
}
}
- рядок 21: клас [MainActivity] успадковує стандартний клас Android [AppCompatActivity]. Отже, це стандартний Android-актив;
- рядок 21: клас [MainActivity] реалізує інтерфейс [IDao];
Повернемося до архітектури додатка:
![]() |
те, що активність реалізує інтерфейс шару [DAO], дозволяє видам не знати про існування шару [DAO]: їхні обробники подій звертатимуться до шару [activité], коли їм потрібно взаємодіяти з сервером.
- рядок 24: посилання на шар [DAO], ініціалізоване конструктором у рядку 35;
- рядок 26: посилання на сесію, спільну для фрагментів, ініціалізоване конструктором у рядку 33;
- рядки 46–59: реалізація інтерфейсу [IDao];
Клас [MainActivity] ініціалізує компоненти пов’язаного з ним представлення наступним чином:
// панель інструментів
private Toolbar toolbar;
// менеджер фрагментів
private MyPager mViewPager;
// контейнер вкладок
private TabLayout tabLayout;
// зображення очікування
private ProgressBar loadingPanel;
...
@Override
public void onCreate(Bundle savedInstanceState) {
// класичний
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// сесія
session.setActivity(this);
// конфігурація тайм-аутів рівня [DAO]
setClientTimeouts(Constants.CONNECT_TIMEOUT, Constants.READ_TIMEOUT);
// компоненти
mViewPager = (MyPager) findViewById(R.id.container);
toolbar = (Toolbar) findViewById(R.id.toolbar);
loadingPanel = (ProgressBar) findViewById(R.id.loadingPanel);
tabLayout = (TabLayout) findViewById(R.id.tabs);
// панель інструментів
setSupportActionBar(toolbar);
// спочатку є лише одна вкладка
TabLayout.Tab tab = tabLayout.newTab();
tab.setText("Request");
tabLayout.addTab(tab);
// менеджер подій
tabLayout.setOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {
@Override
public void onTabSelected(TabLayout.Tab tab) {
// вкладка вибрана — змінюємо фрагмент, що відображається контейнером фрагментів
int position = tab.getPosition();
if (position == 0) {
// вкладка запиту
showView(0);
} else {
// вкладка «Відповідь» — залежить від обраного прикладу
showView(session.getExamplePosition());
}
}
@Override
public void onTabUnselected(TabLayout.Tab tab) {
}
@Override
public void onTabReselected(TabLayout.Tab tab) {
}
});
// створення фрагментів відповідей
createResponseFragments();
// управління зображенням очікування
loadingPanel.setVisibility(View.INVISIBLE);
}
Цей код є досить типовим для активності. Роз’яснимо деякі моменти:
- у рядку 19 згадується наступний клас [Constants]:
package android.aleas.activity;
abstract public class Constants {
final static public int VUE_REQUEST = 0;
final static public int VUE_RESPONSE = 1;
final static public int CONNECT_TIMEOUT = 1000;
final static public int READ_TIMEOUT = 6000;
final static public int DELAY_MAX = 5000;
final static public String EXAMPLES_PACKAGE = "android.aleas.exemples";
}
- рядки 31–33: створюється перша вкладка з назвою [Request]. У певний момент у пам’яті буде:
- фрагмент [Request];
- n фрагментів типу [ExampleXXFragment];
Перша вкладка завжди відображатиме фрагмент [Request]. Друга вкладка відображатиме фрагмент [ExampleXXFragment], що відповідає прикладу, обраному користувачем. Отже, фрагмент, що відображається на другій вкладці, змінюється з часом;
- рядки 37–48: код, що виконується, коли користувач клацає на одній із вкладок;
- рядок 43: відображається фрагмент № 0;
- рядок 46: відображається фрагмент, який наразі використовується (переглядається). Його номер знайдено в сесії;
- рядок 62: створюються фрагменти всіх прикладів, присутніх у списку прикладів у вікні [RequestFragment] (1-ша вкладка);
- рядок 65: зображення очікування наразі приховане;
Щоб зрозуміти метод [showView] (рядки 43, 46) та метод [createResponseFragments], спочатку потрібно ознайомитися з менеджером фрагментів у пам’яті (клас, включений у файл Java MainActivity):
// менеджер фрагментів — повинен визначити методи getItem, getCount
public class SectionsPagerAdapter extends FragmentPagerAdapter {
// управління фрагментами
private MyFragment[] fragments;
// конструктор
public SectionsPagerAdapter(FragmentManager fm, MyFragment[] fragments) {
super(fm);
this.fragments = fragments;
}
// повинен повернути фрагмент під позицією №
@Override
public MyFragment getItem(int position) {
// фрагмент
return fragments[position];
}
// повертає кількість фрагментів, що підлягають управлінню
@Override
public int getCount() {
// кількість фрагментів
return fragments.length;
}
}
}
- клас [SectionsPagerAdapter] є похідним від класу Android [FragmentPagerAdapter]. Він перевизначає два методи свого батьківського класу:
- метод [getItem], рядок 15;
- метод [getCount], рядок 22;
- клас [SectionsPagerAdapter] містить усі фрагменти додатка. Вони зберігаються у рядку 5. Зазначимо, що вони мають тип [MyFragment], описаний у розділі 9.3.7.1;
- рядок 8: для свого формування клас [SectionsPagerAdapter] отримує фрагменти, якими він повинен керувати;
- рядки 14–18: метод [getItem] повертає фрагмент, що знаходиться на позиції [position];
- рядки 21–25: метод [getCount] повертає загальну кількість фрагментів;
Метод [createResponseFragments] створює всі фрагменти, необхідні додатку:
private void createResponseFragments() {
// списк прикладів
ArrayAdapter<CharSequence> adapter = ArrayAdapter.createFromResource(this, R.array.exemples, android.R.layout.simple_spinner_item);
// Вказати макет, який слід використовувати при відображенні списку варіантів
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
// адаптер додається до сеансу, щоб представлення [Request] могло його отримати
session.setSpinnerExemplesAdapter(adapter);
...
}
- рядок 3: створюється адаптер для спінера прикладів, у даному випадку список String, що містить імена прикладів. Ці імена містяться у файлі [layout/exemples.xml]:
![]() |
Файл [exemples.xml] містить такий код:
<!-- приклади -->
<resources>
<string-array name="exemples">
<item>Exemple-01</item>
<item>Exemple-02</item>
<item>Exemple-03</item>
<item>Exemple-04</item>
</string-array>
</resources>
Рядок 1: цей файл є другим параметром методу [createFromResource]. У [R.array.exemples] [exemples] — це назва масиву (рядок 3 вище), а не назва файлу.
- рядок 5: до адаптера прив’язується layout (менеджер відображення). Тепер адаптер має як дані, так і спосіб їхнього відображення;
- рядок 7: адаптер переводиться в сеанс. Саме звідси його отримає фрагмент [RequestFragment], якому він потрібен;
Продовжимо код методу [createResponseFragments]:
private void createResponseFragments() {
// приклади використання спінера
ArrayAdapter<CharSequence> adapter = ArrayAdapter.createFromResource(this, R.array.exemples, android.R.layout.simple_spinner_item);
// Вкажіть макет, який слід використовувати при відображенні списку варіантів
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
// адаптер додається до сеансу, щоб його міг отримати вигляд [Request]
session.setSpinnerExemplesAdapter(adapter);
// створення таблиці фрагментів (1 запит, n відповідей)
MyFragment[] tFragments = new MyFragment[adapter.getCount() + 1];
// фрагмент запиту
tFragments[0] = new RequestFragment();
// фрагменти відповідей
for (int i = 1; i < tFragments.length; i++) {
// будується ім’я фрагмента для інстанціювання, що відповідає прикладу, обраному користувачем
// це ім’я має бути повним, із зазначенням пакета — тут воно безпосередньо пов’язане з номером прикладу у спінері
String exampleClassName = String.format("%s.Example%02dFragment", Constants.EXAMPLES_PACKAGE, i);
// створюється екземпляр фрагмента, пов'язаного з прикладом
MyFragment fragment;
try {
// інстанціювання класу
fragment = (MyFragment) Class.forName(exampleClassName).getConstructors()[0].newInstance(new Object[]{});
} catch (Exception e) {
e.printStackTrace();
return;
}
// фрагмент створено — його додаємо до масиву
tFragments[i] = fragment;
}
// створення екземпляра менеджера фрагментів із цими новими фрагментами
mSectionsPagerAdapter = new SectionsPagerAdapter(getSupportFragmentManager(), tFragments);
// Налаштування ViewPager за допомогою адаптера розділів.
mViewPager.setAdapter(mSectionsPagerAdapter);
// навігація між сторінками — ця інструкція є важливою
// тут зазначено, що по обидва боки від відображуваного виду слід зберігати ініціалізовані види [tFragments.length]
// це означає, що всі фрагменти, які використовує додаток, знаходяться в пам’яті та ініціалізовані
// якщо цього не зробити, то за замовчуванням значення [OffscreenPageLimit] дорівнює 1
// отже, якщо відображається фрагмент № 3, ініціалізуються лише фрагменти 2 та 4
// це відбувається шляхом виклику методу [onCreateView] для цих двох фрагментів — це означає, що в цьому методі потрібно передбачити
// відтворити той вигляд, який мав фрагмент під час його останнього використання — крім того, у цьому методі не повинно бути
// не повинно бути коду, який не витримує дворазового виконання — це створює жахливий безлад і ускладнює управління
// тут ми вирішили уникнути цих труднощів — у логах видно, що під час запуску додатка створюються всі фрагменти
// і їхній метод [onCreateView] виконується — після цього він більше ніколи не виконується —
mViewPager.setOffscreenPageLimit(tFragments.length);
// ми блокуємо перехід між фрагментами
mViewPager.setSwipeEnabled(false);
}
- рядок 9: створення масиву, який міститиме всі фрагменти додатка;
- рядок 11: перший фрагмент — це фрагмент запиту;
- рядки 13–28: ми створимо стільки фрагментів, скільки є прикладів. Усі ці фрагменти розширюють фрагмент відповіді [ResponseFragment] і реалізують лише те, що є специфічним для конкретного прикладу: створення спостережуваних значень. Адже вони відрізняються від одного прикладу до іншого;
- рядок 16: фрагмент прикладу має стандартну назву: ExampleXXFragment, де XX — це його позиція у списку прикладів, збільшена на 1. XX також є номером фрагмента прикладу в менеджері фрагментів;
- рядок 21: створення екземпляра фрагмента прикладу № i зі списку:
- Class.forName(exampleName): завантажує фрагмент у пам'ять;
- Class.forName(exampleName).getConstructors()[0]: отримує посилання на перший конструктор класу. Клас ExampleXXFragment має лише один конструктор. Отже, буде отримано посилання саме на нього;
- Class.forName(exampleName).getConstructors()[0].newInstance(new Object[]{}) створює екземпляр об’єкта типу ExampleXXFragment, використовуючи конструктор з попереднього кроку. new Object[]{} представляє параметри, що передаються цьому конструктору. Оскільки конструктор класу ExampleXXFragment не очікує параметрів, передається порожній масив об’єктів;
- рядок 27: цей фрагмент додається до масиву фрагментів;
- рядок 30: ми бачили, що конструктор менеджера фрагментів [SectionsPagerAdapter] очікував у своїх параметрах масив фрагментів, якими він мав керувати. Саме зараз ми передаємо його йому;
- рядок 22: контейнер фрагментів [mViewPager] з подання, пов’язаного з активністю [MainActivity], тут пов’язується з менеджером фрагментів: контейнер фрагментів [mViewPager] відображає фрагменти менеджера фрагментів;
- рядок 43: прочитаємо коментарі — ця інструкція означає, що всі фрагменти мають залишатися в тому стані, в якому їх встановлює код, незалежно від того, який фрагмент наразі відображається. Отже, коли ми повертаємося до нього, ми бачимо його в тому стані, в якому його залишили;
- рядок 45: контейнер фрагментів [mViewPager] має тип [MyPager], що дозволяє заборонити прокрутку;
Метод [MainActivity.showView] виглядає наступним чином:
// перегляд запису № [position]
private void showView(int position) {
// оновити фрагмент перед його відображенням
mSectionsPagerAdapter.getItem(position).onRefresh();
// відображається запитаний вигляд — відбувається перехід безпосередньо до цього вигляду (другий параметр має значення false)
// без цього параметра за замовчуванням відбувається перехід до потрібного виду з швидким відображенням проміжних видів — небажана поведінка
mViewPager.setCurrentItem(position, false);
}
- рядок 3: потрібно відобразити фрагмент № позиції;
- рядок 4: цей фрагмент запитується у менеджера фрагментів, а потім оновлюється. Адже з моменту його останнього відображення сесія могла змінитися. Тому фрагмент повинен перевірити її, щоб визначити, чи потрібно оновлюватися;
- рядок 7: фрагмент відображається за допомогою [ViewPager]. Оскільки цей фрагмент був пов’язаний з менеджером фрагментів, буде відображено фрагмент № [position] — той самий, який щойно було оновлено в рядку 4;
На завершення розглянемо два методи управління очікуванням:
public void beginWaiting() {
// управління зображенням очікування
loadingPanel.setVisibility(View.VISIBLE);
}
public void cancelWaiting() {
// управління зображенням очікування
loadingPanel.setVisibility(View.INVISIBLE);
// завершення виконання
session.setOnAir(false);
session.setOperationStarted(false);
}
9.3.7.5. Фрагмент [RequestFragment]
Клас [RequestFragment] має такий вигляд:
package android.aleas.fragments;
import android.aleas.R;
import android.aleas.activity.Constants;
import android.aleas.activity.MainActivity;
import android.os.Bundle;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.*;
import java.net.URI;
import java.net.URISyntaxException;
public class RequestFragment extends MyFragment {
// URL веб-сервісу
private EditText edtUrlServiceRest;
private TextView txtMsgErreurUrlServiceWeb;
// кількість запитів
private EditText edtNbRequests;
private TextView txtErrorRequests;
// інтервал генерації
private EditText edtA;
private EditText edtB;
private TextView txtErrorIntervalle;
// затримка
private EditText edtMinDelay;
private EditText edtMaxDelay;
private TextView txtErrorDelay;
// кількість згенерованих значень
private EditText edtMinCount;
private EditText edtMaxCount;
private TextView txtErrorCount;
// кнопка
private Button btnExecuter;
// список відповідей
private ListView listReponses;
private TextView infoReponses;
// список прикладів
private Spinner spinnerExemples;
// введені дані
private int nbRequests;
private int a;
private int b;
private String urlServiceWebJson;
private int minDelay;
private int maxDelay;
private int minCount;
private int maxCount;
// конструктор
public RequestFragment() {
super();
Log.d("rxjava", "RequestFragment constructor");
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
Log.d("rxjava", "RequestFragment onCreateView");
// отримуємо активність та сесію
activity = (MainActivity) getActivity();
session = activity.getSession();
// створення подання фрагмента на основі його визначення XML
View view = inflater.inflate(R.layout.request, container, false);
// компоненти
edtUrlServiceRest = (EditText) view.findViewById(R.id.editTextUrlServiceWeb);
txtMsgErreurUrlServiceWeb = (TextView) view.findViewById(R.id.textViewErreurUrl);
edtNbRequests = (EditText) view.findViewById(R.id.edt_nbrequests);
txtErrorRequests = (TextView) view.findViewById(R.id.txt_error_nbrequests);
edtA = (EditText) view.findViewById(R.id.edt_a);
edtB = (EditText) view.findViewById(R.id.edt_b);
txtErrorIntervalle = (TextView) view.findViewById(R.id.txt_errorIntervalle);
edtMinDelay = (EditText) view.findViewById(R.id.edt_minDelay);
edtMaxDelay = (EditText) view.findViewById(R.id.edt_maxDelay);
txtErrorDelay = (TextView) view.findViewById(R.id.txt_error_delay);
edtMinCount = (EditText) view.findViewById(R.id.edt_minCount);
edtMaxCount = (EditText) view.findViewById(R.id.edt_maxCount);
txtErrorCount = (TextView) view.findViewById(R.id.txt_error_count);
btnExecuter = (Button) view.findViewById(R.id.btn_Executer);
listReponses = (ListView) view.findViewById(R.id.lst_reponses);
infoReponses = (TextView) view.findViewById(R.id.txt_Reponses);
spinnerExemples = (Spinner) view.findViewById(R.id.spinnerExemples);
// кнопка [Exécuter]
btnExecuter.setVisibility(View.VISIBLE);
btnExecuter.setOnClickListener(new View.OnClickListener() {
public void onClick(View arg0) {
doExecuter();
}
});
// Спочатку немає повідомлень про помилки
txtErrorRequests.setVisibility(View.INVISIBLE);
txtErrorIntervalle.setVisibility(View.INVISIBLE);
txtMsgErreurUrlServiceWeb.setVisibility(View.INVISIBLE);
txtErrorCount.setVisibility(View.INVISIBLE);
txtErrorDelay.setVisibility(View.INVISIBLE);
// спінер із прикладами
spinnerExemples.setAdapter(session.getSpinnerExemplesAdapter());
// результат
return view;
}
...
}
- рядок 16: клас [RequestFragment] розширює клас [MyFragment] (див. параграф 9.3.7.1);
- рядки 18–42: візуальні компоненти фрагмента (див. параграф 9.3.7.2);
- рядки 45–52: дані, введені користувачем у форму;
- конструктор (рядки 55–58) та метод [onCreateView] виконуються, коли активність [MainActivity] створює всі фрагменти додатка. Це відбувається лише один раз;
- рядок 61: код методу [onCreateView] є стандартним. Зверніть увагу на рядок 102, де адаптер спінера з прикладів береться із сесії. Також зверніть увагу на рядок 91: натискання кнопки [Exécuter] обробляється методом [doExecuter];
- рядки 64–65: поля [activity] та [session] належать до батьківського класу [MyFragment];
Метод [doExecuter] має такий вигляд:
// введені дані
private int nbRequests;
private int a;
private int b;
private String urlServiceWebJson;
private int minDelay;
private int maxDelay;
private int minCount;
private int maxCount;
...
private void doExecuter() {
// дані введені правильно?
if (isPageValid()) {
// інформація зберігається в сесії
session.setInfos(nbRequests, a, b, minCount, maxCount, minDelay, maxDelay, urlServiceWebJson, spinnerExemples.getSelectedItem().toString(), spinnerExemples.getSelectedItemPosition() + 1);
// зберігаємо URL веб-сервісу
activity.setUrlServiceWebJson(session.getUrlWebJson());
Log.d("rxjava", String.format("RequestFragment doExecuter, session=%s, session.position=%s%n", session, session.getExamplePosition()));
// дія виконується
session.setOnAir(true);
// але ще не розпочато
session.setOperationStarted(false);
// відображається фрагмент відповіді
activity.selectTab(Constants.VUE_RESPONSE);
// починається очікування
beginWaiting();
}
}
- рядок 15: ми не будемо коментувати метод [ispageValid]. Він перевіряє правильність введених даних і повертає значення true лише в тому випадку, якщо всі вони є правильними. У цьому випадку вони використовуються для ініціалізації полів у рядках 2–9;
- рядок 17: різні введені дані зберігаються у сесії:
- [spinnerExemples.getSelectedItem().toString()] — це назва прикладу, обраного користувачем, яка зберігається в [session.exampleName];
- [spinnerExemples.getSelectedItemPosition() + 1] — це номер фрагмента, пов’язаного з прикладом, який було збережено (фрагмент) менеджером фрагментів. Цей номер зберігається в [session.examplePosition];
- рядок 19: URL з веб-сервісу / jSON передається до активності, яка, у свою чергу, передає його на рівень [DAO];
- рядки 21–24: відзначається, що операція розпочнеться;
- рядок 26: буде відображено вкладку відповіді. Щоб зрозуміти, що відбуватиметься, слід згадати код [MainActivity.selectTab]:
// вибір вкладки
public void selectTab(int position) {
// є не більше 2 вкладок
// спочатку є лише одна — та, що вказана у запиті
// якщо запитувана вкладка — № 1 і її ще не існує, то її потрібно створити
if (position == 1 && tabLayout.getTabCount() == 1) {
// додано 1 вкладку
TabLayout.Tab tab = tabLayout.newTab();
tab.setText("Response");
tabLayout.addTab(tab);
}
// вкладку вибирають програмно, що запускає подію [onTabSelected]
//, що прив’яже відповідний вигляд до цієї вкладки
tabLayout.getTabAt(position).select();
}
- спочатку операція створила лише вкладку запиту (вкладка № 0);
- рядки 6–11: створюється вкладка відповіді (вкладка № 1), якщо вона ще не була створена;
- рядок 14: вибирається вкладка № position (0 або 1). Це ставить подію [onTabSelected] у чергу циклу подій додатка для Android;
Обробник події [onTabSelected] у [MainActivity] має такий вигляд:
@Override
public void onTabSelected(TabLayout.Tab tab) {
// вкладка вибрана — змінюється фрагмент, що відображається контейнером фрагментів
int position = tab.getPosition();
if (position == 0) {
// вкладка запиту
showView(0);
} else {
// вкладка «Відповідь» — залежить від обраного прикладу
showView(session.getExamplePosition());
}
}
У випадку вкладки [Response] виконується рядок 9. Буде відображено фрагмент № [session.getExamplePosition()]. Наприклад, для прикладу [exemple-03] номер, який було записано в [session.examplePosition], дорівнює 3. Отже, у рядку 10 відображається фрагмент № 3. Таблиця фрагментів, спочатку сформована активністю, має вигляд [RequestFragment, Exemple01Fragment, Exemple02Fragment, Exemple03Fragment,..]. Отже, саме фрагмент [Exemple03Fragment] буде відображено. Це здійснюється за допомогою такого коду:
// відображення виду № [position]
private void showView(int position) {
// оновлення фрагмента перед його відображенням
mSectionsPagerAdapter.getItem(position).onRefresh();
// відображається запитаний вигляд — відбувається перехід безпосередньо до цього виду (другий параметр має значення false)
// без цього параметра за замовчуванням відбувається перехід до потрібного виду з швидким відображенням проміжних видів — небажана поведінка
mViewPager.setCurrentItem(position, false);
}
Бачимо, що фрагмент буде оновлено (рядок 4) перед тим, як його відобразити (рядок 7).
9.3.7.6. Фрагмент [ResponseFragment]
Клас [ResponseFragment] відображає відповіді сервера. Його код такий:
package android.aleas.fragments;
import android.aleas.R;
import android.aleas.activity.MainActivity;
import android.os.Bundle;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.ListView;
import android.widget.TextView;
import org.codehaus.jackson.map.ObjectMapper;
import rx.Subscription;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
public abstract class ResponseFragment extends MyFragment {
// список відповідей
private ListView listReponses;
private TextView infoReponses;
// кнопка
private Button btnAnnuler;
// маппер jSON
private ObjectMapper mapper;
protected ResponseFragment() {
super();
Log.d("rxjava", String.format("ResponseFragment (%s) constructor", this));
mapper = new ObjectMapper();
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
// отримуємо активність та сесію
activity = (MainActivity) getActivity();
session = activity.getSession();
Log.d("rxjava", String.format("ResponseFragment (%s) onCreateView%n", this));
// створення подання фрагмента на основі його визначення XML
View view = inflater.inflate(R.layout.response, container, false);
// компоненти
listReponses = (ListView) view.findViewById(R.id.lst_reponses);
infoReponses = (TextView) view.findViewById(R.id.txt_Reponses);
btnAnnuler = (Button) view.findViewById(R.id.btn_Annuler);
// кнопка [Annuler]
btnAnnuler.setVisibility(View.INVISIBLE);
btnAnnuler.setOnClickListener(new View.OnClickListener() {
public void onClick(View arg0) {
doAnnuler();
}
});
// результат
return view;
}
...
// метод, який потрібно виконати (за допомогою явного коду) перед кожним відображенням фрагмента
public void onRefresh() {
...
}
}
- рядок 21: клас [ResponseFragment] успадковує клас [MyFragment];
- рядки 23–27: компоненти фрагмента;
- рядки 32–36: конструктор виконується лише один раз під час початкового створення фрагментів прикладів активною дією. Адже всі фрагменти прикладів успадковують фрагмент [ResponseFragment]. Під час їх інстанціювання викликається конструктор їхнього батьківського класу [ResponseFragment];
- рядок 35: ініціалізує мапер jSON із рядка 30, який використовується для відображення рядка jSON зі стека винятків;
- рядки 38–59: метод [onCreateView] виконується лише один раз, під час початкового створення фрагментів прикладів активною дією. У ньому міститься типовий код для додатка Android;
- рядки 52–56: метод, що виконується при натисканні кнопки [Annuler], — це метод [doAnnuler];
- рядки 62–64: метод [onRefresh] виконується щоразу, коли відображається вкладка [Response];
Завдяки різним записам у журналах, розміщеним у важливих методах, можна побачити, що відбувається під час запуску програми:
- рядок 1: побудова фрагмента [RequestFragment];
- рядки 2–9: побудова фрагментів 4 прикладів додатка;
- рядок 10: ініціалізація фрагмента [RequestFragment];
- рядки 11–14: ініціалізація фрагментів 4 прикладів програми;
Далі виклики цих методів більше не зустрічаються.
Метод [ResponseFragment.onRefresh] має такий вигляд:
// метод, який потрібно виконати (за допомогою явного коду) перед кожним відображенням фрагмента
public void onRefresh() {
Log.d("rxjava", String.format("ResponseFragment (%s) onRefresh for %s, sessionIsOnAir=%s session.isOperationStarted=%s%n", this, activity == null ? null : activity.getSession().getExampleName(), session.isOnAir(), session.isOperationStarted()));
// виконання триває?
if (session.isOnAir() && !session.isOperationStarted()) {
// запит на виконання
session.setOperationStarted(true);
doExecuter();
}
}
- рядок 5: перевіряється, чи фрагмент [RequestFragment] здійснив запит (session.isOnAir) і чи цей запит розпочався (isOperationStarted). Якщо фрагмент [RequestFragment] відправив запит і цей запит ще не виконується, операція запускається (рядки 7–8);
- після запуску операції, оскільки вона є асинхронною, користувач може переходити між двома вкладками. Якщо він знову переходить на вкладку [Response] і операція ще триває, то рядки 7–8 не виконуються;
Метод [doExecuter] у рядку 8 виконує операцію, яку запросив користувач:
private void doExecuter() {
Log.d("rxjava", String.format("ResponseFragment (%s) doExecuter for %s%n", this, session.getExampleName()));
// початок очікування
beginWaiting();
// підготовка до виконання
subscriptions.clear();
reponses.clear();
nbInfos = 0;
// створюються та виконуються спостережувані величини з обраного прикладу
createAndExecuteObservables();
}
// метод реалізовано дочірніми класами
protected abstract void createAndExecuteObservables();
- рядок 10: створює, виконує та спостерігає за спостережуваними величинами. Вони різні для кожного прикладу. Саме тому метод [createAndExecuteObservables] є абстрактним (рядок 14). Він буде реалізований фрагментами [ExampleXXFragment], які розширюють клас [ResponseFragment];
- рядок 6: список підписок очищується;
- рядок 7: список, що відображає відповіді, очищується;
- рядок 8: підраховує кількість отриманих відповідей;
Дочірні класи [ExampleXXFragment] доручають наступному методу [showAlea] завдання відображати елементи, за якими вони спостерігають:
protected void showAlea(String data) {
// додаткова інформація
nbInfos++;
infoReponses.setText(String.format("Liste des réponses (%s)", nbInfos));
// 1 відповідь від +
reponses.add(0, data);
Log.d("rxjava", data);
// оновлення UI
listReponses.setAdapter(new ArrayAdapter<String>(getActivity(), android.R.layout.simple_list_item_1, android.R.id.text1, reponses));
}
- рядок 1: бачимо, що спостережуваний елемент надходить у вигляді рядка. Фактично це буде рядок jSON спостережуваного елемента. Це дозволяє нам мати єдиний метод відображення спостережуваного елемента незалежно від його точного типу в Java;
- рядок 6: спостережуваний елемент [data] додається на перше місце у списку відповідей. Таким чином, користувач бачить на початку списку найсвіжіші відповіді;
Очікування обробляється за допомогою таких методів [beginWaiting] та [cancelWaiting]:
private void beginWaiting() {
// вмикаємо пісочний годинник
activity.beginWaiting();
// кнопка [Annuler] відображається
btnAnnuler.setVisibility(View.VISIBLE);
}
protected void cancelWaiting() {
// очікування завершено
activity.cancelWaiting();
// кнопка [Annuler] прихована
btnAnnuler.setVisibility(View.INVISIBLE);
}
Вони викликають однойменні методи активності, а потім просто показують або приховують кнопку [Annuler].
Натискання на кнопку [Annuler] обробляється за допомогою такого коду:
protected void doAnnuler() {
// скасовано всі підписки
for (Subscription s : subscriptions) {
if (!s.isUnsubscribed()) {
s.unsubscribe();
}
}
// завершення очікування
cancelWaiting();
}
- рядки 3–7: по черзі скасовуються всі підписки;
9.3.8. Приклади спостережуваних величин
9.3.8.1. Exemple-01
Класи [ExampleXXFragment] призначені для створення, виконання та спостереження за об’єктами спостереження. Відображення спостережуваних значень здійснюється батьківським класом [ResponseFragment].
Клас [Example01Fragment] має такий вигляд:
![]() |
package android.aleas.exemples;
import android.aleas.dao.AleasDaoResponse;
import android.aleas.fragments.AleasUiResponse;
import android.aleas.fragments.Request;
import android.aleas.fragments.ResponseFragment;
import android.util.Log;
import org.codehaus.jackson.map.ObjectMapper;
import org.codehaus.jackson.map.ser.impl.SimpleBeanPropertyFilter;
import org.codehaus.jackson.map.ser.impl.SimpleFilterProvider;
import rx.Observable;
import rx.android.schedulers.AndroidSchedulers;
import rx.functions.Action0;
import rx.functions.Action1;
import rx.schedulers.Schedulers;
import java.io.IOException;
public class Example01Fragment extends ResponseFragment {
// маппери jSON
private ObjectMapper mapperAleasUiResponse;
// конструктор
public Example01Fragment() {
super();
Log.d("rxjava", "Example01Fragment constructor");
// фільтри jSON
mapperAleasUiResponse = new ObjectMapper();
}
@Override
public void createAndExecuteObservables() {
Log.d("rxjava", "Example01Fragment createAndExecuteObservables");
// запит на випадкові числа
Observable<AleasDaoResponse> observable = Observable.empty();
for (int i = 0; i < session.getNbRequests(); i++) {
// спостережувана конфігурація № i
// запит, який потрібно надіслати на сервер
Request request = session.getRequest();
request.setId(i);
// спостерігач, що виконується у обчислювальному потоці
observable = observable.mergeWith(session.getActivity().getAleas(request).subscribeOn(Schedulers.io()));
}
// спостереження у потоці циклу подій;
observable = observable.observeOn(AndroidSchedulers.mainThread());
// виконуються всі ці спостережувані об’єкти
subscriptions.add(observable.subscribe(new Action1<AleasDaoResponse>() {
@Override
public void call(AleasDaoResponse aleasDaoResponse) {
showAlea(getDataFrom(aleasDaoResponse));
}
}, new Action1<Throwable>() {
...
}, new Action0() {
...
}
private String getDataFrom(AleasDaoResponse aleasDaoResponse) {
// витягується інформація для відображення
String data;
try {
data = mapperAleasUiResponse.writeValueAsString(new AleasUiResponse(aleasDaoResponse));
} catch (IOException e) {
data = String.format("[%s,%s]", e.getClass().getName(), e.getMessage());
}
return data;
}
}
- рядок 36: єдиний спостережуваний параметр, який буде згенеровано;
- рядки 37–44: створення та налаштування різних спостережуваних величин, які об’єднуються (рядок 43) у спостережувану величину з рядка 36;
- рядок 43: спостережувана величина виконується у потоці планувальника [Schedulers.io()]. Виклик HTTP до сервера буде виконано у цьому потоці;
- рядок 46: кінцевий об’єкт спостереження спостерігається у потоці циклу подій;
- рядки 48–57: виконання обсервабелів, а отже, запитів до сервера випадкових чисел. Android поки що не підтримує Java 8 та її лямбди. Тому тут використовуються анонімні класи для інстанціювання функціональних інтерфейсів RxJava;
- рядки 49–52: дія, що виконується, коли спостерігач отримує новий елемент типу [AleasDaoResponse] від об’єкта спостереження (див. параграф 9.3.6.1);
- рядок 51: виклик методу [showAlea] батьківського класу. Нагадаємо, що він очікує на рядок символів. Цей рядок надається методом [getDataFrom] у рядках 59–68;
- рядок 63: повертається рядок jSON типу [AleasUiResponse] наступного вигляду:
package android.aleas.fragments;
import android.aleas.dao.AleasDaoResponse;
import java.text.SimpleDateFormat;
import java.util.Calendar;
public class AleasUiResponse {
// відповідь [DAO]
private AleasDaoResponse aleasDaoResponse;
// потік спостереження
private String observedOn;
// час спостереження
private String observedAt;
// конструктори
public AleasUiResponse() {
observedOn = Thread.currentThread().getName();
observedAt = new SimpleDateFormat("hh:mm:ss:SSS").format(Calendar.getInstance().getTime());
}
public AleasUiResponse(AleasDaoResponse aleasDaoResponse, String on, String at) {
this.aleasDaoResponse = aleasDaoResponse;
this.observedOn = on;
this.observedAt = at;
}
public AleasUiResponse(AleasDaoResponse aleasDaoResponse) {
this();
this.aleasDaoResponse = aleasDaoResponse;
}
// гетери та сеттери
...
}
- до відповіді рівня [DAO] (рядок 11) додаються дві інформації:
- рядок 13: потік спостереження;
- рядок 15: час спостереження;
Повернемося до коду підписки:
@Override
public void createAndExecuteObservables() {
...
// виконуємо всі ці спостережувані величини
subscriptions.add(observable.subscribe(new Action1<AleasDaoResponse>() {
@Override
public void call(AleasDaoResponse aleasDaoResponse) {
showAlea(getDataFrom(aleasDaoResponse));
}
}, new Action1<Throwable>() {
@Override
public void call(Throwable th) {
// виводиться виняток
showAlea(getMessagesFromThrowable(th));
// після отримання винятку об’єкт спостереження не отримує ані onNext, ані onCompleted
// доводиться скасувати підписку вручну
doAnnuler();
}
}, new Action0() {
@Override
public void call() {
// завершення очікування
cancelWaiting();
}
}));
}
- рядки 11–18: випадок, коли спостерігач отримує виняток;
- рядок 14: знову використовується метод [showAlea] батьківського класу для відображення винятку. Метод [getMessagesFromThrowable] є методом батьківського класу [ResponseFragment], який на основі винятку формує рядок символів:
// повідомлення про виняток
protected String getMessagesFromThrowable(Throwable ex) {
// створюється список з повідомленнями про помилки зі стека винятків
List<String> messages = new ArrayList<String>();
Throwable th = ex;
while (th != null) {
messages.add(String.format("[%s, %s]", th.getClass().getName(), th.getMessage()));
th = th.getCause();
}
try {
return mapper.writeValueAsString(messages);
} catch (IOException e) {
return e.getMessage();
}
}
- рядок 11: повертається рядок jSON зі списку повідомлень про помилки (рядок 4);
Повернемося до коду підписки на об’єкт спостереження:
- рядки 19–25: код, що виконується, коли спостерігач отримує повідомлення про завершення трансляції. Тоді скасовується очікування (рядок 23), що оновлює графічний інтерфейс;
Результат виконання прикладу 01 дає результат, подібний до такого:

Кожен елемент списку — це рядок jSON із спостережуваним значенням. Поля рядка jSON такі:
- aleas: список випадкових чисел, наданий сервером;
- idClient: номер запиту (можна побачити, що відповіді надійшли у хаотичному порядку);
- on: потік виконання спостережуваного об’єкта, який надіслав це значення;
- requestAt: час запиту клієнта;
- responseAt: час відповіді сервера;
- delay: час очікування, зафіксований сервером;
- error: код помилки, повернутий сервером (0 = помилки немає);
- message: повідомлення про помилку, надіслане сервером (null = помилки немає);
- observedAt: час спостереження за значенням;
- observedOn: потік спостереження за виміряним значенням;
9.3.8.2. Exemple-02
Клас [Example02Fragment] має такий вигляд:
package android.aleas.exemples;
import android.aleas.dao.AleasDaoResponse;
import android.aleas.fragments.AleasUiResponse;
import android.aleas.fragments.Request;
import android.aleas.fragments.ResponseFragment;
import android.util.Log;
import org.codehaus.jackson.map.ObjectMapper;
import rx.Observable;
import rx.android.schedulers.AndroidSchedulers;
import rx.functions.Action0;
import rx.functions.Action1;
import rx.functions.Func1;
import rx.schedulers.Schedulers;
import java.io.IOException;
public class Example02Fragment extends ResponseFragment {
// маппери jSON
private ObjectMapper mapperAleasUiResponse;
// конструктор
public Example02Fragment() {
super();
Log.d("rxjava", "Example02Fragment constructor");
// фільтр jSON
mapperAleasUiResponse = new ObjectMapper();
}
public void createAndExecuteObservables() {
Log.d("rxjava", "Example02Fragment createAndExecuteObservables");
// запит на випадкові числа
Observable<AleasDaoResponse> observable = Observable.empty();
for (int i = 0; i < session.getNbRequests(); i++) {
// підготовка запиту
Request request = session.getRequest();
request.setId(i);
// зберігаються лише спостережувані величини з парним номером клієнта
observable = observable
.mergeWith(session.getActivity().getAleas(request).filter(new Func1<AleasDaoResponse, Boolean>() {
@Override
public Boolean call(AleasDaoResponse aleasDaoResponse) {
return aleasDaoResponse.getClientState().getIdClient() % 2 == 0;
}
})
// виконання у потоці вводу-виводу
.subscribeOn(Schedulers.io()));
}
// спостереження у потоці циклу подій
observable = observable.observeOn(AndroidSchedulers.mainThread());
// виконуємо ці спостережувані величини
subscriptions.add(observable.subscribe(new Action1<AleasDaoResponse>() {
@Override
public void call(AleasDaoResponse aleasDaoResponse) {
showAlea(getDataFrom(aleasDaoResponse));
}
}, new Action1<Throwable>() {
@Override
public void call(Throwable th) {
showAlea(getMessagesFromThrowable(th));
doAnnuler();
}
}, new Action0() {
@Override
public void call() {
// завершення очікування
cancelWaiting();
}
}));
}
private String getDataFrom(AleasDaoResponse aleasDaoResponse) {
// витягується інформація для відображення
String data;
try {
data = mapperAleasUiResponse.writeValueAsString(new AleasUiResponse(aleasDaoResponse));
} catch (IOException e) {
data = String.format("[%s,%s]", e.getClass().getName(), e.getMessage());
}
return data;
}
}
Цей приклад аналогічний попередньому (рядок 38). Однак із спостережуваних величин, отриманих у попередньому прикладі, зберігаються лише ті, що мають парний номер клієнта (рядки 42–46), завдяки методу [filter] (рядок 41).
Отримані результати такі (для 10 запитів):

9.3.8.3. Exemple-03
Клас [Example03Fragment] має такий вигляд:
package android.aleas.exemples;
import android.aleas.dao.AleasDaoResponse;
import android.aleas.fragments.Request;
import android.aleas.fragments.ResponseFragment;
import android.util.Log;
import org.codehaus.jackson.map.ObjectMapper;
import rx.Observable;
import rx.android.schedulers.AndroidSchedulers;
import rx.functions.Action0;
import rx.functions.Action1;
import rx.functions.Func1;
import rx.schedulers.Schedulers;
import java.io.IOException;
import java.util.List;
public class Example03Fragment extends ResponseFragment {
// маппери jSON
private ObjectMapper mapper;
// конструктор
public Example03Fragment() {
super();
Log.d("rxjava", "Example03Fragment constructor");
// фільтр jSON
mapper = new ObjectMapper();
}
public void createAndExecuteObservables() {
Log.d("rxjava", "Example03Fragment createAndExecuteObservables");
// запит на генерацію випадкових чисел
Observable<List<Integer>> observable = Observable.empty();
for (int i = 0; i < session.getNbRequests(); i++) {
// підготовка запиту
Request request = session.getRequest();
request.setId(i);
// конфігурація спостережуваного
observable = observable.mergeWith(session.getActivity().getAleas(request).filter(new Func1<AleasDaoResponse, Boolean>() {
@Override
public Boolean call(AleasDaoResponse aleasDaoResponse) {
return aleasDaoResponse.getClientState().getIdClient() % 2 == 0;
}
}).map(new Func1<AleasDaoResponse, List<Integer>>() {
@Override
public List<Integer> call(AleasDaoResponse aleasDaoResponse) {
return aleasDaoResponse.getAleas();
}
})
// виконання у потоці вводу-виводу
.subscribeOn(Schedulers.io()));
}
// спостереження у потоці циклу подій
observable = observable.observeOn(AndroidSchedulers.mainThread());
// виконуємо ці спостережувані об’єкти
subscriptions.add(observable
.subscribe(new Action1<List<Integer>>() {
@Override
public void call(List<Integer> aleas) {
showAlea(getDataFrom(aleas));
}
},
new Action1<Throwable>() {
@Override
public void call(Throwable th) {
showAlea(getMessagesFromThrowable(th));
doAnnuler();
}
},
new Action0() {
@Override
public void call() {
// завершення очікування
cancelWaiting();
}
}
));
}
private String getDataFrom(List<Integer> aleas) {
// витягується інформація для відображення
String data;
try {
data = mapper.writeValueAsString(aleas);
} catch (IOException e) {
data = String.format("[%s,%s]", e.getClass().getName(), e.getMessage());
}
return data;
}
}
Цей приклад аналогічний Прикладу-02:
- рядок 40: визначаються ті самі спостережувані величини, що й у Прикладі-02;
- рядок 45: кожне значення, що видається попередніми спостережуваними величинами, за допомогою методу [map] перетворюється на тип List<Integer> — список випадкових чисел, згенерованих сервером;
- рядок 58: відтепер спостережуване значення має тип List<Integer>;
Результат, отриманий для 10 запитів, такий:

9.3.8.4. Exemple-04
Клас [Example04Fragment] має такий вигляд:
package android.aleas.exemples;
import android.aleas.dao.AleasDaoResponse;
import android.aleas.fragments.Request;
import android.aleas.fragments.ResponseFragment;
import android.util.Log;
import org.codehaus.jackson.map.ObjectMapper;
import rx.Observable;
import rx.android.schedulers.AndroidSchedulers;
import rx.functions.Action0;
import rx.functions.Action1;
import rx.functions.Func1;
import rx.schedulers.Schedulers;
public class Example04Fragment extends ResponseFragment {
// маппери jSON
private ObjectMapper mapper;
// конструктор
public Example04Fragment() {
super();
Log.d("rxjava", "Example04Fragment constructor");
// фільтр jSON
mapper = new ObjectMapper();
}
public void createAndExecuteObservables() {
Log.d("rxjava", "Example03Fragment createAndExecuteObservables");
// запит на генерацію випадкових чисел
Observable<Integer> observable = Observable.empty();
for (int i = 0; i < session.getNbRequests(); i++) {
// підготовка запиту
Request request = session.getRequest();
request.setId(i);
// конфігурація спостережуваних величин
observable = observable.mergeWith(session.getActivity().getAleas(request).filter(new Func1<AleasDaoResponse, Boolean>() {
@Override
public Boolean call(AleasDaoResponse aleasDaoResponse) {
return aleasDaoResponse.getClientState().getIdClient() % 2 == 0;
}
}).flatMap(new Func1<AleasDaoResponse, Observable<Integer>>() {
@Override
public Observable<Integer> call(AleasDaoResponse aleasDaoResponse) {
return Observable.from(aleasDaoResponse.getAleas());
}
})
// виконання у потоці вводу-виводу
.subscribeOn(Schedulers.io()));
}
// спостереження у потоці циклу подій
observable = observable.observeOn(AndroidSchedulers.mainThread());
// виконуємо ці спостережувані величини
subscriptions.add(observable
.subscribe(new Action1<Integer>() {
@Override
public void call(Integer alea) {
showAlea(String.valueOf(alea));
}
},
new Action1<Throwable>() {
@Override
public void call(Throwable th) {
showAlea(getMessagesFromThrowable(th));
doAnnuler();
}
},
new Action0() {
@Override
public void call() {
// завершення очікування
cancelWaiting();
}
}
));
}
}
Цей приклад аналогічний Прикладу-03, за винятком того, що замість використання в рядку 42 методу [map] використовується метод [flatMap].
- рядок 55: слід зауважити, що відтепер тип спостережуваного значення — Integer;
Для 10 запитів отримуємо такі результати:

Цього разу спостережуваних значень більше, ніж запитів.
9.3.8.5. Exemple-05
Тепер ми опишемо процедуру, якої слід дотримуватися, щоб додати до додатка новий приклад спостережуваних величин.
Припустимо, що ми хочемо відтворити приклад [Exemple22h] із розділу 7.6.4:
package dvp.rxjava.observables.exemples;
import dvp.rxjava.observables.utils.Process;
import dvp.rxjava.observables.utils.ProcessUtils;
import rx.Observable;
import rx.observables.GroupedObservable;
public class Exemple22h {
public static void main(String[] args) throws InterruptedException {
// процес
Observable<GroupedObservable<Boolean, Integer>> obs = Observable.range(1, 10).groupBy(i -> i % 2 == 0);
Process<Integer> process = new Process<>("process", obs.concatMap(g -> g.asObservable()));
// підписки
ProcessUtils.subscribe(1, process);
}
}
- значення обсервабеля [Observable.range(1, 10)] спочатку групуються на парні та непарні за допомогою методу [groupBy] (рядок 11), а потім об’єднуються в один обсервабель за допомогою методу [concatMap] (рядок 12);
крок 1
Створюємо новий приклад у файлі [exemples.xml]:
![]() |
<!-- приклади -->
<resources>
<string-array name="exemples">
<item>Exemple-01</item>
<item>Exemple-02</item>
<item>Exemple-03</item>
<item>Exemple-04</item>
<item>Exemple-05</item>
</string-array>
</resources>
Вище було додано рядок 8. Назву прикладу можна вибрати будь-яку.
Крок 2
Дублюємо клас [Example04Fragment] у [Example05Fragment]. У цьому випадку назва задається обов’язково.
Крок 3
Змінюємо код [Example05Fragment] наступним чином:
package android.aleas.exemples;
import android.aleas.dao.AleasDaoResponse;
import android.aleas.fragments.Request;
import android.aleas.fragments.ResponseFragment;
import android.util.Log;
import org.codehaus.jackson.map.ObjectMapper;
import rx.Observable;
import rx.android.schedulers.AndroidSchedulers;
import rx.functions.Action0;
import rx.functions.Action1;
import rx.functions.Func1;
import rx.observables.GroupedObservable;
import rx.schedulers.Schedulers;
public class Example05Fragment extends ResponseFragment {
// маппери jSON
private ObjectMapper mapper;
// конструктор
public Example05Fragment() {
super();
Log.d("rxjava", "Example05Fragment constructor");
// фільтр jSON
mapper = new ObjectMapper();
}
public void createAndExecuteObservables() {
Log.d("rxjava", "Example05Fragment createAndExecuteObservables");
// інстанції функціональних інтерфейсів
// фільтр
Func1<AleasDaoResponse, Boolean> filter = new Func1<AleasDaoResponse, Boolean>() {
@Override
public Boolean call(AleasDaoResponse aleasDaoResponse) {
return aleasDaoResponse.getClientState().getIdClient() % 2 == 0;
}
};
// flatMap
Func1<AleasDaoResponse, Observable<Integer>> flatMap = new Func1<AleasDaoResponse, Observable<Integer>>() {
@Override
public Observable<Integer> call(AleasDaoResponse aleasDaoResponse) {
return Observable.from(aleasDaoResponse.getAleas());
}
};
// groupBy
Func1<Integer, Boolean> groupBy = new Func1<Integer, Boolean>() {
@Override
public Boolean call(Integer integer) {
return integer % 2 == 0;
}
};
// concatMap
Func1<GroupedObservable<Boolean, Integer>, Observable<Integer>> concatMap = new Func1<GroupedObservable<Boolean, Integer>, Observable<Integer>>() {
@Override
public Observable<Integer> call(GroupedObservable<Boolean, Integer> integerIntegerGroupedObservable) {
return integerIntegerGroupedObservable.asObservable();
}
};
// запитуються випадкові числа
Observable<Integer> observable = Observable.empty();
for (int i = 0; i < session.getNbRequests(); i++) {
// підготовка запиту
Request request = session.getRequest();
request.setId(i);
// конфігурація спостережуваного об’єкта
observable = observable.mergeWith(session.getActivity().getAleas(request).filter(filter).flatMap(flatMap))
.groupBy(groupBy).concatMap(concatMap)
// виконання у потоці вводу-виводу
.subscribeOn(Schedulers.io());
}
// спостереження у потоці циклу подій
observable = observable.observeOn(AndroidSchedulers.mainThread());
// виконуємо ці спостережувані об’єкти
subscriptions.add(observable
.subscribe(new Action1<Integer>() {
@Override
public void call(Integer alea) {
showAlea(String.valueOf(alea));
}
},
new Action1<Throwable>() {
@Override
public void call(Throwable th) {
showAlea(getMessagesFromThrowable(th));
doAnnuler();
}
},
new Action0() {
@Override
public void call() {
// завершення очікування
cancelWaiting();
}
}
));
}
}
- рядок 67: представляє спостережуваний об’єкт із прикладу 04: потік цілих чисел;
- рядок 68: ми групуємо цей потік цілих чисел за булевим критерієм, який ми визначимо. Ми отримаємо спостережуваний об’єкт типу Observable<GroupedObservable<Boolean, Integer>>, який, отже, генерує елементи типу GroupedObservable<Boolean, Integer>;
- рядок 68: метод [concatMap] генеруватиме елементи типу Integer на основі елементів типу GroupedObservable<Boolean, Integer>;
- рядки 32–59: щоб зробити створення спостережуваного об’єкта (рядки 67–69) більш зрозумілим, ми виділили екземпляри функціональних інтерфейсів, які потрібні різним операторам [filter, flatMap, groupBy, concatMap];
- рядки 47–52: метод [groupBy] очікує параметр типу Func1<T,K>, де T — тип згрупованих елементів, а K — тип критерію групування. На основі елемента T екземпляр Func1<T,K> відповідає за формування ключа групування K для цього елемента;
- рядки 48–51: елементи типу Integer будуть згруповані за парністю. Інстанція Func1<Integer,Boolean> генерує ключ true або false залежно від того, до якої групи має бути віднесено елемент. У результаті отримуємо дві групи: групу парних елементів із ключем true та групу непарних елементів із ключем false;
- рядки 53–59: метод [concatMap] очікує параметр типу Func1<T,Observable<R>> і повертає спостережуваний об’єкт з елементами типу R. Тип T тут буде типом, що видається оператором [groupBy], а саме типом GroupedObservable<Boolean, Integer>;
- рядок 57: з елемента типу [GroupedObservable<Boolean, Integer>] генерується тип Observable<Integer>. Оскільки оператор [groupBy] створив дві групи, оператор [concatMap] створить два об’єкти типу Observable типу [Observable<Integer>]. Як і [flatMap], він об’єднає їх в один об’єкт Observable. Але на відміну від [flatMap], він не змішує елементи об’єднаних об’єктів Observable. Тому ми маємо спостерігати дві ізольовані групи: парні випадкові числа та інші — непарні.
Крок 4
Запускаємо додаток:

і отримуємо такі результати:

- у [1] — парні випадкові числа, у [2] — непарні;
9.3.8.6. Щоб продовжити
Тепер читачеві пропонується створити власні приклади, а також експериментувати з різними значеннями для полів у формі, що налаштовує запити до сервера випадкових чисел.
9.3.9. Висновок
Ми створили в середовищі Android таку архітектуру:
Клієнт Android:
![]() |
Рівень [DAO] взаємодіє із сервером, який генерує випадкові числа, що відображаються на планшеті Android. Цей сервер має таку дворівневу архітектуру:
![]() |
Рівень [DAO] надсилав n запитів HTTP до сервера випадкових чисел, а рівень [swing] асинхронно очікував на їхні результати для відображення. Ці n запитів HTTP надсилалися на один і той самий сервер, який надавав однакові типи відповідей. Це дозволило нам об’єднати (mergeWith) відповіді в єдиний об’єкт спостереження.
Насправді додатки для Android звертаються до різних серверів, і, ймовірно, їхні відповіді не будуть об’єднуватися. Запити HTTP до цих серверів оброблятимуться незалежно один від одного, а їхні результати спостерігатимуться за допомогою окремих методів.

















































