Skip to content

9. RxJava w środowisku Android

9.1. Introduction

W tym miejscu powrócimy do aplikacji omówionej już w kilku dokumentach:

  1. [Android pour les développeurs JEE : un modèle asynchrone pour clients Android] (rozdział 4);
  2. [Introduction à la programmation de tablettes Android par l'exemple] (rozdział 9);
  3. [Introduction à la programmation de tablettes Android par l'exemple - version 2] (punkt 1.11);

Dotyczy ona aplikacji typu klient/serwer, w której serwer asynchronicznie dostarcza liczby losowe, a klient z systemem Android je wyświetla:

  • w dokumencie 1 klient Android wykorzystuje technologię niestandardową;
  • w dokumencie 2 klient Android wykorzystuje standardową technologię systemu Android do operacji asynchronicznych;
  • w dokumencie 3 klient Android wykorzystuje tę samą technologię co w dokumencie 2, ale uproszczoną dzięki zastosowaniu adnotacji z biblioteki Android Annotations;

Klient Android wygląda następująco:

Warstwa [DAO] komunikuje się z serwerem, który generuje liczby losowe wyświetlane na tablecie z systemem Android. Serwer ten ma następującą architekturę dwuwarstwową:

Klienci wysyłają zapytania do niektórych elementów warstwy [web / JSON] o nazwie URL i otrzymują odpowiedź tekstową w formacie JSON (JavaScript Object Notation).

Analizę aplikacji podzielimy na dwa etapy:

Serwer WWW / jSON

  • jego warstwa [métier];
  • jego usługę [web / JSON] zaimplementowaną przy użyciu Springa MVC;

Klient Android

  • jego warstwa [DAO];
  • jego aktywność;
  • jego widoki;

9.2. Usługa internetowa / jSON

Uwaga: usługa internetowa / jSON jest zaimplementowana przy użyciu technologii Spring MVC. Czytelnik, który nie zna tej technologii, może:

  • zadowolić się przeczytaniem paragrafu 9.2.1, który wyjaśnia, jak uruchomić serwer i jak wysyłać do niego zapytania;
  • zapoznać się z dokumentem [Spring MVC et Thymeleaf par l'exemple], a zwłaszcza z rozdziałem 4, który przedstawia główne adnotacje używane w kodzie;

9.2.1. Projekt IntelliJ IDEA

Serwis internetowy / jSON ma następującą architekturę:

Architektura ta została zaimplementowana w następującym projekcie IntelliJ IDEA: [1]:

Serwer jest uruchamiany przez plik [2-3]. Następnie na konsoli wyświetlane są logi:

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
  • wiersz 12: wskazuje, że usługa jest dostępna na porcie 8080;
  • wiersz 10: jedyny identyfikator URL usługi internetowej / jSON dostępny poprzez operację HTTP GET. Jego parametry są następujące:
    • [a,b]: przedział generowania liczb losowych;
    • [minCount, maxCount]: generowanych jest count liczb losowych, gdzie count jest liczbą losową z przedziału [minCount, maxCount];
    • [minDelay, maxDelay]: usługa czeka delay milisekund przed zwróceniem żądanych liczb, gdzie delay jest liczbą losową z przedziału [minDelay, maxDelay];

W przeglądarce wywołajmy tę wartość URL:

 

Zapytałem o:

  • liczb losowych z przedziału [100, 200];
  • n liczb losowych z n w przedziale [10, 20];
  • czas oczekiwania wynoszący x milisekund, przy czym x mieści się w przedziale [300, 400];

W odpowiedzi:

  • aleas: lista wygenerowanych liczb losowych;
  • delay: czas oczekiwania w milisekundach, który serwer zapamiętał;
  • błąd: kod błędu – 0, jeśli nie wystąpił błąd;
  • komunikat: komunikat o błędzie – null, jeśli nie wystąpił błąd;

9.2.2. Zależności Gradle projektu

  

Projekt [serveur] to projekt Gradle skonfigurowany przez następujący plik [build.gradle] [1]:


// wygenerowane przez http://start.spring.io/ (maj 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')
}
  • wiersz 1: komentarz wyjaśniający, w jaki sposób wygenerowano ten plik konfiguracyjny;
  • wiersze 4 i 10: zależność od frameworka [Spring Boot], gałęzi ekosystemu Spring. Framework ten ([http://projects.spring.io/spring-boot/]) umożliwia minimalną konfigurację Springa. Na podstawie plików znajdujących się w ścieżce Classpath projektu, [Spring Boot] wnioskuje o prawdopodobnej lub możliwej konfiguracji projektu. Jeśli więc Hibernate znajduje się w ścieżce Classpath projektu, wówczas [Spring Boot] wywnioskuje, że używaną implementacją JPA będzie Hibernate i skonfiguruje Spring zgodnie z tym założeniem. Programista nie musi już tego robić. Pozostaje mu jedynie wprowadzić te konfiguracje, których [Spring Boot] nie ustawił domyślnie, lub te, które [Spring Boot] ustawił domyślnie, ale które wymagają doprecyzowania. W każdym przypadku ostateczne słowo należy do programisty;
  • wiersze 14–15: dwie wtyczki Gradle niezbędne do wykorzystania zawartości tego pliku Gradle;
  • wiersze 17–20: definiują cechy archiwum generowanego dla tego projektu;
  • wiersze 22–23: zapewniają zgodność z Javą 8;
  • wiersze 25–27: zależności będą wyszukiwane w globalnym repozytorium Maven lub w lokalnym repozytorium na komputerze;
  • wiersz 30: definiuje zależność od artefaktu [spring-boot-starter-web]. Artefakt ten zawiera wszystkie archiwa niezbędne dla projektu Spring MVC. Wśród nich znajduje się archiwum serwera Tomcat. To właśnie on zostanie wykorzystany do wdrożenia aplikacji internetowej. Należy zauważyć, że nie podano wersji zależności. Zastosowana zostanie wersja podana w importowanym projekcie [spring-boot];

Aby zaktualizować projekt, należy wymusić pobranie zależności [1-3]:

Przyjrzyjmy się plikowi [4] i zależnościom wprowadzonym przez ten plik [build.gradle]:

 

Jest ich bardzo wiele. Spring Boot dla sieci zawarł zależności, których aplikacja internetowa Spring MVC prawdopodobnie będzie potrzebować. Oznacza to, że niektóre z nich mogą być zbędne. Spring Boot idealnie nadaje się do samouczka:

  • dostarcza zależności, których prawdopodobnie będziemy potrzebować;
  • przekonamy się, że znacznie upraszcza konfigurację projektu Spring MVC;
  • zawiera wbudowany serwer Tomcat [1], co pozwala nam uniknąć wdrażania aplikacji na zewnętrznym serwerze internetowym;
  • umożliwia wygenerowanie pliku JAR z możliwością uruchomienia, zawierającego wszystkie powyższe zależności. Plik ten można przenosić z jednej platformy na drugą bez konieczności ponownej konfiguracji.

Na stronie ekosystemu Spring [http://spring.io/guides] można znaleźć wiele przykładów wykorzystujących Spring Boot. Teraz, gdy znamy już zależności projektu, możemy przejść do kodu.

9.2.3. Warstwa [métier]

  

Warstwa [métier] będzie miała następujący interfejs [IMetier]:


package dvp.rxjava.server.metier;

public interface IMetier {
  // liczby losowe w przedziale [a,b]
  // generowanych jest n liczb, gdzie n samo w sobie jest liczbą losową z przedziału [minCount, maxCount]
  // liczby są generowane po upływie opóźnienia wynoszącego delay milisekund,
  // gdzie [delay] jest liczbą losową z przedziału [minDelay, maxDelay]
  public AleasMetier getAleas(int a, int b, int minCount, int maxCount, int minDelay, int maxDelay);
}

Interfejs ten jest niemal identyczny z tym omówionym w środowisku Swing w paragrafie 8.4. W wierszu 8 metoda [getAleas] zwraca następujący typ [AleasMetier]:


package dvp.rxjava.server.metier;

import java.util.List;

public class AleasMetier {
  // pola
  private int delay;
  private List<Integer> aleas;

  // konstruktory
  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;
  }

  // metody pobierające i ustawiające
...
}

Kod klasy [Metier] implementującej interfejs [IMetier] jest następujący:


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) {
    // liczby losowe w przedziale [a,b]
    // generowanych jest n liczb, gdzie n samo w sobie jest liczbą losową z przedziału [minCount, maxCount]
    // liczby są generowane po upływie opóźnienia wynoszącego delay milisekund,
    // gdzie [delay] jest liczbą losową z przedziału [minDelay, maxDelay]

    // kilka sprawdzeń
    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;
    }
    // błędy?
    if (!messages.isEmpty()) {
      throw new AleasException(String.join(" [---] ", messages), erreur);
    }
    // generator liczb losowych
    Random random = new Random();
    // oczekiwanie?
    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);
      }
    }
    // generowanie wyniku
    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));
    }
    // zwrot wyniku
    return new AleasMetier(delay,nombres);
  }

}

Nie komentujemy tej klasy: jest ona analogiczna do tej, którą omówiono w środowisku Swing w paragrafie 8.4. Zwróćmy jedynie uwagę na następujące kwestie:

  • wiersz 10: adnotacja Spring [@Service], która sprawi, że Spring utworzy tylko jedną instancję klasy (singleton) i udostępni jej odwołanie innym komponentom Spring. W tym miejscu można było użyć innych adnotacji Spring, aby uzyskać ten sam efekt;
  • wiersze 13–14: wstrzykuje się mapper jSON. Spring jest kontenerem obiektów. Kontener ten jest instancjonowany podczas uruchamiania aplikacji internetowej, a obiekty zdefiniowane w pliku konfiguracyjnym są wówczas instancjonowane – domyślnie w jednym egzemplarzu (singleton). Singleton Springa może zawierać odwołania do innych obiektów Springa. Tak jest w tym przypadku: singleton [metier] (wiersze 10–11) będzie posiadał odwołanie do singletonu [mapper] (wiersze 13–14). Nazywa się to wstrzykiwaniem zależności. Istnieją dwa sposoby wstrzyknięcia singletonu do innego singletonu:
    • według typu: jest to możliwe, jeśli singleton, który ma zostać wstrzyknięty, jest jedynym obiektem Spring o tym typie. Tak jest w tym przypadku przy wstrzykiwaniu w wierszach 13–14 (typ ObjectMapper);
    • według nazwy, jeśli kilka obiektów Spring ma ten sam typ. Należy wówczas dodać adnotację @Qualifier(„nomDuSingleton”), aby określić nazwę singletonu;

Klasa [Metier] generuje wyjątki typu [AleaException]:


package android.exemples.server.metier;

public class AleaException extends RuntimeException {

  // kod błędu
  private int code;

  // konstruktory
  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;
  }

  // metody pobierające i ustawiające

  public int getCode() {
    return code;
  }

  public void setCode(int code) {
    this.code = code;
  }
}
  • wiersz 3: [AleasException] dziedziczy po klasie [RuntimeException]. Jest to zatem wyjątek niekontrolowany (nie ma obowiązku obsługi go za pomocą try / catch);
  • wiersz 6: do klasy [RuntimeException] dodano kod błędu;

9.2.4. Usługa internetowa / JSON

  

Usługa internetowa / JSON jest zaimplementowana przez Spring MVC. Spring MVC implementuje model architektury zwany MVC (Model – Widok – Kontroler) w następujący sposób:

Przetwarzanie żądania klienta przebiega w następujący sposób:

  1. żądanie – żądane obiekty URL mają postać http://machine:port/contexte/Action/param1/param2/....?p1=v1&p2=v2&... [Dispatcher Servlet] to klasa Springa, która przetwarza przychodzące obiekty URL. „Przekierowuje” ona URL do akcji, która ma ją przetworzyć. Akcje te są metodami określonych klas o nazwach [Contrôleurs]. Litera „C” w MVC oznacza tutaj ciąg znaków [Dispatcher Servlet, Contrôleur, Action]. Jeśli nie skonfigurowano żadnej akcji do przetworzenia przychodzącego URL, serwlet [Dispatcher Servlet] odpowie, że żądany URL nie został znaleziony (błąd 404 NOT FOUND);
  1. przetwarzanie
  • wybrana akcja może wykorzystać parametry parami, które przekazała jej serwlet [Dispatcher Servlet]. Mogą one pochodzić z kilku źródeł:
    • ze ścieżki [/param1/param2/...] serwletu URL,
    • z parametrów [p1=v1&p2=v2] serwletu URL,
    • z parametrów przesłanych przez przeglądarkę wraz z żądaniem;
  • podczas przetwarzania żądania użytkownika akcja może wymagać warstwy [metier] [2b]. Po przetworzeniu żądania klienta może ono wywołać różne odpowiedzi. Klasycznym przykładem jest:
    • strona błędu, jeśli żądanie nie mogło zostać poprawnie przetworzone
    • w przeciwnym razie strona potwierdzenia
  • akcja żąda wyświetlenia określonego widoku [3]. Widok ten wyświetli dane, które nazywamy modelem widoku. To właśnie litera M w MVC. Akcja utworzy ten model M o nazwie [2c] i zażąda wyświetlenia widoku V o nazwie [3];
  1. odpowiedź – wybrany widok V wykorzystuje model M utworzony przez akcję do zainicjowania dynamicznych części odpowiedzi HTML, którą musi wysłać do klienta, a następnie wysyła tę odpowiedź.

W przypadku usługi internetowej / JSON powyższa architektura ulega niewielkiej modyfikacji:

  • w [4a] model, który jest klasą Java, jest przekształcany w ciąg znaków JSON przez bibliotekę JSON;
  • w [4b] ten ciąg znaków JSON jest wysyłany do przeglądarki;

Wróćmy do warstwy [web] naszej aplikacji:

W naszej aplikacji występuje tylko jeden kontroler:

  

Usługa internetowa / JSON wyśle do swoich klientów odpowiedź typu [AleasResponse] o następującej treści:


package dvp.rxjava.server.web;

import dvp.rxjava.server.metier.AleasMetier;

public class AleasResponse extends AleasMetier {

  // kod błędu
  private int erreur;
  // komunikat o błędzie
  private String message;

  // konstruktory
  public AleasResponse() {

  }

  public AleasResponse(int erreur, String message, AleasMetier aleasMetier) {
    super(aleasMetier);
    this.erreur = erreur;
    this.message = message;
  }
  // metody pobierające i ustawiające

  public void setAleasMetier(AleasMetier aleasMetier) {
    this.setDelay(aleasMetier.getDelay());
    this.setAleas(aleasMetier.getAleas());
  }
...
}
  • wiersz 5: klasa [AleasResponse] rozszerza klasę [AleasMetier] i w związku z tym przejmuje wszystkie jej atrybuty (aleas, delay);
  • wiersz 8: kod błędu (0, jeśli nie ma błędu);
  • wiersz 10: jeśli erreur!=0, to komunikat o błędzie; jeśli nie ma błędu, to null;

Kontroler [AleasController] wygląda następująco:


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 {

    // warstwa biznesowa
    @Autowired
    private IMetier metier;
    @Autowired
    private ObjectMapper mapper;

    // liczby losowe w [a,b]
    // n liczb jest generowanych, gdzie n mieści się w przedziale [minCount, maxCount]
    // liczby są generowane po upływie opóźnienia wynoszącego milisekund,
    // gdzie [delay] jest liczbą losową z przedziału [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 {

        // przygotowuje się odpowiedź
        AleasResponse response = new AleasResponse();
        // wykorzystuje się warstwę biznesową do generowania liczb losowych
        try {
            response.setAleasMetier(metier.getAleas(a, b, minCount, maxCount, minDelay, maxDelay));
        } catch (AleasException e) {
            // sytuacja błędu (kod i komunikat)
            response.setErreur(e.getCode());
            response.setMessage(e.getMessage());
        }
        // zwracamy odpowiedź jSON
        return mapper.writeValueAsString(response);
    }
}
  • wiersz 16: adnotacja [@Controller] sprawia, że klasa [AleasController] staje się singletonem Springa. Wskazuje ona ponadto, że klasa zawiera metody, które będą obsługiwać żądania dotyczące niektórych elementów URL aplikacji internetowej. W tym przypadku jest tylko jedna, w wierszu 29;
  • wiersze 20–21: adnotacja [@Autowired] nakazuje Springowi wstrzyknięcie do pola komponentu typu [IMetier]. Będzie to poprzednia klasa [Metier]. Dzieje się tak, ponieważ umieściliśmy na niej adnotację [@Service], dzięki czemu jest ona traktowana jako komponent Springa;
  • wiersze 22–23: adnotacja [@Autowired] nakazuje Springowi wstrzyknięcie do pola komponentu typu [ObjectMapper]. Zdefiniujemy go wkrótce;
  • wiersz 31: metoda [getAleas] generuje liczby losowe. Jej nazwa nie ma znaczenia. W momencie jej wykonania parametry z wierszy 31–33 zostały zainicjowane przez Spring MVC. Zobaczymy, w jaki sposób. Ponadto metoda ta jest uruchamiana, ponieważ serwer WWW otrzymał żądanie HTTP GET dotyczące URL z wiersza 29 (atrybut method);
  • wiersz 30: adnotacja [@ResponseBody] wskazuje, że wynik metody musi zostać wysłany do klienta w niezmienionej postaci. W tym przypadku wyślemy do niego ciąg znaków, którym będzie ciąg jSON typu [AleasResponse];
  • wiersz 29: przetwarzany ciąg URL ma postać /{a}/{b}/{minCount}/{maxCount}/{minDelay}/{maxDelay}, gdzie {x} oznacza zmienną. Te różne zmienne są przypisywane do parametrów metody w wierszach 32–33. Odbywa się to za pomocą adnotacji @PathVariable("x"). Należy zauważyć, że wartości {x} są składnikami typu URL, a zatem są typu String. Konwersja typu String na typ parametrów metody może się nie powieść. W takim przypadku Spring MVC zgłasza wyjątek. Podsumowując: jeśli za pomocą przeglądarki wywołam URL /100/200/10/20/300/400, metoda getAleas z linii 31 zostanie wykonana z parametrami a=100 (linia 31), b=200 (wiersz 31), minCount=10 (wiersz 31), maxCount=20 (wiersz 32), minDelay=300 (wiersz 32), maxDelay=400 (wiersz 33);
  • wiersz 39: żądamy od warstwy [métier] listy liczb losowych. Należy pamiętać, że metoda [metier].getAleas może wygenerować wyjątek;
  • wiersze 42–43: obsługa błędu;
  • wiersz 46: odpowiedź typu [AleasResponse] jest zwracana w postaci ciągu znaków jSON;

9.2.5. Konfiguracja projektu Spring

  

Istnieje wiele sposobów konfiguracji Springa:

  • za pomocą plików XML;
  • za pomocą kodu Java;
  • poprzez połączenie obu tych metod;

Zdecydowaliśmy się skonfigurować naszą aplikację internetową za pomocą kodu Java. Konfigurację tę zapewnia powyższa klasa [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 {
  // -------------------------------- konfiguracja warstwy [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);
  }

  // mapper jSON
  @Bean
  public ObjectMapper jsonMapper() {
    return new ObjectMapper();
  }
}
  • wiersz 15: informujemy Spring, w jakich pakietach znajdzie obiekty do instancjonowania. Znajdzie dwa:
    • klasę [Metier] opatrzoną adnotacją [@Service];
    • klasę [AleasController] opatrzoną adnotacją [@Controller];
  • wiersz 16: adnotacja [@EnableWebMvc] powoduje automatyczną konfigurację frameworka Spring MVC;
  • wiersze 19–20: wstrzyknięcie kontekstu Spring (kontener obiektów Spring). Wstrzyknięcie to jest konieczne, ponieważ obiekt z wierszy 22–26 go wymaga;
  • plik konfiguracyjny Springa może definiować nowe obiekty Springa za pomocą metod opatrzonych adnotacją [@Bean]. Wynik działania tej metody staje się wówczas obiektem Springa;
  • wiersze 22–26: definicja serwletu frameworka Spring MVC, który przekierowuje żądania HTTP do właściwego kontrolera i właściwej metody. [DispatcherServlet] jest klasą Springa;
  • wiersze 28–31: wskazano, że ten serwlet obsługuje wszystkie żądania URL;
  • wiersze 33–36: to właśnie obecność tej fasoli spowoduje uruchomienie serwera Tomcat znajdującego się w archiwach projektu. Będzie on oczekiwał na żądania na porcie 8080;
  • wiersze 39–42: mapper jSON. To właśnie on został wstrzyknięty do obiektów Springa [Metier] i [AleasController];

9.2.6. Uruchomienie serwera WWW

  

Projekt uruchamia się z następującej klasy wykonywalnej [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) {
    // uruchomienie aplikacji
    SpringApplication.run(Config.class, args);
  }

}
  • wiersz 6: klasa [Application] jest klasą wykonywalną (wiersze 7–10);
  • wiersz 9: metoda statyczna [SpringApplication.run] jest metodą klasy [spring Boot] (wiersz 4), która uruchamia aplikację. Jej pierwszym parametrem jest klasa Java konfigurująca projekt. W tym przypadku jest to klasa [Config], którą właśnie opisaliśmy. Drugim parametrem jest tablica argumentów przekazywana do metody [main] (wiersz 7). W tym przypadku nie będzie żadnych argumentów;

Aby zapoznać się z samym procesem wykonywania, czytelnik powinien powrócić do punktu 9.2.1.

9.3. Klient Android

Uwaga: poniższy projekt dla systemu Android jest dość złożony. Wymaga on dobrej znajomości systemu Android, którą można znaleźć na przykład w [Introduction à la programmation de tablettes Android avec Android Studio ].

Aktywność

Widoki

Warstwa

[DAO]

Użytkownik

Serwer

Klient będzie miał dwa komponenty:

  1. warstwę [Présentation] (widoki + aktywność);
  2. warstwę [DAO], która komunikuje się z usługą [web / JSON], którą omówiliśmy wcześniej.

9.3.1. RxAndroid

Aby komunikować się asynchronicznie z serwerem liczb losowych, klient na Androida będzie korzystał z biblioteki RxAndroid. Biblioteka ta rozszerza RxJava na środowisko Androida. Podobnie jak w przypadku aplikacji Swing, będziemy korzystać tylko z jednego rozszerzenia wprowadzonego przez bibliotekę RxAndroid, a mianowicie z harmonogramu [AndroidSchedulers.mainThread()]. Interfejs graficzny Androida działa na tych samych zasadach co interfejs Swing:

  • zdarzenia są przetwarzane w jednym wątku zwanym pętlą zdarzeń (event loop) lub wątkiem interfejsu użytkownika;
  • gdy zdarzenie uruchamia operacje asynchroniczne, ich wyniki muszą zostać pobrane w wątku interfejsu użytkownika, jeśli mają służyć do aktualizacji interfejsu;

Klient Android:

  • wysyłać będzie kilka asynchronicznych żądań do serwera liczb losowych. Żądania te będą realizowane po stronie klienta przy użyciu wątków harmonogramu [Schedulers.io()];
  • te asynchroniczne żądania zwrócą obserwowalne, które zostaną połączone w jedną (merge);
  • ten obserwowalny będzie monitorowany po stronie klienta w harmonogramie [AndroidSchedulers.mainThread()], uruchamianym przez RxAndroid;

9.3.2. Projekt IntelliJ IDEA

Projekt na Androida nosi nazwę [client]:

Uruchamia się go za pomocą [2].

Uwaga: uruchomienie w dużym stopniu zależy od konfiguracji używanego środowiska IntelliJ IDEA. Jest prawdopodobne, że powyższe uruchomienie [2] nie zadziała za pierwszym razem na komputerze innym niż mój. Prawidłowe skonfigurowanie programu IntelliJ IDEA w celu uruchomienia tego projektu może stanowić spore wyzwanie dla początkujących. Oto kilka kwestii, na które należy zwrócić uwagę:

  • w [3] należy uzyskać dostęp do struktury projektu;
  • w [4-5], JDK oraz SDK dla systemu Android zainstalowane na moim komputerze. Należy zauważyć, że wersja JDK 1.8 nie jest niezbędna. Android nie obsługuje niektórych funkcji języka Java 8, w tym lambd. Dlatego do instancjonowania interfejsów funkcjonalnych będziemy używać klas anonimowych. Wystarczy więc wersja JDK 1.6. Jednak projekt w postaci, w jakiej jest dystrybuowany, został skonfigurowany z wersją JDK 1.8;

Plik [build.gradle] [6], który konfiguruje projekt Android, wygląda następująco:


buildscript {
  repositories {
    mavenCentral()
    mavenLocal()
  }
  dependencies {
    // zastąpić aktualną wersją wtyczki dla systemu 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'
  }
}

W zależności od obecnych plików SDK dla Androida może zaistnieć konieczność zmiany wersji w wierszach 8, 24–25 i 29.

Aby zainstalować nowe pliki SDK dla systemu Android, należy użyć menedżera SDK w następujący sposób:

Projekt został skonfigurowany dla:

  • SDK, API, 23, [2];
  • SDK Build-tools 23.0.3 [3];
  • SDK Tool 25.1.3 [4]

Na koniec sprawdź ścieżkę do pliku SDK Android w pliku [local.properties] [4], w wierszu 11 poniżej:


## Ten plik jest generowany automatycznie przez Android Studio.
# Nie modyfikuj tego pliku -- YOUR CHANGES WILL BE ERASED!
#
# Ten plik musi *NOT* zostać dodany do systemu kontroli wersji,
# ponieważ zawiera informacje specyficzne dla lokalnej konfiguracji.
#
# Lokalizacja pliku SDK. Jest on używany wyłącznie przez Gradle.
# Aby dostosować ustawienia podczas korzystania z systemu kontroli wersji, zapoznaj się z
# uwagę w nagłówku.
#czw., 7 kwietnia, godz. 14:51:14 CEST 2016
sdk.dir=C\:\\Users\\st\\AppData\\Local\\Android\\sdk

9.3.3. Uruchomienie projektu w IntelliJ IDEA

Po utworzeniu odpowiedniego środowiska dla projektu można go uruchomić w następujący sposób:

  • w przypadku [1] uruchamia się emulator Androida Genymotion;
  • w [2] uruchamia się konfigurację uruchomienia [app];
  • w [3] tworzy się konfigurację uruchomienia;
 
  • w [1, 3] konfiguracja została nazwana [app];
  • na [2], odpowiada to uruchomieniu modułu o nazwie [app];
  • w [4] wymagamy, aby podczas uruchamiania IDE zaproponował nam urządzenie do uruchomienia. W tym przypadku będzie to zawsze emulator Genymotion;
  • w pliku [5] określa się, że urządzenie to ma być zachowane dla wszystkich uruchomień tej konfiguracji;

Uruchomienie projektu na emulatorze Genymotion rozpoczyna się od następującego komunikatu początkowego:

Image

Aby dowiedzieć się, co wpisać w polu [1], otwórz okno poleceń DOS i wpisz następujące polecenie [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


W polu [1] wpisz jeden z adresów IP swojego komputera (wiersze 20, 28, 32). Jeśli korzystasz z zapory systemu Windows, prawdopodobnie będziesz musiał ją wyłączyć, aby emulator Androida mógł połączyć się z serwerem liczb losowych.

Wykonanie żądań asynchronicznych z powyższymi danymi daje następujące wyniki:

Image

Każde zapytanie generuje odpowiedź jSON zawierającą następujące pola:

  • aleas: liczby losowe wygenerowane przez serwer;
  • idClient: numer żądania;
  • on: wątek wykonania żądania po stronie klienta;
  • requestAt: czas wysłania żądania;
  • responseAt: czas otrzymania odpowiedzi;
  • delay: czas oczekiwania, jaki serwer odnotował przed wysłaniem odpowiedzi;
  • błąd: kod błędu – 0, jeśli nie wystąpił błąd;
  • komunikat: komunikat o błędzie – null, jeśli nie wystąpił błąd;
  • observedAt: czas obserwacji odpowiedzi;
  • observedOn: wątek, w którym odnotowano odpowiedź. W tym przypadku będzie to zawsze [main], co oznacza wątek interfejsu użytkownika;

Ponieważ żądania są asynchroniczne, a czasy oczekiwania narzucone serwerowi są losowe, odpowiedzi powracają w nieuporządkowanej kolejności.

9.3.4. Zależności projektu w Gradle

Projekt wymaga zależności, które wpisujemy do pliku [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'
}
  • zależności z wierszy 2–3 to standardowe zależności projektu Android z plikiem SDK 23;
  • zależność linii 5 powoduje załadowanie obiektu Spring [RestTemplate], który obsługuje komunikację warstwy [DAO] z serwerem;
  • zależność z wiersza 6 powoduje załadowanie biblioteki JSON oraz [Jackson], z których korzysta aplikacja;
  • zależność z linii 7 powoduje dołączenie biblioteki RxAndroid (a wraz z nią biblioteki RxJava), której warstwa Ui używa do komunikacji z warstwą [DAO];

9.3.5. Manifest aplikacji na Androida

  

<?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>
  • wiersz 5: należy zezwolić na dostęp do Internetu;

9.3.6. Warstwa [DAO]

 

9.3.6.1. Interfejs [IDao] warstwy [DAO]

Interfejs warstwy [DAO] będzie wyglądał następująco:


package android.aleas.dao;

import android.aleas.fragments.Request;
import rx.Observable;

public interface IDao {

  // liczby losowe w przedziale [a,b]
  // generowanych jest n liczb, gdzie n samo w sobie jest liczbą losową z przedziału [minCount, maxCount]
  // liczby są generowane po upływie opóźnienia wynoszącego delay milisekund,
  //, gdzie [delay] jest liczbą losową z przedziału [minDelay, maxDelay]
  public Observable<AleasDaoResponse> getAleas(final Request request);

  // URL serwisu internetowego
  public void setUrlServiceWebJson(String url);

  // maksymalny czas oczekiwania (ms) na odpowiedź serwera na żądanie połączenia
  // maksymalny czas oczekiwania (ms) na odpowiedź serwera na żądanie
  public void setClientTimeouts(int connectTimeout, int readTimeOut);

}
  • wiersz 12: metoda warstwy [DAO], która generuje liczby losowe w trybie asynchronicznym;
  • wiersz 15: w celu wskazania implementacji [DAO] na usługę generowania liczb losowych URL;
  • wiersz 19: w celu ustalenia dla implementacji [DAO] maksymalnych czasów oczekiwania, co ma na celu uniknięcie zbyt długiego czasu oczekiwania w przypadku braku odpowiedzi serwera;

Metoda [getAleas] otrzymuje wszystkie swoje parametry w następującym obiekcie [Request]:


package android.aleas.fragments;

public class Request {

  // numer żądania
  int id;
  // dane wprowadzone przez użytkownika
  private int nbRequests;
  private int a;
  private int b;
  private int minCount;
  private int maxCount;
  private int minDelay;
  private int maxDelay;

  // konstruktorów
  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;
  }

  // metody pobierające i ustawiające
...
}

Można tu rozpoznać większość parametrów obiektu URL serwera, do którego należy wysłać zapytanie.

Metoda [getAleas] zwraca typ Observable<AleasDaoResponse>, gdzie klasa [AleasDaoResponse] ma następującą postać:


package android.aleas.dao;

import java.util.List;

public class AleasDaoResponse {

  // kod błędu
  private int erreur;
  // komunikat o błędzie
  private String message;
  // czas oczekiwania serwera
  private int delay;
  // liczby losowe generowane przez serwer
  private List<Integer> aleas;
  // stan klienta
  private ClientState clientState;

  // konstruktory

  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;
  }

  // metody pobierające i ustawiające
...
}

Typ [ClientState] ma następującą postać:


package android.aleas.dao;

import org.codehaus.jackson.map.annotate.JsonFilter;

import java.text.SimpleDateFormat;
import java.util.Calendar;

public class ClientState {

  // nazwa wątku wykonawczego
  private String on;
  // czas wysłania żądania
  private String requestAt;
  // czas odpowiedzi
  private String responseAt;
  // identyfikator klienta
  private int idClient;

  // konstruktor
  public ClientState() {
    on = Thread.currentThread().getName();
    requestAt = getTimeStamp();
  }

  public ClientState(int idClient) {
    this();
    this.idClient = idClient;
  }

  // metody prywatne

  private String getTimeStamp() {
    return new SimpleDateFormat("hh:mm:ss:SSS").format(Calendar.getInstance().getTime());
  }

  // metody pobierające i ustawiające
...
}
  • wiersz 11: wątek wykonawczy warstwy [DAO];
  • wiersz 13: czas wysłania żądania;
  • wiersz 15: czas odpowiedzi;
  • wiersz 17: numer żądania;

Pola [on, requestAt, idClient] są inicjowane przez klienta na początku żądania. Pole [responseAt] jest inicjowane, gdy klient otrzymuje odpowiedź z serwera.

9.3.6.2. Implementacja warstwy [DAO]

  

Interfejs [IDao] jest zaimplementowany za pomocą następującej klasy [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 {

  // klient REST
  private RestTemplate restTemplate;
  // URL usługa
  private String urlServiceWebJson;

  // mapper jSON
  private ObjectMapper mapper;

  // konstruktorzy
  public Dao() {
    // mapper jSON
    mapper = new ObjectMapper();
  }

  @Override
  public Observable<AleasDaoResponse> getAleas(final Request request) {
    ...
  }

  @Override
  public void setUrlServiceWebJson(String urlServiceWebJson) {
    // ustala się URL usługi REST
    this.urlServiceWebJson = urlServiceWebJson;
  }

  @Override
  public void setClientTimeouts(int connectTimeout, int readTimeOut) {
...
  }
}
  • wiersz 22: obiekt [RestTemplate], który będzie zapewniał komunikację z serwerem liczb losowych;
  • wiersz 24: obiekt URL usługi generującej – jest ustalany przez metodę [setUrlServiceWebJson] z wiersza 41;
  • wiersz 27: mapper jSON, który będzie służył do deserializacji ciągu jSON wysłanego przez serwer liczb losowych;
  • wiersze 30–33: konstruktor klasy;
  • wiersz 32: tworzony jest mapper jSON z wiersza 27;

Metoda [setClientTimeouts] wygląda następująco:


  // klient REST
  private RestTemplate restTemplate;
...

  @Override
  public void setClientTimeouts(int connectTimeout, int readTimeOut) {
    // ustala się limit czasu dla żądań klienta REST
    HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory();
    factory.setReadTimeout(readTimeOut);
    factory.setConnectTimeout(connectTimeout);
    restTemplate = new RestTemplate(factory);
    restTemplate.getMessageConverters().add(new StringHttpMessageConverter());
}
  • komunikacja klienta z serwerem WWW / JSON jest zapewniana przez obiekt [RestTemplate] z wiersza 2. Na razie go nie zainicjowaliśmy. Robi to metoda [setClientTimeouts];
  • wiersz 8: klasa [HttpComponentsClientHttpRequestFactory] jest dostarczana przez zależność [spring-android-rest-template]. Pozwoli nam ona ustawić maksymalne czasy oczekiwania na odpowiedź serwera (wiersze 9–10);
  • wiersz 11: tworzymy obiekt typu [RestTemplate], który będzie służył do komunikacji z serwisem internetowym. Jako parametr przekazujemy mu obiekt [factory], który właśnie został utworzony;
  • wiersz 12: dialog między klientem a serwerem może przybierać różne formy. Wymiana danych odbywa się za pomocą wierszy tekstowych i musimy wskazać obiektowi typu [RestTemplate], co ma zrobić z danym wierszem tekstowym. W tym celu dostarczamy mu konwertery – klasy zdolne do przetwarzania wierszy tekstowych. Wybór konwertera odbywa się zazwyczaj na podstawie nagłówków HTTP towarzyszących wierszowi tekstowemu. Na podstawie tych nagłówków obiekt [RestTemplate] wybierze spośród swoich konwerterów ten, który najlepiej pasuje do danej sytuacji. W tym przypadku będziemy mieli tylko jeden konwerter – konwerter typu String --> String – co oznacza, że typ String otrzymany z serwera nie zostanie poddany żadnej transformacji.

Metoda [getAleas] jest metodą najbardziej złożoną:


@Override
  public Observable<AleasDaoResponse> getAleas(final Request request) {
    Log.d("rxjava", String.format("service [DAO] pour client n° %s%n", request.getId()));
    // uruchomienie usługi
    return Observable.create(new Observable.OnSubscribe<AleasDaoResponse>() {
      @Override
      public void call(Subscriber<? super AleasDaoResponse> subscriber) {
        try {
          // URL usługi: /{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());
          // informacje o kliencie
          ClientState clientState = new ClientState(request.getId());
          // synchroniczne żądanie HTTP
          String response = executeRestService("get", urlService, null);
          // deseryalizacja odpowiedzi jSON z serwera
          AleasServerResponse aleasServerResponse = mapper.readValue(
            response,
            new TypeReference<AleasServerResponse>() {
            });
          // błąd?
          int erreur = aleasServerResponse.getErreur();
          if (erreur != 0) {
            // przekazywanie wyjątku
            subscriber.onError(new AleasException(aleasServerResponse.getMessage(), erreur));
          } else {
            // zapisano czas odbioru
            clientState.setResponseAt();
            // przekazywanie wyniku do abonenta
            subscriber.onNext(
              new AleasDaoResponse(aleasServerResponse.getErreur(), aleasServerResponse.getMessage(),
                aleasServerResponse.getDelay(), aleasServerResponse.getAleas(), clientState));
          }
        } catch (Exception ex) {
          // przekazywanie wyjątku do abonenta
          subscriber.onError(ex);
        } finally {
          // zgłaszamy zakończenie obserwowalnego
          // podczas wykonywania zauważamy, że ta metoda nie ma żadnego efektu, jeśli wcześniej wywołano metodę [onError]     zgodnie z teorią – można by zatem umieścić tę instrukcję wyłącznie w bloku try
          subscriber.onCompleted();
        }
      }
    });
  }
  • wiersz 2: należy pamiętać, że należy wygenerować typ [Observable<AleasResponse>];
  • wiersz 3: wpis do dziennika na konsoli Androida;
  • wiersz 5: obiekt [RestTemplate] zapewnia synchroniczną komunikację z serwerem. Oznacza to, że wątek wykonawczy wysyłający żądanie jest zablokowany do momentu otrzymania odpowiedzi. W przykładzie dotyczącym Swing widzieliśmy, jak przekształcić operację synchroniczną w asynchroniczną za pomocą metody [Observable.create]. Tą samą ścieżką podążamy również tutaj;
  • wiersz 7: metoda [call] z interfejsu [Observable.OnSubscribe<AleasDaoResponse>] z wiersza 5. To właśnie ta metoda jest wywoływana, gdy obserwator subskrybuje obserwowalną wartość;
  • wiersze 10–12: utworzenie obiektu URL usługi generującej liczby losowe;
  • wiersz 14: inicjalizacja obiektu [ClientState]. Chodzi tu o zapisanie czasu wysłania żądania;
  • wiersz 16: synchroniczne żądanie HTTP. Otrzymujemy odpowiedź jSON. Metoda [executeRestService] oczekuje trzech parametrów:
      1. metodę HTTP, którą należy wykorzystać do wysłania zapytania do serwisu;
      2. identyfikator usługi URL;
      3. obiekt do wysłania typu Object lub null, jeśli metoda HTTP nie jest typu POST;
  • 18–21: deserializacja otrzymanego ciągu jSON do typu [AleasServerResponse]. Typ ten jest następujący:

package android.aleas.dao;

import java.util.List;

public class AleasServerResponse {

  // kod błędu
  private int erreur;
  // komunikat o błędzie
  private String message;
  // czas oczekiwania na serwer
  private int delay;
  // liczby losowe
  private List<Integer> aleas;

  // metody pobierające i ustawiające
...
}
  • wiersz 23: pobieramy kod błędu wysłany przez serwer;
  • wiersze 24–26: w przypadku błędu przekazujemy wyjątek do abonenta;
  • wiersz 29: aktualizuje się [clientState], który będzie częścią odpowiedzi wysyłanej do abonenta;
  • wiersze 31–33: wysłanie odpowiedzi do abonenta. Ma ona postać [AleasDaoResponse];
  • wiersze 35–37: traktują wszystkie przypadki błędów w sposób niezróżnicowany. Najbardziej prawdopodobnym błędem jest błąd sieciowy;
  • wiersz 41: powiadomienie o zakończeniu transmisji;

9.3.7. Widoki aplikacji

  

Aplikacja zawiera dwa następujące widoki:

Widok zapytania

Image

Widok odpowiedzi

Image

9.3.7.1. Klasa [MyFragment]

Istnieją dwa fragmenty:

  • [RequestFragment] dla zapytania;
  • [ResponseFragment] dla odpowiedzi;

Oba fragmenty rozszerzają następującą klasę [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 {

  // ------------- dane wspólne dla fragmentów
  protected MainActivity activity;
  protected Session session;

  public abstract void onRefresh();

}
  • wiersz 7: klasa [MyFragment] rozszerza klasę Android [Fragment];
  • wiersze 10–11: dane wspólne dla wszystkich fragmentów;
  • wiersz 10: każdy fragment rozpoznaje unikalną aktywność aplikacji;
  • wiersz 11: w celu komunikacji między sobą fragmenty wykorzystują sesję;
  • wiersz 13: przed wyświetleniem fragmentu zostanie on poproszony o odświeżenie się zawartością sesji. Metoda ta jest zadeklarowana jako abstrakcyjna, ponieważ jest implementowana przez klasy potomne. Z tego powodu sama klasa jest zadeklarowana jako abstrakcyjna (wiersz 7);

Klasa [Session] zawiera dane, które są współdzielone przez różne fragmenty aplikacji. Jej kod wygląda następująco:

  

package android.aleas.activity;

import android.aleas.fragments.Request;
import android.widget.ArrayAdapter;

public class Session {

  // aktywność aplikacji
  private MainActivity activity;
  // liczba żądań
  private int nbRequests;
  // charakterystyka zapytań
  private int a;
  private int b;
  private int minCount;
  private int maxCount;
  private int minDelay;
  private int maxDelay;
  // URL usługa internetowa / jSON
  private String urlWebJson;
  // rozpoczęcie operacji
  private boolean onAir;
  // to samo, ale nieco później
  private boolean operationStarted;
  // nazwa przykładu wybranego przez użytkownika z listy przykładów
  private String exampleName;
  // jego numer na liście fragmentów
  private int examplePosition;
  // adapter spinnera przykładów w widoku zapytania
  private ArrayAdapter<CharSequence> spinnerExemplesAdapter;

  // metody
  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);
  }

  // metody pobierające i ustawiające
...
}

Metoda w wierszu 46 pozwala utworzyć obiekt [Request], który zawiera wszystkie informacje podane przez użytkownika w widoku zapytania:

  

package android.aleas.fragments;

public class Request {

  // numer żądania
  int id;
  // dane wprowadzone przez użytkownika
  private int nbRequests;
  private int a;
  private int b;
  private int minCount;
  private int maxCount;
  private int minDelay;
  private int maxDelay;

  // konstruktory
  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;
  }

  // metody pobierające i ustawiające
....
}

9.3.7.2. Fragment zapytania [RequestFragment]

Fragment zapytania zawiera następujące elementy:

Image

Aplikacja posiada jeden widok, który składa się z dwóch zakładek:

  • [1]: zakładka zapytania;
  • [2]: zakładka odpowiedzi;

Składniki fragmentu [RequestFragment] są następujące:

nr
Typ
Nazwa
Rola
3
EditText
edtNbRequests
liczba zapytań do serwisu generującego liczby losowe
4
EditText
edtA, edtB
granice [a,b] przedziału generowania liczb;
5
EditText
edtMinCount, edtMaxCount
usługa generuje liczby count, gdzie count jest liczbą losową z przedziału [minCount, maxCount]
6
EditText
edtMinDelay, edtMaxDelay
usługa czeka delay milisekund, zanim wygeneruje liczby, gdzie delay jest liczbą losową z przedziału [minDelay, maxDelay]
7
EditText
edtUrlServiceRest
URL generatora liczb losowych;
8
Spinner
spinnerExemples
rozwijana lista przykładów. Każdy przykład ilustruje konkretną metodę klasy [Observable];
8
Button
btnExecuter
przycisk uruchamiający wywołania do usługi generującej liczby;

Zgłaszane są błędy wprowadzania danych:

Image

Komponenty od 1 do 6 to komponenty [TextView] o następujących nazwach (w kolejności): txtErrorRequests, txtErrorIntervalle, txtErrorCount, txtErrorDelay, txtMsgErreurUrlServiceWeb.

9.3.7.3. Fragment odpowiedzi o nazwie [ResponseFragment]

Fragment odpowiedzi zawiera następujące elementy:

Image

nr
Typ
Nazwa
Rola
1
TextView
infoReponses
liczba otrzymanych odpowiedzi
2
ListView
listReponses
lista kanałów jSON otrzymanych z serwera
3
Przycisk
btnAnnuler
w celu anulowania żądań wysyłanych do serwera

9.3.7.4. Aktywność systemu Android [MainActivity]

  

Klasa [MainActivity] wyświetla następujący widok []:


<?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">

  <!-- pasek aplikacji -->
  <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">

    <!-- pasek narzędzi -->
    <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">

      <!-- obraz ładowania -->
      <ProgressBar
        android:id="@+id/loadingPanel"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:indeterminate="true"/>
    </android.support.v7.widget.Toolbar>

    <!-- kontener kart -->
    <android.support.design.widget.TabLayout
      android:id="@+id/tabs"
      android:layout_width="match_parent"
      android:layout_height="wrap_content"/>
  </android.support.design.widget.AppBarLayout>

  <!-- kontener widoków -->
  <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>

Elementy składowe tego widoku są następujące:

wiersze
Typ
Nazwa
Rola
20–34
Pasek narzędzi
pasek narzędzi
pasek narzędzi aplikacji
29–34
ProgressBar
loadingPanel
obrazek wyświetlany w trakcie przetwarzania żądania użytkownika
37–40
TabLayout
zakładki
pasek zakładek aplikacji
44–51
MyPager
kontener
kontener, w którym wyświetlane są różne fragmenty aplikacji

Klasa [MyPager] ma następującą postać:


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 {

  // kontrola przesuwania
  private boolean isSwipeEnabled;

  // konstruktory
  public MyPager(Context context) {
    super(context);
  }

  public MyPager(Context context, AttributeSet attrs) {
    super(context, attrs);
  }

  // prze definiowanie metod
  @Override
  public boolean onInterceptTouchEvent(MotionEvent event) {
    // czy przesunięcie jest dozwolone?
    if (isSwipeEnabled) {
      return super.onInterceptTouchEvent(event);
    } else {
      return false;
    }
  }

  @Override
  public boolean onTouchEvent(MotionEvent event) {
    // czy przesunięcie jest dozwolone?
    if (isSwipeEnabled) {
      return super.onTouchEvent(event);
    } else {
      return false;
    }
  }

  // metoda setter
  public void setSwipeEnabled(boolean isSwipeEnabled) {
    this.isSwipeEnabled = isSwipeEnabled;
  }

}
  • klasa [MyPager] rozszerza standardową klasę Androida [ViewPager]. Klasa [MyPager] jest używana zamiast klasy [ViewPager] wyłącznie w celu wyłączenia gestu przesuwania: domyślnie w klasie [ViewPager] można przechodzić między kartami za pomocą gestu przesuwania (przesuwając palcem w lewo lub w prawo). W tym przypadku nie chcemy, aby zachowanie to miało miejsce;
  • wiersz 11: zmienna logiczna, która będzie kontrolować przesuwanie (wiersze 26 i 36);
  • wiersze 44–46: metoda służąca do zainicjowania pola z wiersza 11;

Szkielet aktywności Android [MainActivity] wygląda następująco:


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 {

  // warstwa [DAO]
  private IDao dao;
  // sesja
  private Session session;

  // konstruktor
  public MainActivity() {
    // element nadrzędny
    super();
    // sesja
    session = new Session();
    // DAO
    dao = new Dao();
  }


  // metody pobierające

  public Session getSession() {
    return session;
  }

  // implementacja 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);
  }

}
  • wiersz 21: klasa [MainActivity] rozszerza standardową klasę Androida [AppCompatActivity]. Jest to zatem standardowa aktywność Androida;
  • wiersz 21: klasa [MainActivity] implementuje interfejs [IDao];

Wracając do architektury aplikacji:

fakt, że aktywność implementuje interfejs warstwy [DAO], sprawia, że widoki nie muszą być świadome istnienia warstwy [DAO]: ich menedżery zdarzeń będą zwracać się do warstwy [activité], gdy będą chciały komunikować się z serwerem.

  • wiersz 24: odwołanie do warstwy [DAO] zainicjowanej przez konstruktor z wiersza 35;
  • wiersz 26: odwołanie do sesji współdzielonej przez fragmenty, zainicjowane przez konstruktor z wiersza 33;
  • wiersze 46–59: implementacja interfejsu [IDao];

Klasa [MainActivity] inicjuje komponenty powiązanego z nią widoku w następujący sposób:


  // pasek narzędzi
  private Toolbar toolbar;
  // menedżer fragmentów
  private MyPager mViewPager;
  // kontener kart
  private TabLayout tabLayout;
  // obraz ładowania
  private ProgressBar loadingPanel;
...
  @Override
  public void onCreate(Bundle savedInstanceState) {
    // klasyczny
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    // sesja
    session.setActivity(this);
    // konfiguracja limitów czasu warstwy [DAO]
    setClientTimeouts(Constants.CONNECT_TIMEOUT, Constants.READ_TIMEOUT);

    // komponenty
    mViewPager = (MyPager) findViewById(R.id.container);
    toolbar = (Toolbar) findViewById(R.id.toolbar);
    loadingPanel = (ProgressBar) findViewById(R.id.loadingPanel);
    tabLayout = (TabLayout) findViewById(R.id.tabs);

    // pasek narzędzi
    setSupportActionBar(toolbar);

    // na początku mamy tylko jedną kartę
    TabLayout.Tab tab = tabLayout.newTab();
    tab.setText("Request");
    tabLayout.addTab(tab);

    // menedżer zdarzeń
    tabLayout.setOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {
      @Override
      public void onTabSelected(TabLayout.Tab tab) {
        // zaznaczono kartę – zmieniamy fragment wyświetlany przez kontener fragmentów
        int position = tab.getPosition();
        if (position == 0) {
          // zakładka zapytania
          showView(0);
        } else {
          // zakładka odpowiedzi – zależy od wybranego przykładu
          showView(session.getExamplePosition());
        }
      }

      @Override
      public void onTabUnselected(TabLayout.Tab tab) {

      }

      @Override
      public void onTabReselected(TabLayout.Tab tab) {

      }
    });

    // tworzenie fragmentów odpowiedzi
    createResponseFragments();

    // obsługa obrazu oczekiwania
    loadingPanel.setVisibility(View.INVISIBLE);
}

Ten kod jest dość typowy dla tej aktywności. Wyjaśnijmy kilka kwestii:

  • wiersz 19 odwołuje się do następującej klasy [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";
}
  • wiersze 31–33: tworzymy pierwszą zakładkę o tytule [Request]. W pewnym momencie w pamięci będziemy mieli:
    • fragment [Request];
    • n fragmentów typu [ExampleXXFragment];

Pierwsza zakładka będzie zawsze wyświetlać fragment o nazwie [Request]. Druga zakładka będzie wyświetlać fragment o nazwie [ExampleXXFragment] odpowiadający przykładowi wybranemu przez użytkownika. Fragment wyświetlany przez drugą zakładkę zmienia się zatem w miarę upływu czasu;

  • wiersze 37–48: kod wykonywany po kliknięciu przez użytkownika jednej z zakładek;
  • wiersz 43: wyświetlany jest fragment nr 0;
  • wiersz 46: wyświetlany jest fragment aktualnie używany (wyświetlany). Jego numer znajduje się w sesji;
  • wiersz 62: tworzone są fragmenty wszystkich przykładów znajdujących się w listy rozwijanej przykładów w widoku [RequestFragment] (pierwsza zakładka);
  • wiersz 65: obrazek oczekiwania jest na razie ukryty;

Aby zrozumieć metodę [showView] (wiersze 43, 46) oraz metodę [createResponseFragments], musimy najpierw przedstawić menedżera fragmentów w pamięci (klasa zawarta w pliku Java o nazwie MainActivity):


  // menedżer fragmentów – musi zdefiniować metody getItem, getCount
  public class SectionsPagerAdapter extends FragmentPagerAdapter {

    // zarządzane fragmenty
    private MyFragment[] fragments;

    // konstruktor
    public SectionsPagerAdapter(FragmentManager fm, MyFragment[] fragments) {
      super(fm);
      this.fragments = fragments;
    }

    // musi zwrócić fragment o numerze pozycji
    @Override
    public MyFragment getItem(int position) {
      // fragment
      return fragments[position];
    }

    // zwraca liczbę fragmentów do zarządzania
    @Override
    public int getCount() {
      // liczba fragmentów
      return fragments.length;
    }
  }
}
  • klasa [SectionsPagerAdapter] rozszerza klasę Android [FragmentPagerAdapter]. Przedefiniowuje ona dwie metody swojej klasy nadrzędnej:
    • metodę [getItem], wiersz 15;
    • metodę [getCount], wiersz 22;
  • Klasa [SectionsPagerAdapter] zawiera wszystkie fragmenty aplikacji. Są one zapisane w wierszu 5. Należy zauważyć, że są to fragmenty typu [MyFragment], przedstawionego w paragrafie 9.3.7.1;
  • wiersz 8: w celu utworzenia klasy [SectionsPagerAdapter] otrzymuje ona fragmenty, którymi ma zarządzać;
  • wiersze 14–18: metoda [getItem] zwraca fragment znajdujący się w pozycji [position];
  • wiersze 21–25: metoda [getCount] zwraca całkowitą liczbę fragmentów;

Metoda [createResponseFragments] tworzy wszystkie fragmenty potrzebne aplikacji:


private void createResponseFragments() {
    // listę przykładów
    ArrayAdapter<CharSequence> adapter = ArrayAdapter.createFromResource(this, R.array.exemples, android.R.layout.simple_spinner_item);
    // Określ układ, który ma być używany podczas wyświetlania listy opcji
    adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
    // umieszczamy adapter w sesji, aby widok [Request] mógł go pobrać
    session.setSpinnerExemplesAdapter(adapter);
    ...
  }
  • wiersz 3: tworzy się adapter dla spinnera przykładów, w tym przypadku listę String zawierającą nazwy przykładów. Nazwy te znajdują się w pliku [layout/exemples.xml]:
  

Plik [exemples.xml] zawiera następujący kod:


<!-- przykłady -->
<resources>
  <string-array name="exemples">
    <item>Exemple-01</item>
    <item>Exemple-02</item>
    <item>Exemple-03</item>
    <item>Exemple-04</item>
  </string-array>
</resources>

Wiersz 1 – ten plik jest drugim parametrem metody [createFromResource]. W pliku [R.array.exemples] nazwa [exemples] odnosi się do nazwy tablicy (wiersz 3 powyżej), a nie do nazwy pliku.

  • wiersz 5: do adaptera przypisuje się layout (moduł zarządzania wyświetlaniem). Teraz adapter posiada zarówno dane, jak i sposób ich wyświetlania;
  • wiersz 7: uruchamiamy adapter. W tym momencie zostanie on pobrany przez fragment kodu [RequestFragment], który go potrzebuje;

Kontynuujmy kod metody [createResponseFragments]:


private void createResponseFragments() {
    // przykłady spinnerów
    ArrayAdapter<CharSequence> adapter = ArrayAdapter.createFromResource(this, R.array.exemples, android.R.layout.simple_spinner_item);
    // Określ układ, który ma być używany przy wyświetlaniu listy opcji
    adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
    // umieszczamy adapter w sesji, aby widok [Request] mógł go pobrać
    session.setSpinnerExemplesAdapter(adapter);
    // tworzenie tabeli fragmentów (1 zapytanie, n odpowiedzi)
    MyFragment[] tFragments = new MyFragment[adapter.getCount() + 1];
    // fragment zapytania
    tFragments[0] = new RequestFragment();
    // fragmenty odpowiedzi
    for (int i = 1; i < tFragments.length; i++) {
      // tworzy się nazwę fragmentu do instancjonowania, odpowiadającą przykładowi wybranemu przez użytkownika
      // nazwa ta musi być pełną nazwą wraz z pakietem – w tym przypadku jest ona bezpośrednio powiązana z numerem przykładu w spinnerze
      String exampleClassName = String.format("%s.Example%02dFragment", Constants.EXAMPLES_PACKAGE, i);
      // tworzy się instancję fragmentu powiązanego z przykładem
      MyFragment fragment;
      try {
        // instancjonowanie klasy
        fragment = (MyFragment) Class.forName(exampleClassName).getConstructors()[0].newInstance(new Object[]{});
      } catch (Exception e) {
        e.printStackTrace();
        return;
      }
      // fragment został utworzony – umieszczamy go w tablicy
      tFragments[i] = fragment;
    }
    // tworzenie instancji menedżera fragmentów z tymi nowymi fragmentami
    mSectionsPagerAdapter = new SectionsPagerAdapter(getSupportFragmentManager(), tFragments);
    // Konfiguracja ViewPager za pomocą adaptera sekcji.
    mViewPager.setAdapter(mSectionsPagerAdapter);
    // nawigacja między stronami – ta instrukcja jest ważna
    // tutaj mówi się, że po obu stronach wyświetlanego widoku należy zachować zainicjowane widoki [tFragments.length]
    // oznacza to, że wszystkie fragmenty używane przez aplikację znajdują się w pamięci i są zainicjowane
    // jeśli tego nie zrobimy, to domyślnie wartość [OffscreenPageLimit] wynosi 1
    // w związku z tym, jeśli wyświetlany jest fragment nr 3, zainicjowane zostaną tylko fragmenty 2 i 4
    // odbywa się to poprzez wywołanie metody [onCreateView] dla tych dwóch fragmentów – oznacza to, że w tej metodzie należy przewidzieć
    // odtworzyć wygląd, jaki miał fragment podczas ostatniego użycia – ponadto w tej metodzie nie może
    // nie może znajdować się kod, który nie wytrzymałby dwukrotnego wykonania – powoduje to ogromny bałagan i jest trudne do opanowania
    // w tym przypadku postanowiliśmy uniknąć tych trudności – w logach widać, że przy uruchomieniu aplikacji tworzone są wszystkie fragmenty
    // i ich metoda [onCreateView] jest wykonywana – później już nigdy nie jest wykonywana –
    mViewPager.setOffscreenPageLimit(tFragments.length);
    // blokujemy przechodzenie między fragmentami
    mViewPager.setSwipeEnabled(false);
  }
  • wiersz 9: tworzenie tablicy, która będzie zawierać wszystkie fragmenty aplikacji;
  • wiersz 11: pierwszy fragment to fragment zapytania;
  • wiersze 13–28: utworzymy tyle fragmentów, ile jest przykładów. Wszystkie te fragmenty rozszerzają fragment odpowiedzi [ResponseFragment] i implementują wyłącznie to, co jest specyficzne dla danego przykładu: tworzenie wartości obserwowanych. Różnią się one bowiem w zależności od przykładu;
  • wiersz 16: fragment przykładu nosi standardową nazwę: ExampleXXFragment, gdzie XX to jego pozycja w spinnerze przykładów powiększona o 1. XX to również numer fragmentu przykładu w menedżerze fragmentów;
  • wiersz 21: instancjonowanie fragmentu przykładu nr i z listy:
    • Class.forName(exampleName): ładuje fragment do pamięci;
    • Class.forName(exampleName).getConstructors()[0]: pobiera odwołanie do pierwszego konstruktora klasy. Klasa ExampleXXFragment ma tylko jeden konstruktor. Uzyskane zostanie zatem odwołanie do tego konstruktora;
    • Class.forName(exampleName).getConstructors()[0].newInstance(new Object[]{}) tworzy instancję obiektu typu ExampleXXFragment przy użyciu konstruktora z poprzedniego etapu. new Object[]{} reprezentuje parametry przekazane do tego konstruktora. Ponieważ konstruktor klasy ExampleXXFragment nie oczekuje żadnych parametrów, przekazywana jest pusta tablica obiektów;
  • wiersz 27: ten fragment zostaje dodany do tablicy fragmentów;
  • wiersz 30: widzieliśmy, że konstruktor menedżera fragmentów [SectionsPagerAdapter] oczekiwał w swoich parametrach tablicy fragmentów, którymi miał zarządzać. Teraz mu ją przekazujemy;
  • wiersz 22: kontener fragmentów [mViewPager] widoku powiązanego z aktywnością [MainActivity] jest tutaj powiązany z menedżerem fragmentów: kontener fragmentów [mViewPager] wyświetla fragmenty z menedżera fragmentów;
  • wiersz 43: warto zapoznać się z komentarzami – instrukcja ta oznacza, że wszystkie fragmenty muszą pozostać w stanie, w jakim umieścił je kod, niezależnie od tego, który fragment jest aktualnie wyświetlany. Dzięki temu, gdy do niego powrócimy, zastaniemy go w stanie, w jakim go zostawiliśmy;
  • wiersz 45: kontener fragmentów [mViewPager] jest typu [MyPager], co pozwala zablokować przesuwanie palcem;

Metoda [MainActivity.showView] wygląda następująco:


  // wyświetlanie widoku nr [position]
  private void showView(int position) {
    // odświeżamy fragment przed jego wyświetleniem
    mSectionsPagerAdapter.getItem(position).onRefresh();
    // wyświetla się żądany widok – przechodzi się bezpośrednio do widoku (drugi parametr ustawiony na „false”)
    // bez tego parametru domyślnie przechodzi się do żądanego widoku, szybko wyświetlając widoki pośrednie – niepożądane zachowanie
    mViewPager.setCurrentItem(position, false);
}
  • wiersz 3: chcemy wyświetlić fragment o numerze pozycji;
  • wiersz 4: fragment ten jest pobierany z menedżera fragmentów, a następnie odświeżany. Od czasu ostatniego wyświetlenia sesja mogła ulec zmianie. Fragment musi zatem sprawdzić sesję, aby ustalić, czy wymaga aktualizacji;
  • wiersz 7: fragment jest wyświetlany przez [ViewPager]. Ponieważ został on powiązany z menedżerem fragmentów, wyświetlony zostanie fragment o numerze [position], czyli ten, który właśnie został odświeżony w wierszu 4;

Na koniec omówmy dwie metody zarządzania kolejką:


  public void beginWaiting() {
    // obsługa obrazu oczekiwania
    loadingPanel.setVisibility(View.VISIBLE);
  }

  public void cancelWaiting() {
    // obsługa obrazu oczekiwania
    loadingPanel.setVisibility(View.INVISIBLE);
    // zakończenie wykonywania
    session.setOnAir(false);
    session.setOperationStarted(false);
}

9.3.7.5. Fragment [RequestFragment]

Klasa [RequestFragment] wygląda następująco:


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 usługi internetowej
  private EditText edtUrlServiceRest;
  private TextView txtMsgErreurUrlServiceWeb;
  // liczba żądań
  private EditText edtNbRequests;
  private TextView txtErrorRequests;
  // interwał generowania
  private EditText edtA;
  private EditText edtB;
  private TextView txtErrorIntervalle;
  // opóźnienie
  private EditText edtMinDelay;
  private EditText edtMaxDelay;
  private TextView txtErrorDelay;
  // liczba wygenerowanych wartości
  private EditText edtMinCount;
  private EditText edtMaxCount;
  private TextView txtErrorCount;
  // przycisk
  private Button btnExecuter;
  // lista odpowiedzi
  private ListView listReponses;
  private TextView infoReponses;
  // pokrętło przykładów
  private Spinner spinnerExemples;

  // wprowadzone dane
  private int nbRequests;
  private int a;
  private int b;
  private String urlServiceWebJson;
  private int minDelay;
  private int maxDelay;
  private int minCount;
  private int maxCount;

  // konstruktor
  public RequestFragment() {
    super();
    Log.d("rxjava", "RequestFragment constructor");
  }

  @Override
  public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
    Log.d("rxjava", "RequestFragment onCreateView");
    // pobieramy aktywność i sesję
    activity = (MainActivity) getActivity();
    session = activity.getSession();
    // tworzymy widok fragmentu na podstawie jego definicji XML
    View view = inflater.inflate(R.layout.request, container, false);
    // komponenty
    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);

    // przycisk [Exécuter]
    btnExecuter.setVisibility(View.VISIBLE);
    btnExecuter.setOnClickListener(new View.OnClickListener() {
      public void onClick(View arg0) {
        doExecuter();
      }
    });

    // na początku brak komunikatów o błędach
    txtErrorRequests.setVisibility(View.INVISIBLE);
    txtErrorIntervalle.setVisibility(View.INVISIBLE);
    txtMsgErreurUrlServiceWeb.setVisibility(View.INVISIBLE);
    txtErrorCount.setVisibility(View.INVISIBLE);
    txtErrorDelay.setVisibility(View.INVISIBLE);
    // przykładowe ikony ładowania
    spinnerExemples.setAdapter(session.getSpinnerExemplesAdapter());
    // wynik
    return view;
  }
...
}
  • wiersz 16: klasa [RequestFragment] rozszerza klasę [MyFragment] (patrz punkt 9.3.7.1);
  • wiersze 18–42: elementy wizualne fragmentu (patrz punkt 9.3.7.2);
  • wiersze 45–52: dane wprowadzone przez użytkownika w formularzu;
  • konstruktor (wiersze 55–58) oraz metoda [onCreateView] są wykonywane, gdy aktywność [MainActivity] tworzy wszystkie fragmenty aplikacji. Dzieje się to tylko raz;
  • wiersz 61: kod metody [onCreateView] jest standardowy. W wierszu 102 należy zwrócić uwagę, że adapter spinnera z przykładów jest pobierany z sesji. Warto również zwrócić uwagę na wiersz 91, gdzie kliknięcie przycisku [Exécuter] jest obsługiwane przez metodę [doExecuter];
  • wiersze 64–65: pola [activity] i [session] należą do klasy nadrzędnej [MyFragment];

Metoda [doExecuter] ma następującą postać:


  // wprowadzone dane
  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() {
    // czy dane są prawidłowe?
    if (isPageValid()) {
      // zapisujemy informacje w sesji
      session.setInfos(nbRequests, a, b, minCount, maxCount, minDelay, maxDelay, urlServiceWebJson, spinnerExemples.getSelectedItem().toString(), spinnerExemples.getSelectedItemPosition() + 1);
      // zapisujemy identyfikator URL serwisu internetowego
      activity.setUrlServiceWebJson(session.getUrlWebJson());
      Log.d("rxjava", String.format("RequestFragment doExecuter, session=%s, session.position=%s%n", session, session.getExamplePosition()));
      // czynność w toku
      session.setOnAir(true);
      // ale nie rozpoczęto
      session.setOperationStarted(false);
      // wyświetlany jest fragment odpowiedzi
      activity.selectTab(Constants.VUE_RESPONSE);
      // rozpoczyna się oczekiwanie
      beginWaiting();
    }
}
  • wiersz 15: nie będziemy omawiać metody [ispageValid]. Sprawdza ona poprawność wprowadzonych danych i zwraca wartość true tylko wtedy, gdy wszystkie są poprawne. W takim przypadku są one wykorzystywane do zainicjowania pól w wierszach 2–9;
  • wiersz 17: poszczególne dane są zapisywane w sesji:
    • [spinnerExemples.getSelectedItem().toString()] to nazwa przykładu wybranego przez użytkownika, zapisana w [session.exampleName];
    • [spinnerExemples.getSelectedItemPosition() + 1] to numer fragmentu powiązanego z przykładem, który został zapisany (fragment) przez menedżera fragmentów. Numer ten jest zapisany w [session.examplePosition];
  • wiersz 19: URL z serwisu internetowego / jSON jest przekazywane do procesu, który z kolei przekazuje je do warstwy [DAO];
  • wiersze 21–24: zauważamy, że rozpocznie się operacja;
  • wiersz 26: zostanie wyświetlona karta odpowiedzi. Aby zrozumieć, co się stanie, należy przypomnieć sobie kod [MainActivity.selectTab]:

  // wybór zakładki
  public void selectTab(int position) {
    // są maksymalnie 2 zakładki
    // początkowo jest tylko jedna zakładka – ta z zapytania
    // jeśli żądana zakładka to nr 1 i jeszcze nie istnieje, należy ją utworzyć
    if (position == 1 && tabLayout.getTabCount() == 1) {
      // 1 dodatkowa zakładka
      TabLayout.Tab tab = tabLayout.newTab();
      tab.setText("Response");
      tabLayout.addTab(tab);
    }
    // programowo wybieramy zakładkę, co uruchomi zdarzenie [onTabSelected]
    //, które przypisze odpowiedni widok do tej zakładki
    tabLayout.getTabAt(position).select();
}
  • początkowo operacja utworzyła jedynie zakładkę zapytania (zakładka nr 0);
  • wiersze 6–11: tworzy się kartę odpowiedzi (karta nr 1), jeśli nie została jeszcze utworzona;
  • wiersz 14: wybiera się kartę nr position (0 lub 1). Powoduje to umieszczenie zdarzenia [onTabSelected] w kolejce pętli zdarzeń aplikacji na Androida;

Obsługa zdarzenia [onTabSelected] w [MainActivity] wygląda następująco:


      @Override
      public void onTabSelected(TabLayout.Tab tab) {
        // zakładka została wybrana – zmieniamy fragment wyświetlany przez kontener fragmentów
        int position = tab.getPosition();
        if (position == 0) {
          // zakładka zapytania
          showView(0);
        } else {
          // zakładka odpowiedzi – zależy od wybranego przykładu
          showView(session.getExamplePosition());
        }
}

W przypadku zakładki [Response] wykonywana jest linia 9. Wyświetlony zostanie fragment nr [session.getExamplePosition()]. Na przykład w przypadku [exemple-03] numer zapisany w [session.examplePosition] to 3. Wiersz 10 wyświetla zatem fragment nr 3. Tabela fragmentów utworzona początkowo przez aktywność to [RequestFragment, Exemple01Fragment, Exemple02Fragment, Exemple03Fragment,..]. Zatem to właśnie fragment [Exemple03Fragment] zostanie wyświetlony. Odbywa się to za pomocą następującego kodu:


  // wyświetlanie widoku nr [position]
  private void showView(int position) {
    // odświeżamy fragment przed jego wyświetleniem
    mSectionsPagerAdapter.getItem(position).onRefresh();
    // wyświetla się żądany widok – przechodzi się bezpośrednio do widoku (drugi parametr ustawiony na „false”)
    // bez tego parametru domyślnie przechodzi się do żądanego widoku, szybko wyświetlając widoki pośrednie – niepożądane zachowanie
    mViewPager.setCurrentItem(position, false);
}

Widać, że fragment zostanie odświeżony (wiersz 4) przed wyświetleniem (wiersz 7).

9.3.7.6. Fragment [ResponseFragment]

Klasa [ResponseFragment] wyświetla odpowiedzi serwera. Jej kod wygląda następująco:


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 {

  // lista odpowiedzi
  private ListView listReponses;
  private TextView infoReponses;
  // przycisk
  private Button btnAnnuler;

  // mapper 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) {
    // pobieramy aktywność i sesję
    activity = (MainActivity) getActivity();
    session = activity.getSession();
    Log.d("rxjava", String.format("ResponseFragment (%s) onCreateView%n", this));
    // tworzy się widok fragmentu na podstawie jego definicji XML
    View view = inflater.inflate(R.layout.response, container, false);
    // komponenty
    listReponses = (ListView) view.findViewById(R.id.lst_reponses);
    infoReponses = (TextView) view.findViewById(R.id.txt_Reponses);
    btnAnnuler = (Button) view.findViewById(R.id.btn_Annuler);
    // przycisk [Annuler]
    btnAnnuler.setVisibility(View.INVISIBLE);
    btnAnnuler.setOnClickListener(new View.OnClickListener() {
      public void onClick(View arg0) {
        doAnnuler();
      }
    });
    // wynik
    return view;
  }
...
  // metoda do wykonania (za pomocą kodu jawnego) przed każdym wyświetleniem fragmentu
  public void onRefresh() {
...
  }
}
  • wiersz 21: klasa [ResponseFragment] rozszerza klasę [MyFragment];
  • wiersze 23–27: składniki fragmentu;
  • wiersze 32–36: konstruktor jest wykonywany tylko raz, podczas początkowego tworzenia fragmentów przykładów przez aktywność. Wszystkie fragmenty przykładów dziedziczą bowiem po fragmencie [ResponseFragment]. Podczas ich instancjonowania wywoływany jest konstruktor ich klasy nadrzędnej [ResponseFragment];
  • wiersz 35: inicjuje mapper jSON z wiersza 30, używany do wyświetlania ciągu jSON ze stosu wyjątków;
  • wiersze 38–59: metoda [onCreateView] jest wykonywana tylko raz, podczas początkowego tworzenia fragmentów przykładów przez aktywność. Znajduje się w niej klasyczny kod aplikacji na Androida;
  • wiersze 52–56: metodą wykonywaną po kliknięciu przycisku [Annuler] jest metoda [doAnnuler];
  • wiersze 62–64: metoda [onRefresh] jest wykonywana za każdym razem, gdy wyświetlana jest karta [Response];

Dzięki różnym wpisom logowym umieszczonym w kluczowych metodach możemy zobaczyć, co dzieje się podczas uruchamiania aplikacji:

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
  • wiersz 1: tworzenie fragmentu [RequestFragment];
  • wiersze 2–9: tworzenie fragmentów czterech przykładów aplikacji;
  • wiersz 10: inicjalizacja fragmentu [RequestFragment];
  • wiersze 11–14: inicjalizacja fragmentów czterech przykładów aplikacji;

Następnie nie pojawiają się już żadne wywołania tych metod.

Metoda [ResponseFragment.onRefresh] wygląda następująco:


  // metoda do wykonania (poprzez kod jawny) przed każdym wyświetleniem fragmentu
  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()));
    // czy trwa wykonywanie?
    if (session.isOnAir() && !session.isOperationStarted()) {
      // wykonywanie żądania
      session.setOperationStarted(true);
      doExecuter();
    }
}
  • wiersz 5: sprawdzamy, czy fragment [RequestFragment] wysłał żądanie (session.isOnAir) i czy zostało ono uruchomione (isOperationStarted). Jeśli fragment [RequestFragment] wysłał żądanie, a żądanie to nie jest jeszcze w trakcie realizacji, operacja zostaje uruchomiona (wiersze 7–8);
  • po uruchomieniu operacji, ponieważ ma ona charakter asynchroniczny, użytkownik może przechodzić między obiema kartami. Jeśli ponownie przejdzie do karty [Response], a operacja jest w toku, wówczas wiersze 7–8 nie są wykonywane;

Metoda [doExecuter] w wierszu 8 wykonuje operację żądaną przez użytkownika:


  private void doExecuter() {
    Log.d("rxjava", String.format("ResponseFragment (%s) doExecuter for %s%n", this, session.getExampleName()));
    // początek oczekiwania
    beginWaiting();
    // przygotowanie do wykonania
    subscriptions.clear();
    reponses.clear();
    nbInfos = 0;
    // tworzy się i uruchamia obserwowalne z wybranego przykładu
    createAndExecuteObservables();
}

// metoda zaimplementowana przez klasy potomne
protected abstract void createAndExecuteObservables();
  • wiersz 10: tworzy, wykonuje i obserwuje obserwowalne. Są one różne dla każdego przykładu. Dlatego metoda [createAndExecuteObservables] jest abstrakcyjna (wiersz 14). Zostanie ona zaimplementowana przez fragmenty [ExampleXXFragment], które rozszerzają klasę [ResponseFragment];
  • wiersz 6: lista subskrypcji zostaje wyczyszczona;
  • wiersz 7: lista wyświetlająca odpowiedzi jest czyszczona;
  • wiersz 8: zlicza liczbę otrzymanych odpowiedzi;

Klasy potomne [ExampleXXFragment] powierzają następującej metodzie [showAlea] zadanie wyświetlania elementów, które obserwują:


  protected void showAlea(String data) {
    // dodatkowa informacja
    nbInfos++;
    infoReponses.setText(String.format("Liste des réponses (%s)", nbInfos));
    // 1 dodatkowa odpowiedź
    reponses.add(0, data);
    Log.d("rxjava", data);
    // aktualizacja UI
    listReponses.setAdapter(new ArrayAdapter<String>(getActivity(), android.R.layout.simple_list_item_1, android.R.id.text1, reponses));
}
  • wiersz 1: widać, że obserwowany element pojawia się w postaci ciągu znaków. Będzie to w rzeczywistości ciąg znaków jSON obserwowanego elementu. Pozwala nam to na stosowanie jednej metody wyświetlania obserwowanego elementu, niezależnie od jego dokładnego typu w Javie;
  • wiersz 6: obserwowany element [data] jest dodawany na pierwszą pozycję listy odpowiedzi. Użytkownik widzi zatem na początku listy najnowsze odpowiedzi;

Oczekiwanie jest obsługiwane przez następujące metody [beginWaiting] i [cancelWaiting]:


  private void beginWaiting() {
    // włączamy klepsydrę
    activity.beginWaiting();
    // wyświetlono przycisk [Annuler]
    btnAnnuler.setVisibility(View.VISIBLE);
  }

  protected void cancelWaiting() {
    // koniec oczekiwania
    activity.cancelWaiting();
    // przycisk [Annuler] jest ukryty
    btnAnnuler.setVisibility(View.INVISIBLE);
}

Wykorzystują one metody o tych samych nazwach z danej aktywności, a następnie ograniczają się do wyświetlania/ukrywania przycisku [Annuler].

Kliknięcie przycisku [Annuler] jest obsługiwane przez następujący kod:


  protected void doAnnuler() {
    // anulowano wszystkie subskrypcje
    for (Subscription s : subscriptions) {
      if (!s.isUnsubscribed()) {
        s.unsubscribe();
      }
    }
    // koniec oczekiwania
    cancelWaiting();
}
  • wiersze 3–7: anulujemy po kolei wszystkie subskrypcje;

9.3.8. Przykłady obserwowalnych

9.3.8.1. Exemple-01

Klasy [ExampleXXFragment] służą do tworzenia, uruchamiania i obserwowania obserwowalnych. Wyświetlanie obserwowanych wartości odbywa się za pośrednictwem klasy nadrzędnej [ResponseFragment].

Klasa [Example01Fragment] wygląda następująco:

  

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 {

    // mapery jSON
    private ObjectMapper mapperAleasUiResponse;

    // konstruktor
    public Example01Fragment() {
        super();
        Log.d("rxjava", "Example01Fragment constructor");
        // filtry jSON
        mapperAleasUiResponse = new ObjectMapper();
    }

    @Override
    public void createAndExecuteObservables() {
        Log.d("rxjava", "Example01Fragment createAndExecuteObservables");
        // żądane są liczby losowe
        Observable<AleasDaoResponse> observable = Observable.empty();
        for (int i = 0; i < session.getNbRequests(); i++) {
            // konfiguracja obserwowalna nr i
            // żądanie do wysłania do serwera
            Request request = session.getRequest();
            request.setId(i);
            // wielkość obserwowalna wykonywana w wątku obliczeniowym
            observable = observable.mergeWith(session.getActivity().getAleas(request).subscribeOn(Schedulers.io()));
        }
        // obserwacja w wątku pętli zdarzeń;
        observable = observable.observeOn(AndroidSchedulers.mainThread());
        // wykonujemy wszystkie te obserwowalne
        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) {
        // wyodrębniamy informacje do wyświetlenia
        String data;
        try {
            data = mapperAleasUiResponse.writeValueAsString(new AleasUiResponse(aleasDaoResponse));
        } catch (IOException e) {
            data = String.format("[%s,%s]", e.getClass().getName(), e.getMessage());
        }
        return data;
    }
}
  • wiersz 36: jedyna obserwowalna, która zostanie wygenerowana;
  • wiersze 37–44: generowanie i konfiguracja różnych obserwowalnych, które są scalane (wiersz 43) w obserwowalną z wiersza 36;
  • wiersz 43: obserwowalna jest wykonywana w wątku harmonogramu [Schedulers.io()]. Wywołanie HTTP do serwera zostanie wykonane w tym wątku;
  • wiersz 46: końcowa obserwowalna jest obserwowana w wątku pętli zdarzeń;
  • wiersze 48–57: wykonywanie obserwowalnych, a tym samym wysyłanie żądań do serwera liczb losowych. System Android nie obsługuje jeszcze języka Java 8 i jego lambd. Dlatego w tym przypadku używamy klas anonimowych do instancjonowania interfejsów funkcjonalnych RxJava;
  • wiersze 49–52: akcja wykonywana, gdy obserwator otrzymuje nowy element typu [AleasDaoResponse] od obiektu obserwowalnego (por. paragraf 9.3.6.1);
  • wiersz 51: wywołanie metody [showAlea] klasy nadrzędnej. Przypomnijmy, że oczekuje ona ciągu znaków. Jest on dostarczany przez metodę [getDataFrom] z wierszy 59–68;
  • wiersz 63: zwracamy ciąg znaków jSON typu [AleasUiResponse] w następujący sposób:

package android.aleas.fragments;

import android.aleas.dao.AleasDaoResponse;

import java.text.SimpleDateFormat;
import java.util.Calendar;

public class AleasUiResponse {

  // odpowiedź [DAO]
  private AleasDaoResponse aleasDaoResponse;
  // wątek obserwacji
  private String observedOn;
  // czas obserwacji
  private String observedAt;

  // konstruktory
  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;
  }
// metody pobierające i ustawiające
...
}
  • do odpowiedzi warstwy [DAO] (wiersz 11) dodaje się dwie informacje:
    • wiersz 13: wątek obserwacji;
    • wiersz 15: czas obserwacji;

Wróćmy do kodu subskrypcji:


    @Override
    public void createAndExecuteObservables() {
...
        // wykonujemy wszystkie te obserwowalne
        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) {
                // wyświetla się wyjątek
                showAlea(getMessagesFromThrowable(th));
                // po otrzymaniu wyjątku obserwowalna nie otrzymuje ani onNext, ani onCompleted
                // konieczne jest ręczne anulowanie subskrypcji
                doAnnuler();
            }
        }, new Action0() {
            @Override
            public void call() {
                // koniec oczekiwania
                cancelWaiting();
            }
        }));
}
  • wiersze 11–18: przypadek, w którym obserwator otrzymuje wyjątek;
  • wiersz 14: ponownie wykorzystujemy metodę [showAlea] z klasy nadrzędnej, aby wyświetlić wyjątek. Metoda [getMessagesFromThrowable] jest metodą klasy nadrzędnej [ResponseFragment], która na podstawie wyjątku generuje ciąg znaków:

  // komunikaty o wyjątku
  protected String getMessagesFromThrowable(Throwable ex) {
    // tworzy się listę z komunikatami o błędach ze stosu wyjątków
    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();
    }
}
  • wiersz 11: zwracamy ciąg znaków jSON z listy komunikatów o błędach (wiersz 4);

Wróćmy do kodu subskrypcji obserwowalnego:

  • wiersze 19–25: kod wykonywany, gdy obserwator otrzymuje powiadomienie o zakończeniu emisji. Następnie anulujemy oczekiwanie (wiersz 23), co powoduje aktualizację interfejsu graficznego;

Wynik wykonania przykładu 01 jest podobny do poniższego:

Image

Każdy element listy to ciąg znaków jSON odpowiadający obserwowanej wartości. Pola ciągu znaków jSON są następujące:

  • aleas: lista liczb losowych dostarczona przez serwer;
  • idClient: numer zapytania (widać, że odpowiedzi nadeszły w nieuporządkowanej kolejności);
  • on: wątek wykonawczy obserwowalnej wielkości, który wygenerował tę wartość;
  • requestAt: czas wysłania żądania przez klienta;
  • responseAt: czas odpowiedzi serwera;
  • delay: opóźnienie zaobserwowane przez serwer;
  • błąd: kod błędu zwrócony przez serwer (0 = brak błędu);
  • komunikat: komunikat o błędzie zwrócony przez serwer (null = brak błędu);
  • observedAt: czas obserwacji odczytanej wartości;
  • observedOn: wątek, w którym odnotowano daną wartość;

9.3.8.2. Exemple-02

Klasa [Example02Fragment] ma następującą postać:


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 {

    // mapery jSON
    private ObjectMapper mapperAleasUiResponse;

    // konstruktor
    public Example02Fragment() {
        super();
        Log.d("rxjava", "Example02Fragment constructor");
        // filtr jSON
        mapperAleasUiResponse = new ObjectMapper();
    }

    public void createAndExecuteObservables() {
        Log.d("rxjava", "Example02Fragment createAndExecuteObservables");
        // żądanie liczb losowych
        Observable<AleasDaoResponse> observable = Observable.empty();
        for (int i = 0; i < session.getNbRequests(); i++) {
            // przygotowanie zapytania
            Request request = session.getRequest();
            request.setId(i);
            // zachowuje się tylko te obserwowalne, których numer klienta jest parzysty
            observable = observable
                    .mergeWith(session.getActivity().getAleas(request).filter(new Func1<AleasDaoResponse, Boolean>() {
                        @Override
                        public Boolean call(AleasDaoResponse aleasDaoResponse) {
                            return aleasDaoResponse.getClientState().getIdClient() % 2 == 0;
                        }
                    })
                            // wykonanie w wątku wejścia/wyjścia
                            .subscribeOn(Schedulers.io()));
        }
        // obserwacja w wątku pętli zdarzeń
        observable = observable.observeOn(AndroidSchedulers.mainThread());
        // wykonujemy te obserwowalne
        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() {
                // koniec oczekiwania
                cancelWaiting();
            }
        }));

    }

    private String getDataFrom(AleasDaoResponse aleasDaoResponse) {
        // pobieranie informacji do wyświetlenia
        String data;
        try {
            data = mapperAleasUiResponse.writeValueAsString(new AleasUiResponse(aleasDaoResponse));
        } catch (IOException e) {
            data = String.format("[%s,%s]", e.getClass().getName(), e.getMessage());
        }
        return data;
    }

}

Ten przykład jest analogiczny do poprzedniego (wiersz 38). Jednak spośród obserwowalnych wartości uzyskanych w poprzednim przykładzie zachowujemy tylko te, których numer klienta jest parzysty (wiersze 42–46), dzięki metodzie [filter] (wiersz 41).

Uzyskane wyniki są następujące (dla 10 zapytań):

Image

9.3.8.3. Exemple-03

Klasa [Example03Fragment] wygląda następująco:


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 {

  // mapy jSON
  private ObjectMapper mapper;

  // konstruktor
  public Example03Fragment() {
    super();
    Log.d("rxjava", "Example03Fragment constructor");
    // filtr jSON
    mapper = new ObjectMapper();
  }

  public void createAndExecuteObservables() {
    Log.d("rxjava", "Example03Fragment createAndExecuteObservables");
    // generowanie liczb losowych
    Observable<List<Integer>> observable = Observable.empty();
    for (int i = 0; i < session.getNbRequests(); i++) {
      // przygotowanie zapytania
      Request request = session.getRequest();
      request.setId(i);
      // konfiguracja obserwowalna
      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();
        }
      })
        // wykonanie w wątku wejścia/wyjścia
        .subscribeOn(Schedulers.io()));
    }
    // obserwacja w wątku pętli zdarzeń
    observable = observable.observeOn(AndroidSchedulers.mainThread());
    // wykonujemy te obserwowalne
    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() {
            // koniec oczekiwania
            cancelWaiting();
          }
        }
      ));

  }

  private String getDataFrom(List<Integer> aleas) {
    // pobieranie informacji do wyświetlenia
    String data;
    try {
      data = mapper.writeValueAsString(aleas);
    } catch (IOException e) {
      data = String.format("[%s,%s]", e.getClass().getName(), e.getMessage());
    }
    return data;
  }

}

Ten przykład jest analogiczny do Przykładu-02:

  • wiersz 40: definiuje się te same wielkości obserwowalne, co w Przykładzie-02;
  • wiersz 45: każda z wartości generowanych przez powyższe obserwowalne jest przekształcana za pomocą metody [map] na typ List<Integer>, który stanowi listę liczb losowych generowanych przez serwer;
  • wiersz 58: od tej pory obserwowana wartość jest typu List<Integer>;

Wynik uzyskany dla 10 zapytań jest następujący:

Image

9.3.8.4. Exemple-04

Klasa [Example04Fragment] wygląda następująco:


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 {

  // mapy jSON
  private ObjectMapper mapper;

  // konstruktor
  public Example04Fragment() {
    super();
    Log.d("rxjava", "Example04Fragment constructor");
    // filtr jSON
    mapper = new ObjectMapper();
  }

  public void createAndExecuteObservables() {
    Log.d("rxjava", "Example03Fragment createAndExecuteObservables");
    // generowanie liczb losowych
    Observable<Integer> observable = Observable.empty();
    for (int i = 0; i < session.getNbRequests(); i++) {
      // przygotowanie zapytania
      Request request = session.getRequest();
      request.setId(i);
      // konfiguracja obserwowalnych
      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());
        }
      })
        // wykonanie w wątku wejścia/wyjścia
        .subscribeOn(Schedulers.io()));
    }
    // obserwacja w wątku pętli zdarzeń
    observable = observable.observeOn(AndroidSchedulers.mainThread());
    // wykonujemy te obserwowalne
    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() {
            // koniec oczekiwania
            cancelWaiting();
          }
        }
      ));

  }
}

Ten przykład jest analogiczny do Przykładu-03, z tą różnicą, że zamiast metody [map] w wierszu 42 zastosowano metodę [flatMap].

  • wiersz 55: należy zauważyć, że od tej pory typ obserwowanej wartości to Integer;

Dla 10 zapytań otrzymujemy następujące wyniki:

Image

Tym razem mamy więcej wartości obserwowanych niż zapytań.

9.3.8.5. Exemple-05

Przedstawiamy teraz procedurę, którą należy wykonać, aby dodać nowy przykład obserwowalnych do aplikacji.

Załóżmy, że chcemy odtworzyć przykład [Exemple22h] z paragrafu 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 {
        // proces
        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()));
        // subskrypcje
        ProcessUtils.subscribe(1, process);
    }
}
  • wartości obserwowalnej [Observable.range(1, 10)] są najpierw pogrupowane na wartości parzyste i nieparzyste za pomocą metody [groupBy] (wiersz 11), a następnie połączone w jedną obserwowalną za pomocą metody [concatMap] (wiersz 12);

krok 1

Tworzymy nowy przykład w pliku [exemples.xml]:

  

<!-- przykłady -->
<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>

Powyżej dodano wiersz 8. Nazwa przykładu może być dowolna.

Krok 2

Klasę [Example04Fragment] powielamy jako [Example05Fragment]. W tym przypadku nazwa jest narzucona.

Krok 3

Modyfikujemy kod klasy [Example05Fragment] w następujący sposób:


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 {

  // mapy jSON
  private ObjectMapper mapper;

  // konstruktor
  public Example05Fragment() {
    super();
    Log.d("rxjava", "Example05Fragment constructor");
    // filtr jSON
    mapper = new ObjectMapper();
  }

  public void createAndExecuteObservables() {
    Log.d("rxjava", "Example05Fragment createAndExecuteObservables");
    // instancje interfejsów funkcjonalnych
    // filtr
    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();
      }
    };
    // żądane są liczby losowe
    Observable<Integer> observable = Observable.empty();
    for (int i = 0; i < session.getNbRequests(); i++) {
      // przygotowywanie zapytania
      Request request = session.getRequest();
      request.setId(i);
      // konfiguracja obserwowalna
      observable = observable.mergeWith(session.getActivity().getAleas(request).filter(filter).flatMap(flatMap))
        .groupBy(groupBy).concatMap(concatMap)
        // wykonanie w wątku wejścia/wyjścia
        .subscribeOn(Schedulers.io());
    }
    // obserwacja w wątku pętli zdarzeń
    observable = observable.observeOn(AndroidSchedulers.mainThread());
    // wykonujemy te obserwowalne
    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() {
            // koniec oczekiwania
            cancelWaiting();
          }
        }
      ));

  }
}
  • wiersz 67: odpowiada obserwowalnej z przykładu 04: strumień liczb całkowitych;
  • wiersz 68: grupujemy ten strumień liczb całkowitych według kryterium logicznego, które zdefiniujemy. Otrzymamy obserwowalną typu Observable<GroupedObservable<Boolean, Integer>>, która generuje zatem elementy typu GroupedObservable<Boolean, Integer>;
  • wiersz 68: metoda [concatMap] będzie generować elementy typu Integer na podstawie elementów typu GroupedObservable<Boolean, Integer>;
  • wiersze 32–59: aby uczynić tworzenie obserwowalnej w wierszach 67–69 bardziej czytelnym, wyodrębniliśmy instancje interfejsów funkcjonalnych, których potrzebują różne operatory [filter, flatMap, groupBy, concatMap];
  • wiersze 47–52: metoda [groupBy] oczekuje parametru typu Func1<T,K>, gdzie T jest typem grupowanych elementów, a K typem kryterium grupowania. Na podstawie elementu T instancja Func1<T,K> ma za zadanie wygenerować klucz grupowania K dla tego elementu;
  • wiersze 48–51: elementy typu Integer zostaną pogrupowane według parzystości. Instancja Func1<Integer,Boolean> generuje klucz true lub false w zależności od tego, do której grupy ma zostać przypisany dany element. W wyniku operacji otrzymujemy dwie grupy: grupę elementów parzystych o kluczu true oraz grupę elementów nieparzystych o kluczu false;
  • wiersze 53–59: metoda [concatMap] oczekuje parametru typu Func1<T,Observable<R>> i zwraca obserwowalną kolekcję elementów typu R. Typem T będzie tutaj typ generowany przez operator [groupBy], w tym przypadku typ GroupedObservable<Boolean, Integer>;
  • wiersz 57: z elementu typu [GroupedObservable<Boolean, Integer>] generowany jest typ Observable<Integer>. Ponieważ operator [groupBy] wygenerował dwie grupy, operator [concatMap] wygeneruje dwa obserwowalne typu [Observable<Integer>]. Podobnie jak [flatMap], operator ten spłaszczy je do pojedynczego obserwowalnego. Jednak w odróżnieniu od [flatMap] nie miesza on elementów spłaszczonych obserwowalnych. Należy zatem zaobserwować dwie oddzielne grupy: liczby losowe parzyste i pozostałe nieparzyste.

Krok 4

Uruchamiamy aplikację:

Image

i otrzymujemy następujące wyniki:

Image

  • w pliku [1] znajdują się liczby losowe parzyste, a w pliku [2] – nieparzyste;

9.3.8.6. Aby kontynuować

Czytelnik jest teraz zachęcany do tworzenia własnych przykładów, a także do eksperymentowania z różnymi wartościami wprowadzanymi w formularzu konfigurującym zapytania kierowane do serwera liczb losowych.

9.3.9. Wnioski

W środowisku Android stworzyliśmy następującą architekturę:

Klient Android:

Warstwa [DAO] komunikuje się z serwerem, który generuje liczby losowe wyświetlane na tablecie z systemem Android. Serwer ten ma następującą dwuwarstwową architekturę:

Warstwa [DAO] wysyłała n zapytań HTTP do serwera liczb losowych, a warstwa [swing] czekała asynchronicznie na wyniki tych zapytań, aby je wyświetlić. Te n żądań HTTP były kierowane do tego samego serwera, który dostarczał ten sam typ odpowiedzi. Pozwoliło nam to połączyć (mergeWith) odpowiedzi w jedną obserwowalną wielkość.

W rzeczywistości aplikacje na Androida łączą się z różnymi serwerami i prawdopodobnie nie będziemy łączyć ich odpowiedzi. Żądania HTTP kierowane do tych serwerów będą obsługiwane niezależnie od siebie, a ich wyniki będą obserwowane za pomocą oddzielnych metod.