9. RxJava w środowisku Android
9.1. Introduction
W tym miejscu powrócimy do aplikacji omówionej już w kilku dokumentach:
- [Android pour les développeurs JEE : un modèle asynchrone pour clients Android] (rozdział 4);
- [Introduction à la programmation de tablettes Android par l'exemple] (rozdział 9);
- [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:
- 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:
- żą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);
- 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];
- 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:
- warstwę [Présentation] (widoki + aktywność);
- 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:

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:

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:
- metodę HTTP, którą należy wykorzystać do wysłania zapytania do serwisu;
- identyfikator usługi URL;
- 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

Widok odpowiedzi

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:

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:

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:

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:
- 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:

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ń):

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:

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:

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ę:

i otrzymujemy następujące wyniki:

- 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.

















































