Skip to content

9. RxJava у середовищі Android

9.1. Introduction

Тут ми розглянемо додаток, який вже розглядався в кількох документах:

  1. [Android pour les développeurs JEE : un modèle asynchrone pour clients Android] (розділ 4);
  2. [Introduction à la programmation de tablettes Android par l'exemple] (розділ 9);
  3. [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]. Після цього на консолі відображаються такі записи журналу:

2016-05-17 10:47:12.642  INFO 13116 --- [           main] dvp.rxjava.server.boot.Application       : Starting Application on st-PC with PID 13116 (D:\data\istia-1516\projets\rxjava\dvp\android\serveur\build\classes\main started by st in D:\data\istia-1516\projets\rxjava\dvp\android\serveur)
2016-05-17 10:47:12.647  INFO 13116 --- [           main] dvp.rxjava.server.boot.Application       : No active profile set, falling back to default profiles: default
2016-05-17 10:47:12.706  INFO 13116 --- [           main] ationConfigEmbeddedWebApplicationContext : Refreshing org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext@71623278: startup date [Tue May 17 10:47:12 CEST 2016]; root of context hierarchy
2016-05-17 10:47:13.736  INFO 13116 --- [           main] s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat initialized with port(s): 8080 (http)
2016-05-17 10:47:13.749  INFO 13116 --- [           main] o.apache.catalina.core.StandardService   : Starting service Tomcat
2016-05-17 10:47:13.750  INFO 13116 --- [           main] org.apache.catalina.core.StandardEngine  : Starting Servlet Engine: Apache Tomcat/8.0.33
2016-05-17 10:47:13.914  INFO 13116 --- [ost-startStop-1] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring embedded WebApplicationContext
2016-05-17 10:47:13.914  INFO 13116 --- [ost-startStop-1] o.s.web.context.ContextLoader            : Root WebApplicationContext: initialization completed in 1214 ms
2016-05-17 10:47:13.965  INFO 13116 --- [ost-startStop-1] o.s.b.c.e.ServletRegistrationBean        : Mapping servlet: 'dispatcherServlet' to [/*]
2016-05-17 10:47:14.251  INFO 13116 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/{a}/{b}/{minCount}/{maxCount}/{minDelay}/{maxDelay}],methods=[GET],produces=[application/json]}" onto public java.lang.String dvp.rxjava.server.web.AleasController.getAleas(int,int,int,int,int,int) throws com.fasterxml.jackson.core.JsonProcessingException
2016-05-17 10:47:14.342  INFO 13116 --- [           main] s.w.s.m.m.a.RequestMappingHandlerAdapter : Looking for @ControllerAdvice: org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext@71623278: startup date [Tue May 17 10:47:12 CEST 2016]; root of context hierarchy
2016-05-17 10:47:14.485  INFO 13116 --- [           main] s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat started on port(s): 8080 (http)
2016-05-17 10:47:14.489  INFO 13116 --- [           main] dvp.rxjava.server.boot.Application       : Started Application in 2.289 seconds (JVM running for 2.859)
2016-05-17 10:48:37.061  INFO 13116 --- [nio-8080-exec-2] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring FrameworkServlet 'dispatcherServlet'
2016-05-17 10:48:37.061  INFO 13116 --- [nio-8080-exec-2] o.s.web.servlet.DispatcherServlet        : FrameworkServlet 'dispatcherServlet': initialization started
2016-05-17 10:48:37.087  INFO 13116 --- [nio-8080-exec-2] o.s.web.servlet.DispatcherServlet        : FrameworkServlet 'dispatcherServlet': initialization completed in 26 ms
  • рядок 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 (Модель – Вигляд – Контролер), наступним чином:

Обробка запиту клієнта відбувається наступним чином:

  1. запит — запитувані 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);
  1. обробка
  • обрана дія може використовувати параметри parami, які їй передала сервлета [Dispatcher Servlet]. Вони можуть походити з кількох джерел:
    • з шляху [/param1/param2/...] сервлета URL,
    • з параметрів [p1=v1&p2=v2] сервлета URL,
    • з параметрів, надісланих браузером разом із запитом;
  • під час обробки запиту користувача дії може знадобитися рівень [metier] [2b]. Після обробки запиту клієнта це може викликати різні відповіді. Класичним прикладом є:
    • сторінка з повідомленням про помилку, якщо запит не вдалося обробити належним чином
    • сторінка підтвердження в іншому випадку
  • дія вимагає відображення певного виду [3]. Цей вид відображатиме дані, які називаються моделлю виду. Це «M» у MVC. Дія створить цю модель M [2c] і вимагатиме відображення виду V [3];
  1. відповідь — обраний вигляд 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]

Користувач

Сервер

Клієнт матиме два компоненти:

  1. рівень [Présentation] (види + активність);
  2. рівень [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 починається з такого початкового вікна:

Image

Щоб дізнатися, що вказати в полі [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 міг підключитися до сервера випадкових чисел.

Виконання асинхронних запитів із зазначеними вище даними дає такі результати:

Image

Кожен запит генерує відповідь 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] очікує три параметри:
      1. метод HTTP, який слід використовувати для запиту до сервісу;
      2. URL сервісу;
      3. об’єкт для відправлення типу 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. Види додатка

  

Додаток має два такі види:

Вигляд запиту

Image

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

Image

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]

Фрагмент запиту має такі компоненти:

Image

Додаток має єдиний вікно, яке складається з двох вкладок:

  • [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
кнопка, яка запускає виклики до служби генерації чисел;

Повідомляється про помилки введення:

Image

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

9.3.7.3. Фрагмент [ResponseFragment] відповіді

Фрагмент відповіді містить такі компоненти:

Image

Тип
Назва
Роль
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];

Завдяки різним записам у журналах, розміщеним у важливих методах, можна побачити, що відбувається під час запуску програми:

05-17 08:45:05.803 14158-14158/android.aleas D/rxjava: RequestFragment constructor
05-17 08:45:05.804 14158-14158/android.aleas D/rxjava: ResponseFragment (Example01Fragment{c6fd1a7}) constructor
05-17 08:45:05.804 14158-14158/android.aleas D/rxjava: Example01Fragment constructor
05-17 08:45:05.810 14158-14158/android.aleas D/rxjava: ResponseFragment (Example02Fragment{ba75654}) constructor
05-17 08:45:05.810 14158-14158/android.aleas D/rxjava: Example02Fragment constructor
05-17 08:45:05.810 14158-14158/android.aleas D/rxjava: ResponseFragment (Example03Fragment{b8589fd}) constructor
05-17 08:45:05.810 14158-14158/android.aleas D/rxjava: Example03Fragment constructor
05-17 08:45:05.810 14158-14158/android.aleas D/rxjava: ResponseFragment (Example04Fragment{e9506f2}) constructor
05-17 08:45:05.810 14158-14158/android.aleas D/rxjava: Example04Fragment constructor
05-17 08:45:05.934 14158-14158/android.aleas D/rxjava: RequestFragment onCreateView
05-17 08:45:05.962 14158-14158/android.aleas D/rxjava: ResponseFragment (Example01Fragment{c6fd1a7 #1 id=0x7f0d006e android:switcher:2131558510:1}) onCreateView
05-17 08:45:05.969 14158-14158/android.aleas D/rxjava: ResponseFragment (Example02Fragment{ba75654 #2 id=0x7f0d006e android:switcher:2131558510:2}) onCreateView
05-17 08:45:05.972 14158-14158/android.aleas D/rxjava: ResponseFragment (Example03Fragment{b8589fd #3 id=0x7f0d006e android:switcher:2131558510:3}) onCreateView
05-17 08:45:05.978 14158-14158/android.aleas D/rxjava: ResponseFragment (Example04Fragment{e9506f2 #4 id=0x7f0d006e android:switcher:2131558510:4}) onCreateView
  • рядок 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 дає результат, подібний до такого:

Image

Кожен елемент списку — це рядок 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 запитів):

Image

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 запитів, такий:

Image

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 запитів отримуємо такі результати:

Image

Цього разу спостережуваних значень більше, ніж запитів.

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

Запускаємо додаток:

Image

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

Image

  • у [1] — парні випадкові числа, у [2] — непарні;

9.3.8.6. Щоб продовжити

Тепер читачеві пропонується створити власні приклади, а також експериментувати з різними значеннями для полів у формі, що налаштовує запити до сервера випадкових чисел.

9.3.9. Висновок

Ми створили в середовищі Android таку архітектуру:

Клієнт Android:

Рівень [DAO] взаємодіє із сервером, який генерує випадкові числа, що відображаються на планшеті Android. Цей сервер має таку дворівневу архітектуру:

Рівень [DAO] надсилав n запитів HTTP до сервера випадкових чисел, а рівень [swing] асинхронно очікував на їхні результати для відображення. Ці n запитів HTTP надсилалися на один і той самий сервер, який надавав однакові типи відповідей. Це дозволило нам об’єднати (mergeWith) відповіді в єдиний об’єкт спостереження.

Насправді додатки для Android звертаються до різних серверів, і, ймовірно, їхні відповіді не будуть об’єднуватися. Запити HTTP до цих серверів оброблятимуться незалежно один від одного, а їхні результати спостерігатимуться за допомогою окремих методів.