Skip to content

9. RxJava in the Android Environment

9.1. Introduction

Here we will revisit an application already discussed in several documents:

  1. [Android for JEE Developers: An Asynchronous Model for Android Clients] (Chapter 4);
  2. [Introduction to Android Tablet Programming Through Examples] (Chapter 9);
  3. [Introduction to Android Tablet Programming Through Examples - Version 2] (Section 1.11);

It covers a client/server application where the server asynchronously delivers random numbers that the Android client displays:

  • In Document 1, the Android client uses non-standard technology;
  • in document 2, the Android client uses Android’s standard technology for asynchronous operations;
  • In Document 3, the Android client uses the same technology as in Document 2 but simplified through the use of annotations from the Android Annotations library;

The Android client is as follows:

The [DAO] layer communicates with the server that generates the random numbers displayed by the Android tablet. This server has the following two-tier architecture:

Clients query certain URLs in the [web / JSON] layer and receive a text response in JSON (JavaScript Object Notation) format.

We will break down the analysis of the application into two steps:

The web/JSON server

  • its [business] layer;
  • its [web / JSON] service implemented with Spring MVC;

The Android client

  • its [DAO] layer;
  • its activity;
  • its views;

9.2. The web service / JSON

Note: The web service / JSON is implemented using Spring MVC technology. Readers unfamiliar with this technology can either:

  • simply read section 9.2.1, which explains how to start the server and how to query it;
  • consult the document [Spring MVC and Thymeleaf by Example], particularly Chapter 4, which presents the main annotations used in the code;

9.2.1. The IntelliJ IDEA Project

The web service / JSON has the following architecture:

This architecture is implemented by the following IntelliJ IDEA project [1]:

The server is launched via [2-3]. Console logs are then displayed:


2016-05-17 10:47:12.642  INFO 13116 --- [           main] dvp.rxjava.server.boot.       : Starting Application on st-PC with PID 13116 (D:\data\istia-1516\projets\rxjava\dvp\android\serveur\build\classes\main started by st in D:\data\istia-1516\projets\rxjava\dvp\android\serveur)
2016-05-17 10:47:12.647  INFO 13116 --- [           main] dvp.rxjava.server.boot.Application       : No active profile set, falling back to default profiles: default
2016-05-17 10:47:12.706  INFO 13116 --- [           main] ationConfigEmbeddedWebApplicationContext : Refreshing org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext@71623278: startup date [Tue May 17 10:47:12 CEST 2016]; root of context hierarchy
2016-05-17 10:47:13.736  INFO 13116 --- [           main] s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat initialized with port(s): 8080 (http)
2016-05-17 10:47:13.749  INFO 13116 --- [           main] o.apache.catalina.core.StandardService   : Starting service Tomcat
2016-05-17 10:47:13.750  INFO 13116 --- [           main] org.apache.catalina.core.StandardEngine  : Starting Servlet Engine: Apache Tomcat/8.0.33
2016-05-17 10:47:13.914  INFO 13116 --- [ost-startStop-1] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring embedded WebApplicationContext
2016-05-17 10:47:13.914  INFO 13116 --- [ost-startStop-1] o.s.web.context.ContextLoader            : Root WebApplicationContext: initialization completed in 1214 ms
2016-05-17 10:47:13.965  INFO 13116 --- [ost-startStop-1] o.s.b.c.e.ServletRegistrationBean        : Mapping servlet: 'dispatcherServlet' to [/*]
2016-05-17 10:47:14.251  INFO 13116 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/{a}/{b}/{minCount}/{maxCount}/{minDelay}/{maxDelay}],methods=[GET],produces=[application/json]}" onto public java.lang.String dvp.rxjava.server.web.AleasController.getAleas(int,int,int,int,int,int) throws com.fasterxml.jackson.core.JsonProcessingException
2016-05-17 10:47:14.342  INFO 13116 --- [           main] s.w.s.m.m.a.RequestMappingHandlerAdapter : Looking for @ControllerAdvice: org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext@71623278: startup date [Tue May 17 10:47:12 CEST 2016]; root of context hierarchy
2016-05-17 10:47:14.485  INFO 13116 --- [           main] s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat started on port(s): 8080 (http)
2016-05-17 10:47:14.489  INFO 13116 --- [           main] dvp.rxjava.server.boot.Application       : Started Application in 2.289 seconds (JVM running for 2.859)
2016-05-17 10:48:37.061  INFO 13116 --- [nio-8080-exec-2] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring FrameworkServlet 'dispatcherServlet'
2016-05-17 10:48:37.061  INFO 13116 --- [nio-8080-exec-2] o.s.web.servlet.DispatcherServlet        : FrameworkServlet 'dispatcherServlet': initialization started
2016-05-17 10:48:37.087  INFO 13116 --- [nio-8080-exec-2] o.s.web.servlet.DispatcherServlet        : FrameworkServlet 'dispatcherServlet': initialization completed in 26 ms
  • Line 12: indicates that the service is available on port 8080;
  • line 10: the unique URL of the web service / JSON available via an HTTP GET operation. Its parameters are as follows:
    • [a,b]: range for generating random numbers;
    • [minCount, maxCount]: count random numbers are generated, where count is a random number in the interval [minCount, maxCount];
    • [minDelay, maxDelay]: the service waits delay milliseconds before returning the requested numbers, where delay is a random number in [minDelay, maxDelay];

In a browser, let’s request this URL:

 

We requested:

  • random numbers in the interval [100, 200];
  • n random numbers where n is in the interval [10, 20];
  • a wait time of x milliseconds, where x is in the interval [300, 400];

In the response:

  • aleas: list of generated random numbers;
  • delay: the wait time in milliseconds that the server has set;
  • error: an error code—0 if no error;
  • message: an error message - null if no error;

9.2.2. The project's Gradle dependencies

  

The [server] project is a Gradle project configured by the following [build.gradle] file [1]:


// generated by http://start.spring.io/ (May 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 = 'server'
  version = '0.0.1-SNAPSHOT'
}

sourceCompatibility = 1.8
targetCompatibility = 1.8

repositories {
  mavenCentral()
}

dependencies {
  compile('org.springframework.boot:spring-boot-starter-web')
}
  • line 1: a comment explaining how this configuration file was generated;
  • lines 4 and 10: a dependency on the [Spring Boot] framework, a branch of the Spring ecosystem. This [http://projects.spring.io/spring-boot/] framework allows for a minimal Spring configuration. Based on the libraries present in the project’s classpath, [Spring Boot] infers a plausible or likely configuration for the project. Thus, if Hibernate is in the project’s classpath, then [Spring Boot] will infer that the JPA implementation being used is Hibernate and will configure Spring accordingly. The developer no longer has to do this. All that remains for them to do is configure the settings that [Spring Boot] did not configure by default, or those that [Spring Boot] did configure by default but which need to be specified. In any case, the configuration made by the developer takes precedence;
  • lines 14–15: two Gradle plugins required to utilize the contents of this Gradle file;
  • lines 17–20: define the characteristics of the archive generated for this project;
  • lines 22–23: for compatibility with Java 8;
  • lines 25–27: dependencies will be searched for in the global Maven repository or in the local repository on the machine;
  • line 30: defines a dependency on the [spring-boot-starter-web] artifact. This artifact includes all the archives necessary for a Spring MVC project. Among these is the Tomcat server archive. This is the one that will be used to deploy the web application. Note that the dependency version has not been specified. The version specified in the imported [spring-boot] project will be used;

To update the project, you must force the download of dependencies [1-3]:

Let’s look at [4] the dependencies included in this [build.gradle] file:

 

There are a lot of them. Spring Boot for the web has included the dependencies that a Spring MVC web application will likely need. This means that some may be unnecessary. Spring Boot is ideal for a tutorial:

  • it includes the dependencies we’ll likely need;
  • we’ll see that it greatly simplifies the configuration of the Spring MVC project;
  • it includes an embedded Tomcat server [1], which saves us from having to deploy the application on an external web server;
  • it allows us to generate an executable JAR file that includes all of the above dependencies. This JAR file can be moved from one platform to another without reconfiguration.

You can find many examples using Spring Boot on the Spring ecosystem website [http://spring.io/guides]. Now that we know the project’s dependencies, we can move on to the code.

9.2.3. The [business] layer

  

The [business] layer will have the following [IMetier] interface:


package dvp.rxjava.server.business;

public interface IBusiness {
  // random numbers in the interval [a, b]
  // n numbers are generated, where n itself is a random number in the interval [minCount, maxCount]
  // the numbers are generated after a delay of delay milliseconds,
  // where [delay] is itself a random number in the interval [minDelay, maxDelay]
  public AleasMetier getAleas(int a, int b, int minCount, int maxCount, int minDelay, int maxDelay);
}

This interface is nearly identical to the one discussed in the Swing environment in Section 8.4. In line 8, the [getAleas] method returns the following [AleasMetier] type:


package dvp.rxjava.server.metier;

import java.util.List;

public class BusinessRandom {
  // fields
  private int delay;
  private List<Integer> aleas;

  // constructors
  public AleasMetier(){

  }

  public AleasMetier(int delay, List<Integer> aleas) {
    this.delay = delay;
    this.random = random;
  }

  public AleasMetier(AleasMetier aleasMetier){
    this.delay = aleasMetier.delay;
    this.aleas = aleasMetier.aleas;
  }

  // getters and setters
...
}

The code for the [Metier] class implementing the [IMetier] interface is as follows:


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 BusinessImplementation implements IBusinessImplementation {

  @Autowired
  private ObjectMapper mapper;

  @Override
  public BusinessRandom getRandom(int a, int b, int minCount, int maxCount, int minDelay, int maxDelay) {
    // random numbers in the interval [a,b]
    // n numbers are generated, where n itself is a random number in the range [minCount, maxCount]
    // the numbers are generated after a delay of delay milliseconds,
    // where [delay] is itself a random number in the interval [minDelay, maxDelay]

    // some checks
    List<String> messages = new ArrayList<>();
    int error = 0;
    if (a < 0) {
      messages.add("The number a from the generation interval [a,b] must be greater than 0");
      error |= 2;
    }
    if (a >= b) {
      messages.add("In the generation interval [a,b], a must be less than b");
      error |= 4;
    }
    if (minCount < 0) {
      messages.add("The minimum number in the [min, count] interval of generated values must be greater than 0");
      error |= 16;
    }
    if (minCount > maxCount) {
      messages.add("In the interval [min, count] of the number of generated values, min must be less than or equal to max");
      error |= 32;
    }
    if (minDelay < 0) {
      messages.add("The minimum value of the [min, count] interval for the timeout must be greater than 0");
      error |= 64;
    }
    if (minCount > maxCount) {
      messages.add("In the [min, count] interval of the timeout, min must be less than or equal to max");
      error |= 128;
    }
    if (maxDelay > 5000) {
      messages.add("The wait time in milliseconds before generating numbers must be in the range [0,5000]");
      error |= 256;
    }
    // errors?
    if (!messages.isEmpty()) {
      throw new AleasException(String.join(" [---] ", messages), error);
    }
    // random number generator
    Random random = new Random();
    // wait?
    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);
      }
    }
    // generate result
    int count = minCount + random.nextInt(maxCount - minCount + 1);
    List<Integer> numbers = new ArrayList<Integer>();
    for (int i = 0; i < count; i++) {
      numbers.add(a + random.nextInt(b - a + 1));
    }
    // return result
    return new AleasMetier(delay, numbers);
  }

}

We will not comment on the class: it is similar to the one encountered in the Swing environment in Section 8.4. We will simply note the following points:

  • line 10: the Spring annotation [@Service], which causes Spring to instantiate the class as a single instance (singleton) and make its reference available to other Spring components. Other Spring annotations could have been used here to achieve the same effect;
  • lines 13–14: a JSON mapper is injected. Spring is an object container. This container is instantiated when the web application starts, and objects defined by a configuration file are then instantiated, by default as a single instance (singleton). A Spring singleton can contain references to other Spring objects. This is the case here: the [business] singleton (lines 10–11) will have a reference to the [mapper] singleton (lines 13–14). This is called dependency injection. There are two ways to inject a singleton into another singleton:
    • by its type: this is possible if the singleton to be injected is the only Spring object of that type. This is the case here for the injection in lines 13–14 (type ObjectMapper);
    • by its name if multiple Spring objects have the same type. In this case, you must add the @Qualifier("singletonName") annotation to specify the singleton’s name;

The [Metier] class throws exceptions of type [AleaException]:


package android.exemples.server.metier;

public class AleaException extends RuntimeException {

  // error code
  private int code;

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

  // getters and setters

  public int getCode() {
    return code;
  }

  public void setCode(int code) {
    this.code = code;
  }
}
  • line 3: [AleasException] extends the [RuntimeException] class. It is therefore an unhandled exception (no requirement to handle it with a try/catch);
  • line 6: an error code is added to the [RuntimeException] class;

9.2.4. The Web Service / JSON

  

The web service / JSON is implemented by Spring MVC. Spring MVC implements the MVC (Model–View–Controller) architectural pattern as follows:

The processing of a client request proceeds as follows:

  1. request – the requested URLs are of the form http://machine:port/contexte/Action/param1/param2/....?p1=v1&p2=v2&... The [Dispatcher Servlet] is the Spring class that handles incoming URLs. It "routes" the URL to the action that must process it. These actions are methods of specific classes called [Controllers]. The C in MVC here is the chain [Dispatcher Servlet, Controller, Action]. If no action has been configured to handle the incoming URL, the [Dispatcher Servlet] will respond that the requested URL was not found (404 NOT FOUND error);
  1. processing
  • the selected action can use the parameters that the [Dispatcher Servlet] has passed to it. These can come from several sources:
    • the path [/param1/param2/...] of the URL,
    • the URL parameters [p1=v1&p2=v2],
    • from parameters posted by the browser with its request;
  • when processing the user's request, the action may need the [business] layer [2b]. Once the client's request has been processed, it may trigger various responses. A classic example is:
    • an error page if the request could not be processed correctly
    • a confirmation page otherwise
  • the action instructs a specific view to be displayed [3]. This view will display data known as the view model. This is the M in MVC. The action will create this M model [2c] and instruct a V view to be displayed [3];
  1. response - the selected view V uses the model M constructed by the action to initialize the dynamic parts of the HTML response it must send to the client, then sends this response.

For a web service / JSON, the previous architecture is slightly modified:

  • in [4a], the model, which is a Java class, is converted into a JSON string by a JSON library;
  • in [4b], this JSON string is sent to the browser;

Let’s return to the [web] layer of our application:

In our application, there is only one controller:

  

The web service / JSON will send its clients a response of type [AleasResponse] as follows:


package dvp.rxjava.server.web;

import dvp.rxjava.server.business.AleasBusiness;

public class AleasResponse extends AleasMetier {

  // error code
  private int error;
  // error message
  private String message;

  // constructors
  public AleasResponse() {

  }

  public AleasResponse(int error, String message, AleasMetier aleasMetier) {
    super(businessException);
    this.error = error;
    this.message = message;
  }
  // getters and setters

  public void setBusinessRisk(BusinessRisk businessRisk) {
    this.setDelay(businessRandom.getDelay());
    this.setRandom(businessRandom.getRandom());
  }
...
}
  • line 5: the [AleasResponse] class extends the [AleasMetier] class and therefore inherits all of its attributes (aleas, delay);
  • line 8: an error code (0 if no error);
  • line 10: if error != 0, an error message; null if no error;

The [AleasController] controller is as follows:


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.business.AleasException;
import dvp.rxjava.server.business.IMetier;

@Controller
public class AleasController {

    // Business layer
    @Autowired
    private IBusinessModel businessModel;
    @Autowired
    private ObjectMapper mapper;

    // random numbers in [a,b]
    // n numbers are generated with n in the range [minCount, maxCount]
    // numbers are generated after a delay of delay milliseconds,
    // where [delay] is a random number in the interval [minDelay, maxDelay]
    @RequestMapping(value = "/{a}/{b}/{minCount}/{maxCount}/{minDelay}/{maxDelay}", method = RequestMethod.GET, produces = "application/json")
    @ResponseBody
    public String getRandom(@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 {

        // prepare the response
        AleasResponse response = new AleasResponse();
        // use the business layer to generate random numbers
        try {
            response.setAleasMetier(metier.getAleas(a, b, minCount, maxCount, minDelay, maxDelay));
        } catch (AleasException e) {
            // error case (code and message)
            response.setError(e.getCode());
            response.setMessage(e.getMessage());
        }
        // Return the JSON response
        return mapper.writeValueAsString(response);
    }
}
  • line 16: the [@Controller] annotation makes the [AleasController] class a Spring singleton. It also indicates that the class contains methods that will handle requests for certain URLs in the web application. Here, there is only one on line 29;
  • lines 20–21: The [@Autowired] annotation instructs Spring to inject a component of type [IMetier] into the field. This will be the previous [Metier] class. Because we added the [@Service] annotation to it, it is treated as a Spring component;
  • lines 22–23: the [@Autowired] annotation instructs Spring to inject a component of type [ObjectMapper] into the field. We will define this shortly;
  • line 31: the [getAleas] method generates random numbers. Its name is irrelevant. When it runs, the parameters on lines 31–33 have been initialized by Spring MVC. We’ll see how. Furthermore, it runs because the web server received an HTTP GET request for the URL in line 29 (method attribute);
  • line 30: the [@ResponseBody] annotation indicates that the method’s result must be sent as-is to the client. Here, we will send it a string that will be the JSON string of type [AleasResponse];
  • Line 29: The processed URL is of the form /{a}/{b}/{minCount}/{maxCount}/{minDelay}/{maxDelay}, where {x} represents a variable. These various variables are assigned to the method’s parameters on lines 32–33. This is done via the @PathVariable("x") annotation. Note that the {x} values are components of a URL and are therefore of type String. The conversion from String to the method’s parameter type may fail. Spring MVC then throws an exception. To summarize: if I request the URL /100/200/10/20/300/400 in a browser, the getAleas method on line 31 will execute with the parameters a=100 (line 31), b=200 (line 31), minCount=10 (line 31), maxCount=20 (line 32), minDelay=300 (line 32), maxDelay=400 (line 33);
  • line 39: we request a list of random numbers from the [business] layer. Remember that the [business].getRandom method can throw an exception;
  • lines 42–43: error handling;
  • line 46: the [AleasResponse] response is returned as a JSON string;

9.2.5. Spring Project Configuration

  

There are various ways to configure Spring:

  • using XML files;
  • with Java code;
  • using a combination of both;

We choose to configure our web application using Java code. The [Config] class above handles this configuration:


package dvp.rxjava.server.config;

import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.embedded.EmbeddedServletContainerFactory;
import org.springframework.boot.context.embedded.ServletRegistrationBean;
import org.springframework.boot.context.embedded.tomcat.TomcatEmbeddedServletContainerFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.servlet.DispatcherServlet;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;

@ComponentScan(basePackages = { "dvp.rxjava.server.metier", "dvp.rxjava.server.web" })
@EnableWebMvc
public class Config {
  // -------------------------------- [web] layer configuration
  @Autowired
  private ApplicationContext context;

  @Bean
  public DispatcherServlet dispatcherServlet() {
    DispatcherServlet servlet = new DispatcherServlet((WebApplicationContext) context);
    return servlet;
  }

  @Bean
  public ServletRegistrationBean servletRegistrationBean(DispatcherServlet dispatcherServlet) {
    return new ServletRegistrationBean(dispatcherServlet, "/*");
  }

  @Bean
  public EmbeddedServletContainerFactory embeddedServletContainerFactory() {
    return new TomcatEmbeddedServletContainerFactory("", 8080);
  }

  // JSON mapper
  @Bean
  public ObjectMapper jsonMapper() {
    return new ObjectMapper();
  }
}
  • Line 15: We tell Spring in which packages it will find objects to instantiate. It will find two:
    • the [Metier] class annotated with [@Service];
    • the [AleasController] class annotated with [@Controller];
  • line 16: the [@EnableWebMvc] annotation triggers automatic configurations for the Spring MVC framework;
  • lines 19–20: injection of the Spring context (container for Spring objects). This injection is necessary because the object in lines 22–26 requires it;
  • the Spring configuration file can define new Spring objects using methods annotated with [@Bean]. The result of the method then becomes a Spring object;
  • lines 22–26: definition of the Spring MVC framework’s servlet, which routes HTTP requests to the correct controller and method. [DispatcherServlet] is a Spring class;
  • lines 28–31: this specifies that this servlet handles all URLs;
  • lines 33–36: the presence of this bean will activate the Tomcat server included in the project archives. It will listen for requests on port 8080;
  • lines 39–42: a JSON mapper. This is the one that has been injected into the Spring objects [Metier] and [AleasController];

9.2.6. Running the web server

  

The project runs from the following executable class [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) {
    // Run the application
    SpringApplication.run(Config.class, args);
  }

}
  • line 6: the [Application] class is an executable class (lines 7–10);
  • line 9: the static method [SpringApplication.run] is a method from [Spring Boot] (line 4) that will launch the application. Its first parameter is the Java class that configures the project. Here, it is the [Config] class we just described. The second parameter is the array of arguments passed to the [main] method (line 7). Here, there will be no arguments;

For the actual execution, the reader is invited to refer back to section 9.2.1.

9.3. The Android Client

Note: The following Android project is quite complex. It requires a solid understanding of Android, which can be found, for example, in [Introduction to Android Tablet Programming with Android Studio].

ActivityViewsLayer[DAO]UserServer

The client will have two components:

  1. a [Presentation] layer (views + activity);
  2. a [DAO] layer that communicates with the [web / JSON] service we studied earlier.

9.3.1. RxAndroid

To communicate asynchronously with the random number server, the Android client will use the RxAndroid library. This library extends RxJava to the Android ecosystem. As we did for the Swing application, we will use only a single extension provided by RxAndroid: the scheduler [AndroidSchedulers.mainThread()]. An Android GUI follows the same rules as a Swing interface:

  • events are processed in a single thread called the event loop or UI thread;
  • when an event triggers asynchronous actions, the results of those actions must be retrieved in the UI thread if they are to be used to update the UI;

The Android client:

  • will send multiple asynchronous requests to the random number server. These requests will be executed on the client side using the scheduler’s threads [Schedulers.io()];
  • These asynchronous requests will return observables that will be merged into a single observable;
  • This observable will be observed on the client side in the scheduler [AndroidSchedulers.mainThread()] provided by RxAndroid;

9.3.2. The IntelliJ IDEA Project

The Android project is named [client]:

It will be run via [2].

Note: Execution is highly dependent on the configuration of the IntelliJ IDEA IDE being used. It is likely that the execution [2] above will not work on the first try on a machine other than mine. Properly configuring the IntelliJ IDEA IDE to run this project can be a daunting task for beginners. Here are a few points to look at:

  • in [3], access the project structure;
  • in [4-5], the JDK and Android SDKs installed on my machine. Note that JDK 1.8 is not essential. Android does not support certain Java 8 features, including lambdas. Therefore, to instantiate functional interfaces, we will use anonymous classes. A JDK 1.6 is therefore sufficient. However, the project as distributed has been configured with a JDK 1.8;

The [build.gradle] [6] file that configures the Android project is as follows:


buildscript {
  repositories {
    mavenCentral()
    mavenLocal()
  }
  dependencies {
    // replace with the current version of the Android plugin
    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'
  }
}

Depending on the Android SDKs present, the versions in lines 8, 24–25, and 29 may need to be modified.

To install new Android SDKs, use the SDK Manager as follows [1]:

The project has been configured for:

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

Finally, verify the Android SDK path in the [local.properties] file [4], line 11 below:


## This file is automatically generated by Android Studio.
# Do not modify this file -- YOUR CHANGES WILL BE ERASED!
#
# This file must *NOT* be checked into Version Control Systems,
# as it contains information specific to your local configuration.
#
# Location of the SDK. This is only used by Gradle.
# For customization when using a version control system, please read the
# header note.
#Thu Apr 07 14:51:14 CEST 2016
sdk.dir=C:\Users\st\AppData\Local\Android\sdk

9.3.3. Running the IntelliJ IDEA Project

Once a proper environment for the project has been created, it can be run as follows:

  • In [1], launch the Genymotion Android emulator;
  • in [2], run the [app] run configuration;
  • in [3], to create a run configuration;
 
  • In [1, 3], the configuration was named [app];
  • in [2], it corresponds to the execution of the module named [app];
  • in [4], we specify that during execution, the IDE should offer us an execution device. Here, this will always be the Genymotion emulator;
  • in [5], we specify that this device should be used for all executions of the configuration;

Running the project on the Genymotion emulator begins with the following initial command:

Image

To find out what to enter in [1], open a DOS command window and type the following [ipconfig] command:


C:\Program Files\Console2>ipconfig

Windows IP Configuration


Ethernet adapter:

   Media status. . . . . . . . . . . . : Media disconnected
   Connection-specific DNS suffix. . . : ad.univ-angers.fr

Wireless Network Adapter Local Area Connection* 3:

   Media status. . . . . . . . . . . . : Media disconnected
   Connection-specific DNS suffix. . . :

VirtualBox Host-Only Network Ethernet adapter:

   Connection-specific DNS suffix. . . :
   Local link IPv6 address. . . . .: fe80::8076:36e6:3b38:5e98%16
   IPv4 address . . . . . . . . . . . . . .: 192.168.56.2
   Subnet mask. . . . . . . . . : 255.255.255.0
   Default gateway. . . . . . . . . :

Ethernet 2 card:

   Connection-specific DNS suffix. . . :
   Local link IPv6 address . . . . .: fe80::d0d9:e01f:ddde:1f4b%14
   IPv4 address . . . . . . . . . . . . . .: 192.168.95.1
   Subnet mask. . . . . . . . . . . . . . . : 255.255.255.0
   Default gateway. . . . . . . . . :

Wi-Fi wireless network adapter:

   Connection-specific DNS suffix. . . :
   Local link IPv6 address . . . . .: fe80::54b3:afe5:e199:2206%10
   IPv4 address . . . . . . . . . . . . . .: 192.168.0.13
   Subnet mask. . . . . . . . . . . . . . . : 255.255.255.0
   Default gateway. . . . . . . . . : fe80::523d:e5ff:fe0c:4ad9 192.168.0.1


Enter [1] for one of your machine's IP addresses (lines 20, 28, 32). If you have a Windows firewall, you will likely need to disable it so that the Android emulator can reach the random number server.

Executing asynchronous requests with the information above yields the following results:

Image

Each request returns a JSON response with the following fields:

  • aleas: the random numbers generated by the server;
  • idClient: the request ID;
  • on: the client-side thread executing the request;
  • requestAt: time of the request;
  • responseAt: time the response was received;
  • delay: the wait time the server observed before returning its response;
  • error: an error code—0 if no error;
  • message: an error message - null if no error;
  • observedAt: time the response was observed;
  • observedOn: thread observing the response. Here, this will always be [main], which refers to the UI thread;

Since requests are asynchronous and the wait times imposed on the server are random, responses return in a scattered order.

9.3.4. The project's Gradle dependencies

The project requires dependencies, which we specify in the [app/build.gradle] file:

  

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'
}
  • The dependencies on lines 2–3 are standard dependencies for an Android project using SDK 23;
  • The dependency on line 5 brings in the Spring [RestTemplate] object, which manages communication between the [DAO] layer and the server;
  • the dependency on line 6 brings in the JSON library [Jackson] used by the application;
  • The dependency on line 7 brings in the RxAndroid library (and with it the RxJava library) that the UI layer uses to communicate with the [DAO] layer;

9.3.5. The Android application manifest

  

<?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>
  • Line 5: Internet access must be allowed;

9.3.6. The [DAO] layer

 

9.3.6.1. The [IDao] interface of the [DAO] layer

The interface of the [DAO] layer will be as follows:


package android.aleas.dao;

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

public interface IDao {

  // random numbers in the interval [a, b]
  // n numbers are generated, where n itself is a random number in the interval [minCount, maxCount]
  // the numbers are generated after a delay of delay milliseconds,
  // where [delay] is itself a random number in the interval [minDelay, maxDelay]
  public Observable<AleasDaoResponse> getAleas(final Request request);

  // Web service URL
  public void setUrlServiceWebJson(String url);

  // maximum wait time (ms) for the server's response to a connection request
  // Maximum wait time (ms) for the server's response to a request
  public void setClientTimeouts(int connectTimeout, int readTimeout);

}
  • line 12: the [DAO] layer method that generates random numbers asynchronously;
  • line 15: to provide the [DAO] implementation with the URL of the random number generation service;
  • line 19: to set maximum timeouts for the [DAO] implementation, to avoid excessively long waits when the server does not respond;

The [getAleas] method receives all its parameters in the following [Request] object:


package android.aleas.fragments;

public class Request {

  // request ID
  int id;
  // user input
  private int numRequests;
  private int a;
  private int b;
  private int minCount;
  private int maxCount;
  private int minDelay;
  private int maxDelay;

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

  // getters and setters
...
}

Here, we can see most of the parameters from the server URL that needs to be queried.

The [getAleas] method returns an Observable<AleasDaoResponse> type, where the [AleasDaoResponse] class is as follows:


package android.aleas.dao;

import java.util.List;

public class AleasDaoResponse {

  // error code
  private int error;
  // error message
  private String message;
  // server timeout
  private int delay;
  // Random numbers generated by the server
  private List<Integer> aleas;
  // client state
  private ClientState clientState;

  // constructors

  public AleasDaoResponse() {
  }

  public AleasDaoResponse(int error, String message, int delay, List<Integer> aleas, ClientState clientState) {
    this.error = error;
    this.message = message;
    this.delay = delay;
    this.random = random;
    this.clientState = clientState;
  }

  // getters and setters
...
}

The [ClientState] type is as follows:


package android.aleas.dao;

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

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

public class ClientState {

  // name of the execution thread
  private String on;
  // time of the request
  private String requestAt;
  // response time
  private String responseAt;
  // client ID
  private int clientId;

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

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

  // private methods

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

  // getters and setters
...
}
  • line 11: execution thread of the [DAO] layer;
  • line 13: request time;
  • line 15: response time;
  • line 17: request number;

The fields [on, requestAt, idClient] are initialized by the client at the start of the request. The field [responseAt] is initialized when the client receives the response from the server.

9.3.6.2. Implementation of the [DAO] layer

  

The [IDao] interface is implemented with the following [Dao] class:


package android.aleas.dao;

import android.aleas.fragments.Request;
import android.util.Log;
import org.codehaus.jackson.map.ObjectMapper;
import org.codehaus.jackson.map.ser.impl.SimpleBeanPropertyFilter;
import org.codehaus.jackson.map.ser.impl.SimpleFilterProvider;
import org.codehaus.jackson.type.TypeReference;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.http.converter.StringHttpMessageConverter;
import org.springframework.web.client.RestTemplate;
import rx.Observable;
import rx.Subscriber;

import java.util.HashMap;
import java.util.Locale;
import java.util.Map;

public class Dao implements IDao {

  // REST client
  private RestTemplate restTemplate;
  // Service URL
  private String webServiceJsonURL;

  // JSON mapper
  private ObjectMapper mapper;

  // constructors
  public Dao() {
    // JSON mapper
    mapper = new ObjectMapper();
  }

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

  @Override
  public void setUrlServiceWebJson(String urlServiceWebJson) {
    // Set the REST service URL
    this.urlServiceWebJson = urlServiceWebJson;
  }

  @Override
  public void setClientTimeouts(int connectTimeout, int readTimeout) {
...
  }
}
  • line 22: the [RestTemplate] object that will handle communication with the random number server;
  • line 24: the URL of the generation service—set by the [setUrlServiceWebJson] method on line 41;
  • line 27: the JSON mapper used to deserialize the JSON string sent by the random number server;
  • lines 30–33: the class constructor;
  • line 32: the JSON mapper from line 27 is created;

The [setClientTimeouts] method is as follows:


  // REST client
  private RestTemplate restTemplate;
...

  @Override
  public void setClientTimeouts(int connectTimeout, int readTimeout) {
    // Set the timeout for REST client requests
    HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory();
    factory.setReadTimeout(readTimeOut);
    factory.setConnectTimeout(connectTimeout);
    restTemplate = new RestTemplate(factory);
    restTemplate.getMessageConverters().add(new StringHttpMessageConverter());
}
  • The client's communication with the web server / JSON is handled by the [RestTemplate] object on line 2. We haven't initialized it yet. The [setClientTimeouts] method does this;
  • Line 8: The [HttpComponentsClientHttpRequestFactory] class is provided by the [spring-android-rest-template] dependency. It will allow us to set the maximum wait times for the server’s response (lines 9–10);
  • line 11: we construct the [RestTemplate] object, which will serve as the communication channel with the web service. We pass the [factory] object that was just constructed as a parameter to it;
  • line 12: the client/server dialogue can take various forms. Exchanges occur via text lines, and we must tell the [RestTemplate] object what to do with this text line. To do this, we provide it with converters—classes capable of processing text lines. The choice of converter is generally made via the HTTP headers accompanying the text line. Based on these headers, the [RestTemplate] object will select, from among its converters, the one best suited to the situation. Here, we will have only a single converter, a String --> String converter, which means that the String type received from the server will not undergo any transformation.

The [getAleas] method is the most complex:


@Override
  public Observable<AleasDaoResponse> getAleas(final Request request) {
    Log.d("rxjava", String.format("service [DAO] for client #%s%n", request.getId()));
    // execute service
    return Observable.create(new Observable.OnSubscribe<AleasDaoResponse>() {
      @Override
      public void call(Subscriber<? super AleasDaoResponse> subscriber) {
        try {
          // Service URL: /{a}/{b}/{minCount}/{maxCount}/{minDelay}/{maxDelay}
          String urlService = String.format("%s/%s/%s/%s/%s/%s/%s",
            urlServiceWebJson, request.getA(), request.getB(), request.getMinCount(),
            request.getMaxCount(), request.getMinDelay(), request.getMaxDelay());
          // client information
          ClientState clientState = new ClientState(request.getId());
          // synchronous HTTP request
          String response = executeRestService("get", urlService, null);
          // deserialize the JSON response from the server
          AleasServerResponse aleasServerResponse = mapper.readValue(
            response,
            new TypeReference<AleasServerResponse>() {
            });
          // Error?
          int error = aleasServerResponse.getError();
          if (error != 0) {
            // propagate the exception
            subscriber.onError(new AleasException(aleasServerResponse.getMessage(), error));
          } else {
            // record the time of receipt
            clientState.setResponseAt();
            // forward the result to the subscriber
            subscriber.onNext(
              new AleasDaoResponse(aleasServerResponse.getError(), aleasServerResponse.getMessage(),
                aleasServerResponse.getDelay(), aleasServerResponse.getAleas(), clientState));
          }
        } catch (Exception ex) {
          // forward the exception to the subscriber
          subscriber.onError(ex);
        } finally {
          // signal the end of the observable
          // At runtime, we observe that this method has no effect if the [onError] method has been called previously     —as expected—so we could place this statement only within the try block
          subscriber.onCompleted();
        }
      }
    });
  }
  • line 2: remember that we must return a type [Observable<AleasResponse>];
  • line 3: a log line on the Android console;
  • line 5: the [RestTemplate] object ensures synchronous communication with the server. This means that the execution thread making the request is blocked until the response is received. In the Swing example, we saw how to transform a synchronous action into an asynchronous one using the [Observable.create] method. We are following the same approach here;
  • line 7: the [call] method of the [Observable.OnSubscribe<AleasDaoResponse>] interface from line 5. This method is called when an observer subscribes to the observable;
  • lines 10–12: construction of the URL for the random number service;
  • line 14: initialization of the [ClientState] object. Here, we record the time of the request;
  • line 16: synchronous HTTP request. A JSON response is returned. The [executeRestService] method expects three parameters:
      1. the HTTP method to use to query the service;
      2. the service URL;
      3. the object to be posted (type Object), null if the HTTP method is not POST;
  • 18-21: Deserialization of the received JSON string into an [AleasServerResponse] type. This type is as follows:

package android.aleas.dao;

import java.util.List;

public class AleasServerResponse {

  // error code
  private int error;
  // error message
  private String message;
  // server timeout
  private int delay;
  // random numbers
  private List<Integer> randomNumbers;

  // getters and setters
...
}
  • line 23: retrieve the error code sent by the server;
  • lines 24–26: if an error occurs, an exception is forwarded to the subscriber;
  • line 29: we update [clientState], which will be part of the response sent to the subscriber;
  • lines 31–33: send the response to the subscriber. It is of type [AleasDaoResponse];
  • lines 35–37: handle all error cases without distinction. The most likely error is a network error;
  • line 41: notification of end of transmission;

9.3.7. Application Views

  

The application has the following two views:

The request view

Image

The response view

Image

9.3.7.1. The [MyFragment] class

There are two fragments:

  • [RequestFragment] for the request;
  • [ResponseFragment] for the response;

Both fragments extend the following [MyFragment] class:


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 {

  // ------------- data shared by fragments
  protected MainActivity activity;
  protected Session session;

  public abstract void onRefresh();

}
  • line 7: the [MyFragment] class extends the Android [Fragment] class;
  • lines 10–11: data shared by all fragments;
  • line 10: each fragment knows the application's single activity;
  • line 11: to communicate with each other, fragments use a session;
  • line 13: before displaying a fragment, it will be asked to refresh itself with the session’s content. This method is declared abstract because it is implemented by the child classes. For this reason, the class itself is declared abstract (line 7);

The [Session] class contains the data shared by the application’s various fragments. Its code is as follows:

  

package android.aleas.activity;

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

public class Session {

  // application activity
  private MainActivity activity;
  // number of requests
  private int numRequests;
  // request characteristics
  private int a;
  private int b;
  private int minCount;
  private int maxCount;
  private int minDelay;
  private int maxDelay;
  // Web service URL / JSON
  private String webJsonUrl;
  // Operation started
  private boolean onAir;
  // Same, but a little later
  private boolean operationStarted;
  // the name of the example selected by the user from the list of examples
  private String exampleName;
  // its number in the list of fragments
  private int examplePosition;
  // the adapter for the example spinner in the query view
  private ArrayAdapter<CharSequence> spinnerExemplesAdapter;

  // methods
  public void setInfo(int nbRequests, int a, int b, int minCount, int maxCount, int minDelay, int maxDelay, String webJsonUrl, 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);
  }

  // getters and setters
...
}

The method on line 46 creates the [Request] object, which encapsulates all the information provided by the user in the request view:

  

package android.aleas.fragments;

public class Request {

  // request ID
  int id;
  // user input
  private int numRequests;
  private int a;
  private int b;
  private int minCount;
  private int maxCount;
  private int minDelay;
  private int maxDelay;

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

  // getters and setters
....
}

9.3.7.2. The [RequestFragment] fragment of the request

The request fragment has the following components:

Image

The application has a single view, which is a view with two tabs:

  • [1]: the request tab;
  • [2]: the response tab;

The components of the [RequestFragment] fragment are as follows:

No.
Type
Name
Role
3
EditText
edtNbRequests
number of requests to make to the random number generator service
4
EditText
edtA, edtB
the bounds [a,b] of the number generation interval;
5
EditText
edtMinCount, edtMaxCount
the service generates count numbers, where count is a random number in the interval [minCount, maxCount]
6
EditText
edtMinDelay, edtMaxDelay
The service waits delay milliseconds before generating the numbers, where delay is a random number in the range [minDelay, maxDelay]
7
EditText
edtUrlServiceRest
the URL of the random number generation service;
8
Spinner
spinnerExamples
the dropdown list of examples. Each example illustrates a specific method of the [Observable] class;
8
Button
btnExecute
the button that triggers calls to the number generation service;

Input errors are reported:

Image

Components 1 through 6 are [TextView] components with the following names (in order): txtErrorRequests, txtErrorInterval, txtErrorCount, txtErrorDelay, txtWebServiceErrorMessage.

9.3.7.3. The [ResponseFragment] fragment of the response

The response fragment has the following components:

Image

No.
Type
Name
Role
1
TextView
infoResponses
number of responses received
2
ListView
listResponses
list of JSON strings received from the server
3
Button
btnCancel
to cancel requests to the server

9.3.7.4. The Android activity [MainActivity]

  

The [MainActivity] class displays the following view:


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

  <!-- app bar -->
  <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">

    <!-- toolbar -->
    <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">

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

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

  <!-- view container -->
  <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>

The components of this view are as follows:

lines
Type
Name
Role
20-34
Toolbar
toolbar
application toolbar
29-34
ProgressBar
loadingPanel
placeholder image displayed while the user's request is being processed
37-40
TabLayout
tabs
the application's tab bar
44-51
MyPager
container
the container in which the various fragments of the application are displayed

The [MyPager] class is as follows:


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 {

  // controls swiping
  private boolean isSwipeEnabled;

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

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

  // method overrides
  @Override
  public boolean onInterceptTouchEvent(MotionEvent event) {
    // Is swiping allowed?
    if (isSwipeEnabled) {
      return super.onInterceptTouchEvent(event);
    } else {
      return false;
    }
  }

  @Override
  public boolean onTouchEvent(MotionEvent event) {
    // Is swiping allowed?
    if (isSwipeEnabled) {
      return super.onTouchEvent(event);
    } else {
      return false;
    }
  }

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

}
  • The [MyPager] class extends the standard Android [ViewPager] class. We use the [MyPager] class instead of the [ViewPager] class solely because we want to disable swiping: by default, with the [ViewPager] class, you can switch between tabs by swiping (swiping left or right). Here, we do not want this behavior;
  • line 11: the boolean that controls swiping (lines 26 and 36);
  • lines 44–46: the method that initializes the field in line 11;

The skeleton of the Android activity [MainActivity] is as follows:


package android.aleas.activity;

import android.aleas.R;
import android.aleas.dao.AleasDaoResponse;
import android.aleas.dao.Dao;
import android.aleas.dao.IDao;
import android.aleas.fragments.MyFragment;
import android.aleas.fragments.Request;
import android.aleas.fragments.RequestFragment;
import android.os.Bundle;
import android.support.design.widget.TabLayout;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentPagerAdapter;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.Toolbar;
import android.view.View;
import android.widget.ArrayAdapter;
import android.widget.ProgressBar;
import rx.Observable;

public class MainActivity extends AppCompatActivity implements IDao {

  // [DAO] layer
  private IDao dao;
  // the session
  private Session session;

  // constructor
  public MainActivity() {
    // parent
    super();
    // session
    session = new Session();
    // DAO
    dao = new Dao();
  }


  // getters

  public Session getSession() {
    return session;
  }

  // IDao implementation ----------------------------------------
  @Override
  public Observable<AleasDaoResponse> getAleas(Request request) {
    return dao.getAleas(request);
  }

  @Override
  public void setWebServiceJsonUrl(String url) {
    dao.setUrlServiceWebJson(url);
  }

  @Override
  public void setClientTimeouts(int connectTimeout, int readTimeout) {
    dao.setClientTimeouts(connectTimeout, readTimeOut);
  }

}
  • Line 21: The [MainActivity] class extends the standard Android class [AppCompatActivity]. It is therefore a standard Android activity;
  • line 21: the [MainActivity] class implements the [IDao] interface;

Returning to the application architecture:

the fact that the activity implements the [DAO] layer interface allows the views to remain unaware of the [DAO] layer: their event handlers will communicate with the [Activity] layer when they need to interact with the server.

  • Line 24: a reference to the [DAO] layer initialized by the constructor on line 35;
  • line 26: a reference to the session shared by the fragments, initialized by the constructor on line 33;
  • lines 46–59: implementation of the [IDao] interface;

The [MainActivity] class initializes the components of its associated view as follows:


  // toolbar
  private Toolbar toolbar;
  // fragment manager
  private MyPager mViewPager;
  // tab container
  private TabLayout tabLayout;
  // loading image
  private ProgressBar loadingPanel;
...
  @Override
  public void onCreate(Bundle savedInstanceState) {
    // classic
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    // session
    session.setActivity(this);
    // configure [DAO] layer timeouts
    setClientTimeouts(Constants.CONNECT_TIMEOUT, Constants.READ_TIMEOUT);

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

    // toolbar
    setSupportActionBar(toolbar);

    // Initially, there is only one tab
    TabLayout.Tab tab = tabLayout.newTab();
    tab.setText("Request");
    tabLayout.addTab(tab);

    // event handler
    tabLayout.setOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {
      @Override
      public void onTabSelected(TabLayout.Tab tab) {
        // A tab has been selected - change the fragment displayed by the fragment container
        int position = tab.getPosition();
        if (position == 0) {
          // query tab
          showView(0);
        } else {
          // response tab - depends on the selected example
          showView(session.getExamplePosition());
        }
      }

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

      }

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

      }
    });

    // Create response fragments
    createResponseFragments();

    // manage loading image
    loadingPanel.setVisibility(View.INVISIBLE);
}

This code is fairly standard in an activity. Let’s explain a few points:

  • Line 19 references the following [Constants] class:

package android.aleas.activity;

abstract public class Constants {

  final static public int VIEW_REQUEST = 0;
  final static public int VIEW_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";
}
  • Lines 31–33: We create the first tab with the title [Request]. At some point, we will have the following in memory:
    • the [Request] fragment;
    • n fragments of type [ExampleXXFragment];

The first tab will always display the [Request] fragment. The second tab will display the [ExampleXXFragment] fragment corresponding to the example chosen by the user. The fragment displayed by the second tab therefore changes over time;

  • lines 37–48: the code executed when the user clicks on one of the tabs;
  • line 43: fragment #0 is displayed;
  • line 46: the fragment currently in use (displayed) is shown. Its number is retrieved from the session;
  • line 62: creates the fragments for all examples present in the example spinner in the [RequestFragment] view (1st tab);
  • line 65: the loading image is currently hidden;

To understand the [showView] method (lines 43, 46) and the [createResponseFragments] method, we must first introduce the in-memory fragment manager (class included in the MainActivity Java file):


  // the fragment manager - must define the getItem and getCount methods
  public class SectionsPagerAdapter extends FragmentPagerAdapter {

    // the managed fragments
    private MyFragment[] fragments;

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

    // must return the fragment at position
    @Override
    public MyFragment getItem(int position) {
      // the fragment
      return fragments[position];
    }

    // returns the number of fragments to manage
    @Override
    public int getCount() {
      // number of fragments
      return fragments.length;
    }
  }
}
  • The [SectionsPagerAdapter] class extends the Android [FragmentPagerAdapter] class. It overrides two methods of its parent class:
    • the [getItem] method, line 15;
    • the [getCount] method, line 22;
  • The [SectionsPagerAdapter] class contains all the application’s fragments. These are stored on line 5. Note that they are of type [MyFragment], as described in section 9.3.7.1;
  • line 8: to construct itself, the [SectionsPagerAdapter] class receives the fragments it must manage;
  • lines 14–18: the [getItem] method returns the fragment at position [position];
  • lines 21–25: the [getCount] method returns the total number of fragments;

The [createResponseFragments] method creates all the fragments needed by the application:


private void createResponseFragments() {
    // example spinner
    ArrayAdapter<CharSequence> adapter = ArrayAdapter.createFromResource(this, R.array.examples, android.R.layout.simple_spinner_item);
    // Specify the layout to use when the list of choices appears
    adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
    // Add the adapter to the session so that the [Request] view can retrieve it
    session.setSpinnerExamplesAdapter(adapter);
    ...
  }
  • Line 3: We create an adapter for the examples spinner, in this case a list of Strings representing the names of the examples. These names are present in the [layout/exemples.xml] file:
  

The [examples.xml] file contains the following code:


<!-- examples -->
<resources>
  <string-array name="examples">
    <item>Example-01</item>
    <item>Example-02</item>
    <item>Example-03</item>
    <item>Example-04</item>
  </string-array>
</resources>

Line 1: This file is the second parameter of the [createFromResource] method. In [R.array.examples], [examples] is the name of the array (see line 3 above), not the name of the file.

  • Line 5: We associate a layout (display manager) with the adapter. Now the adapter has both the data and its display mode;
  • Line 7: We add the adapter to the session. This is where the [RequestFragment] that needs it will retrieve it;

Let’s continue with the code for the [createResponseFragments] method:


private void createResponseFragments() {
    // example spinner
    ArrayAdapter<CharSequence> adapter = ArrayAdapter.createFromResource(this, R.array.examples, android.R.layout.simple_spinner_item);
    // Specify the layout to use when the list of choices appears
    adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
    // Add the adapter to the session so that the [Request] view can retrieve it
    session.setSpinnerExemplesAdapter(adapter);
    // Create the fragment array (1 request, n responses)
    MyFragment[] tFragments = new MyFragment[adapter.getCount() + 1];
    // Request fragment
    tFragments[0] = new RequestFragment();
    // response fragments
    for (int i = 1; i < tFragments.length; i++) {
      // construct the name of the fragment to instantiate corresponding to the example chosen by the user
      // this name must be the full name including its package—here it is directly associated with the example number in the spinner
      String exampleClassName = String.format("%s.Example%02dFragment", Constants.EXAMPLES_PACKAGE, i);
      // instantiate the fragment associated with the example
      MyFragment fragment;
      try {
        // instantiate the class
        fragment = (MyFragment) Class.forName(exampleClassName).getConstructors()[0].newInstance(new Object[]{});
      } catch (Exception e) {
        e.printStackTrace();
        return;
      }
      // the fragment has been created - we put it in the array
      tFragments[i] = fragment;
    }
    // instantiate the fragment manager with these new fragments
    mSectionsPagerAdapter = new SectionsPagerAdapter(getSupportFragmentManager(), tFragments);
    // Set up the ViewPager with the sections adapter.
    mViewPager.setAdapter(mSectionsPagerAdapter);
    // navigation between pages - this instruction is important
    // Here we specify that on either side of the displayed view, we must keep [tFragments.length] initialized views
    // This means that all fragments used by the application are in memory and initialized
    // If this is not done, the default [OffscreenPageLimit] is 1
    // so if the displayed fragment is No. 3, only fragments 2 and 4 will be initialized
    // This happens when the [onCreateView] method of these two fragments is called—which means that within this method, you must ensure that
    // Reconstruct the visual appearance the fragment had the last time it was used—also, this method must not
    // there should be no code that cannot be executed twice—this creates a massive mess and is difficult to manage
    // here we chose to avoid these difficulties—in the logs, we can see that when the app starts, all fragments are created
    // and their [onCreateView] method is executed—it is never executed again after that—
    mViewPager.setOffscreenPageLimit(tFragments.length);
    // we disable swiping between fragments
    mViewPager.setSwipeEnabled(false);
  }
  • line 9: creation of the array that will contain all the app's fragments;
  • line 11: the first fragment is the query fragment;
  • lines 13–28: we will create as many fragments as there are examples. These fragments all extend the response fragment [ResponseFragment] and implement only what is specific to the example: the creation of the observed values. These values differ from one example to another;
  • line 16: an example fragment has a standard name: ExampleXXFragment, where XX is its position in the example spinner plus 1. XX is also the fragment number of the example in the fragment manager;
  • line 21: instantiation of the fragment of example #i from the spinner:
    • Class.forName(exampleName): loads the fragment into memory;
    • Class.forName(exampleName).getConstructors()[0]: obtains a reference to the first constructor of the class. The ExampleXXFragment class has only one constructor. Therefore, a reference to this constructor will be obtained;
    • Class.forName(exampleName).getConstructors()[0].newInstance(new Object[]{}) instantiates an object of type ExampleXXFragment using the constructor from the previous step. new Object[]{} represents the parameters passed to this constructor. Since the ExampleXXFragment class constructor does not expect any parameters, an empty array of objects is passed;
  • line 27: this fragment is added to the fragment array;
  • line 30: we saw that the fragment manager’s constructor [SectionsPagerAdapter] expected the array of fragments it was to manage as a parameter. We now pass it to the constructor;
  • line 22: the fragment container [mViewPager] of the view associated with the activity [MainActivity] is linked here to the fragment manager: the fragment container [mViewPager] displays the fragments from the fragment manager;
  • line 43: read the comments—the instruction essentially states that all fragments must remain in the state the code puts them in, regardless of which fragment is currently displayed. So when we return to it, we find it in the state we left it in;
  • line 45: the fragment container [mViewPager] is of type [MyPager], which disables swiping;

The [MainActivity.showView] method is as follows:


  // display view #[position]
  private void showView(int position) {
    // refresh the fragment before displaying it
    mSectionsPagerAdapter.getItem(position).onRefresh();
    // display the requested view - go directly to the view (second parameter set to false)
    // without this parameter, we go to the desired view by default, quickly displaying the intermediate views—undesirable behavior
    mViewPager.setCurrentItem(position, false);
}
  • line 3: we want to display fragment #position;
  • line 4: this fragment is requested from the fragment manager and then refreshed. Since the last time it was displayed, the session may have changed. The fragment must therefore inspect the session to see if it needs to update;
  • line 7: the fragment is displayed by the [ViewPager]. Since this has been associated with the fragment manager, fragment #[position] will be displayed—the one we just refreshed on line 4;

Let’s finish with the two methods for managing the wait:


  public void beginWaiting() {
    // wait screen management
    loadingPanel.setVisibility(View.VISIBLE);
  }

  public void cancelWaiting() {
    // manage loading image
    loadingPanel.setVisibility(View.INVISIBLE);
    // End execution
    session.setOnAir(false);
    session.setOperationStarted(false);
}

9.3.7.5. The [RequestFragment] fragment

The [RequestFragment] class is as follows:


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 {

  // Web service URL
  private EditText edtUrlServiceRest;
  private TextView txtWebServiceErrorMessage;
  // number of requests
  private EditText edtNumberOfRequests;
  private TextView txtErrorRequests;
  // generation interval
  private EditText edtA;
  private EditText edtB;
  private TextView txtErrorInterval;
  // delay
  private EditText edtMinDelay;
  private EditText edtMaxDelay;
  private TextView txtErrorDelay;
  // number of generated values
  private EditText edtMinCount;
  private EditText edtMaxCount;
  private TextView txtErrorCount;
  // button
  private Button btnExecute;
  // list of answers
  private ListView listOfAnswers;
  private TextView responseInfo;
  // example spinner
  private Spinner spinnerExamples;

  // user input
  private int numRequests;
  private int a;
  private int b;
  private String webServiceJsonURL;
  private int minDelay;
  private int maxDelay;
  private int minCount;
  private int maxCount;

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

  @Override
  public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
    Log.d("rxjava", "RequestFragment onCreateView");
    // Get the activity and session
    activity = (MainActivity) getActivity();
    session = activity.getSession();
    // create the fragment view from its XML definition
    View view = inflater.inflate(R.layout.request, container, false);
    // components
    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);
    txtErrorInterval = (TextView) view.findViewById(R.id.txt_error_interval);
    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);
    btnExecute = (Button) view.findViewById(R.id.btn_Execute);
    listAnswers = (ListView) view.findViewById(R.id.lst_answers);
    infoAnswers = (TextView) view.findViewById(R.id.txt_Answers);
    spinnerExamples = (Spinner) view.findViewById(R.id.spinnerExamples);

    // [Execute] button
    btnExecute.setVisibility(View.VISIBLE);
    btnExecute.setOnClickListener(new View.OnClickListener() {
      public void onClick(View arg0) {
        doExecute();
      }
    });

    // initially, no error messages
    txtErrorRequests.setVisibility(View.INVISIBLE);
    txtErrorInterval.setVisibility(View.INVISIBLE);
    txtWebServiceErrorUrl.setVisibility(View.INVISIBLE);
    txtErrorCount.setVisibility(View.INVISIBLE);
    txtErrorDelay.setVisibility(View.INVISIBLE);
    // example spinner
    spinnerExamples.setAdapter(session.getSpinnerExamplesAdapter());
    // result
    return view;
  }
...
}
  • Line 16: The [RequestFragment] class extends the [MyFragment] class (see Section 9.3.7.1);
  • lines 18–42: the fragment’s visual components (see section 9.3.7.2);
  • lines 45–52: user input in the form;
  • the constructor (lines 55–58) and the [onCreateView] method are executed when the [MainActivity] activity creates all the fragments in the application. This happens only once;
  • line 61: the code for the [onCreateView] method is standard. Note on line 102 that the spinner adapter from the examples is retrieved from the session. Also note on line 91 that clicking the [Execute] button is handled by the [doExecute] method;
  • Lines 64–65: The [activity] and [session] fields belong to the parent class [MyFragment];

The [doExecute] method is as follows:


  // the inputs
  private int nbRequests;
  private int a;
  private int b;
  private String webServiceJsonURL;
  private int minDelay;
  private int maxDelay;
  private int minCount;
  private int maxCount;

...

  private void execute() {
    // Are the inputs valid?
    if (isPageValid()) {
      // store the info in the session
      session.setInfos(nbRequests, a, b, minCount, maxCount, minDelay, maxDelay, urlServiceWebJson, spinnerExemples.getSelectedItem().toString(), spinnerExemples.getSelectedItemPosition() + 1);
      // store the web service URL
      activity.setUrlServiceWebJson(session.getUrlWebJson());
      Log.d("rxjava", String.format("RequestFragment doExecuter, session=%s, session.position=%s%n", session, session.getExamplePosition()));
      // action in progress
      session.setOnAir(true);
      // but not yet started
      session.setOperationStarted(false);
      // display the response fragment
      activity.selectTab(Constants.VUE_RESPONSE);
      // Start waiting
      beginWaiting();
    }
}
  • line 15: We will not comment on the [ispageValid] method. It checks the validity of the entries and returns true only if they are all valid. In this case, they are used to initialize the fields in lines 2–9;
  • Line 17: The various entries are saved to the session:
    • [spinnerExemples.getSelectedItem().toString()] is the name of the example selected by the user and is stored in [session.exampleName];
    • [spinnerExemples.getSelectedItemPosition() + 1] is the fragment ID associated with the example, which has been stored (the fragment) by the fragment manager. This ID is stored in [session.examplePosition];
  • line 19: the URL of the web service / JSON is passed to the activity, which in turn passes it to the [DAO] layer;
  • lines 21–24: note that an operation is about to start;
  • line 26: the response tab will be displayed. To understand what will happen, recall the code [MainActivity.selectTab]:

  // Select a tab
  public void selectTab(int position) {
    // there are at most 2 tabs
    // initially, there is only one, the one from the request
    // if the requested tab is #1 and it doesn't exist yet, then it must be created
    if (position == 1 && tabLayout.getTabCount() == 1) {
      // Add 1 tab
      TabLayout.Tab tab = tabLayout.newTab();
      tab.setText("Response");
      tabLayout.addTab(tab);
    }
    // We programmatically select the tab, which will trigger the [onTabSelected] event
    // which will associate the correct view with this tab
    tabLayout.getTabAt(position).select();
}
  • Initially, the activity had only created the request tab (tab #0);
  • lines 6–11: create the response tab (tab #1) if it hasn’t been created yet;
  • line 14: we select tab no. position (0 or 1). This places the [onTabSelected] event in the queue of the Android app’s event loop;

The handler for the [onTabSelected] event in [MainActivity] is as follows:


      @Override
      public void onTabSelected(TabLayout.Tab tab) {
        // A tab has been selected - change the fragment displayed by the fragment container
        int position = tab.getPosition();
        if (position == 0) {
          // query tab
          showView(0);
        } else {
          // response tab - depends on the selected example
          showView(session.getExamplePosition());
        }
}

In the case of the [Response] tab, line 9 is executed. The fragment with ID [session.getExamplePosition()] will be displayed. For example, for example [example-03], the ID stored in [session.examplePosition] is 3. Line 10 then displays fragment ID 3. The array of fragments initially created by the activity is [RequestFragment, Example01Fragment, Example02Fragment, Example03Fragment,..]. Therefore, it is indeed the [Example03Fragment] that will be displayed. It is displayed by the following code:


  // display view #[position]
  private void showView(int position) {
    // refresh the fragment before displaying it
    mSectionsPagerAdapter.getItem(position).onRefresh();
    // display the requested view - go directly to the view (second parameter set to false)
    // without this parameter, we go to the desired view by default, quickly displaying the intermediate views—undesirable behavior
    mViewPager.setCurrentItem(position, false);
}

We can see that the fragment will be refreshed (line 4) before being displayed (line 7).

9.3.7.6. The [ResponseFragment] fragment

The [ResponseFragment] class displays responses from the server. Its code is as follows:


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 {

  // list of responses
  private ListView listOfResponses;
  private TextView responseInfo;
  // button
  private Button cancelButton;

  // JSON mapper
  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) {
    // retrieve the activity and session
    activity = (MainActivity) getActivity();
    session = activity.getSession();
    Log.d("rxjava", String.format("ResponseFragment (%s) onCreateView%n", this));
    // create the fragment view from its XML definition
    View view = inflater.inflate(R.layout.response, container, false);
    // components
    listResponses = (ListView) view.findViewById(R.id.lst_responses);
    infoReponses = (TextView) view.findViewById(R.id.txt_Reponses);
    btnCancel = (Button) view.findViewById(R.id.btn_Cancel);
    // [Cancel] button
    btnCancel.setVisibility(View.INVISIBLE);
    btnCancel.setOnClickListener(new View.OnClickListener() {
      public void onClick(View arg0) {
        doCancel();
      }
    });
    // result
    return view;
  }
...
  // method to be executed (by explicit code) before each view of the fragment
  public void onRefresh() {
...
  }
}
  • line 21: the [ResponseFragment] class extends the [MyFragment] class;
  • lines 23–27: the fragment’s components;
  • lines 32–36: the constructor is executed only once, during the initial creation of the example fragments by the activity. This is because all the example fragments extend the [ResponseFragment] fragment. When they are instantiated, the constructor of their parent class [ResponseFragment] is called;
  • line 35: initializes the JSON mapper from line 30 used to display the JSON string of an exception stack;
  • lines 38–59: the [onCreateView] method is executed only once, during the initial creation of the example fragments by the activity. It contains standard code found in an Android application;
  • lines 52–56: the method executed when the [Cancel] button is clicked is the [doCancel] method;
  • lines 62–64: the [onRefresh] method is executed every time the [Response] tab is displayed;

Thanks to the various logs placed in key methods, we can see what happens when the app starts:

05-17 08:45:05.803 14158-14158/android.aleas D/rxjava: RequestFragment constructor
05-17 08:45:05.804 14158-14158/android.aleas D/rxjava: ResponseFragment (Example01Fragment{c6fd1a7}) constructor
05-17 08:45:05.804 14158-14158/android.aleas D/rxjava: Example01Fragment constructor
05-17 08:45:05.810 14158-14158/android.aleas D/rxjava: ResponseFragment (Example02Fragment{ba75654}) constructor
05-17 08:45:05.810 14158-14158/android.aleas D/rxjava: Example02Fragment constructor
05-17 08:45:05.810 14158-14158/android.aleas D/rxjava: ResponseFragment (Example03Fragment{b8589fd}) constructor
05-17 08:45:05.810 14158-14158/android.aleas D/rxjava: Example03Fragment constructor
05-17 08:45:05.810 14158-14158/android.aleas D/rxjava: ResponseFragment (Example04Fragment{e9506f2}) constructor
05-17 08:45:05.810 14158-14158/android.aleas D/rxjava: Example04Fragment constructor
05-17 08:45:05.934 14158-14158/android.aleas D/rxjava: RequestFragment onCreateView
05-17 08:45:05.962 14158-14158/android.aleas D/rxjava: ResponseFragment (Example01Fragment{c6fd1a7 #1 id=0x7f0d006e android:switcher:2131558510:1}) onCreateView
05-17 08:45:05.969 14158-14158/android.aleas D/rxjava: ResponseFragment (Example02Fragment{ba75654 #2 id=0x7f0d006e android:switcher:2131558510:2}) onCreateView
05-17 08:45:05.972 14158-14158/android.aleas D/rxjava: ResponseFragment (Example03Fragment{b8589fd #3 id=0x7f0d006e android:switcher:2131558510:3}) onCreateView
05-17 08:45:05.978 14158-14158/android.aleas D/rxjava: ResponseFragment (Example04Fragment{e9506f2 #4 id=0x7f0d006e android:switcher:2131558510:4}) onCreateView
  • line 1: construction of the fragment [RequestFragment];
  • lines 2–9: construction of the fragments for the 4 examples in the application;
  • line 10: initialization of the [RequestFragment] fragment;
  • lines 11–14: initialization of the fragments for the 4 examples in the application;

After that, we never see calls to these methods again.

The [ResponseFragment.onRefresh] method is as follows:


  // method to be executed (explicitly) before each fragment is rendered
  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()));
    // Is execution in progress?
    if (session.isOnAir() && !session.isOperationStarted()) {
      // Execute request
      session.setOperationStarted(true);
      doExecute();
    }
}
  • Line 5: We check if the [RequestFragment] has made a request (session.isOnAir) and if it has started (isOperationStarted). If the [RequestFragment] has made a request and it is not already running, the operation is launched (lines 7–8);
  • once the operation is launched, since it is asynchronous, the user can navigate between the two tabs. If the user navigates back to the [Response] tab and an operation is in progress, then lines 7–8 are not executed;

The [doExecute] method on line 8 executes the operation requested by the user:


  private void doExecute() {
    Log.d("rxjava", String.format("ResponseFragment (%s) doExecuter for %s%n", this, session.getExampleName()));
    // start waiting
    beginWaiting();
    // prepare execution
    subscriptions.clear();
    responses.clear();
    nbInfos = 0;
    // Create and execute the observables from the selected example
    createAndExecuteObservables();
}

// method implemented by child classes
protected abstract void createAndExecuteObservables();
  • line 10: creates, executes, and observes observables. These are different for each example. That is why the [createAndExecuteObservables] method is abstract (line 14). It will be implemented by the [ExampleXXFragment] fragments that extend the [ResponseFragment] class;
  • line 6: the list of subscriptions is cleared;
  • line 7: the list displaying the responses is cleared;
  • line 8: counts the number of responses received;

The child classes [ExampleXXFragment] entrust the following [showAlea] method with the task of displaying the elements they observe:


  protected void showAlea(String data) {
    // one more piece of information
    nbInfos++;
    infoReponses.setText(String.format("List of answers (%s)", nbInfos));
    // 1 more answer
    answers.add(0, data);
    Log.d("rxjava", data);
    // Update the UI
    listAnswers.setAdapter(new ArrayAdapter<String>(getActivity(), android.R.layout.simple_list_item_1, android.R.id.text1, answers));
}
  • Line 1: We see that the observed element arrives as a string. This is actually the JSON string of the observed element. This allows us to have a single method for displaying the observed element regardless of its exact Java type;
  • line 6: the observed [data] element is added to the first position of the list of responses. The user therefore sees the most recent responses at the top of the list;

Waiting is managed by the following [beginWaiting] and [cancelWaiting] methods:


  private void beginWaiting() {
    // start the timer
    activity.beginWaiting();
    // the [Cancel] button is displayed
    btnCancel.setVisibility(View.VISIBLE);
  }

  protected void cancelWaiting() {
    // End of wait
    activity.cancelWaiting();
    // the [Cancel] button is hidden
    btnCancel.setVisibility(View.INVISIBLE);
}

They call the methods of the same names in the activity and simply show or hide the [Cancel] button.

Clicking the [Cancel] button is handled by the following code:


  protected void doCancel() {
    // cancel all subscriptions
    for (Subscription s : subscriptions) {
      if (!s.isUnsubscribed()) {
        s.unsubscribe();
      }
    }
    // end of wait
    cancelWaiting();
}
  • lines 3–7: cancel all subscriptions one by one;

9.3.8. Examples of observables

9.3.8.1. Example-01

The [ExampleXXFragment] classes are designed to create, execute, and observe observables. The observed values are displayed by the parent class [ResponseFragment].

The [Example01Fragment] class is as follows:

  

package android.aleas.examples;

import android.aleas.dao.AleasDaoResponse;
import android.aleas.fragments.AleasUiResponse;
import android.aleas.fragments.Request;
import android.aleas.fragments.ResponseFragment;
import android.util.Log;
import org.codehaus.jackson.map.ObjectMapper;
import org.codehaus.jackson.map.ser.impl.SimpleBeanPropertyFilter;
import org.codehaus.jackson.map.ser.impl.SimpleFilterProvider;
import rx.Observable;
import rx.android.schedulers.AndroidSchedulers;
import rx.functions.Action0;
import rx.functions.Action1;
import rx.schedulers.Schedulers;

import java.io.IOException;

public class Example01Fragment extends ResponseFragment {

    // JSON mappers
    private ObjectMapper mapperAleasUiResponse;

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

    @Override
    public void createAndExecuteObservables() {
        Log.d("rxjava", "Example01Fragment createAndExecuteObservables");
        // request random numbers
        Observable<AleasDaoResponse> observable = Observable.empty();
        for (int i = 0; i < session.getNbRequests(); i++) {
            // configure observable #i
            // request to be sent to the server
            Request request = session.getRequest();
            request.setId(i);
            // observable executed on the computation thread
            observable = observable.mergeWith(session.getActivity().getAleas(request).subscribeOn(Schedulers.io()));
        }
        // Observe on the event loop thread;
        observable = observable.observeOn(AndroidSchedulers.mainThread());
        // execute all these observables
        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) {
        // extract the data to be displayed
        String data;
        try {
            data = mapperAleasUiResponse.writeValueAsString(new AleasUiResponse(aleasDaoResponse));
        } catch (IOException e) {
            data = String.format("[%s,%s]", e.getClass().getName(), e.getMessage());
        }
        return data;
    }
}
  • line 36: the single observable that will be generated;
  • lines 37–44: generation and configuration of the various observables that are merged (line 43) into the observable on line 36;
  • line 43: the observable is executed in a thread of the scheduler [Schedulers.io()]. The HTTP request to the server will be executed in this thread;
  • line 46: the final observable is observed on the event loop thread;
  • lines 48–57: Execution of observables, i.e., requests to the random number server. Android does not yet support Java 8 and its lambdas. Therefore, anonymous classes are used here to instantiate RxJava’s functional interfaces;
  • lines 49–52: action executed when the observer receives a new element of type [AleasDaoResponse] from the observable (see section 9.3.6.1);
  • line 51: call to the [showAlea] method of the parent class. Recall that it expects a string. This is provided by the [getDataFrom] method in lines 59–68;
  • line 63: we return the JSON string of type [AleasUiResponse] as follows:

package android.aleas.fragments;

import android.aleas.dao.AleasDaoResponse;

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

public class AleasUiResponse {

  // [DAO] response
  private AleasDaoResponse aleasDaoResponse;
  // observation thread
  private String observedOn;
  // observation time
  private String observedAt;

  // constructors
  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;
  }
// getters and setters
...
}
  • To the [DAO] layer response (line 11), we add two pieces of information:
    • line 13: the observation thread;
    • line 15: the observation time;

Let's go back to the subscription code:


    @Override
    public void createAndExecuteObservables() {
...
        // We execute all these observables
        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) {
                // display the exception
                showAlea(getMessagesFromThrowable(th));
                // After receiving an exception, the observable receives neither onNext nor onCompleted
                // must cancel the subscription manually
                doAnnul();
            }
        }, new Action0() {
            @Override
            public void call() {
                // end wait
                cancelWaiting();
            }
        }));
}
  • lines 11–18: case where the observer receives an exception;
  • line 14: we use the [showAlea] method of the parent class again to display the exception. The [getMessagesFromThrowable] method is a method of the parent class [ResponseFragment] that generates a string from an exception:

  // messages from an exception
  protected String getMessagesFromThrowable(Throwable ex) {
    // create a list with the error messages from the exception stack
    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();
    }
}
  • line 11: returns the JSON string of a list of error messages (line 4);

Let’s return to the observable subscription code:

  • lines 19–25: the code executed when the observer receives the end-of-emission notification. We then cancel the wait (line 23), which updates the GUI;

Running Example 01 produces an output similar to the following:

Image

Each element in the list is the JSON string of an observed value. The fields of the JSON string are as follows:

  • aleas: the list of random numbers provided by the server;
  • idClient: the request number (you can see that the responses came back in a scattered order);
  • on: the execution thread of the observable that emitted this value;
  • requestAt: time of the client request;
  • responseAt: time of the server response;
  • delay: delay observed by the server;
  • error: error code returned by the server (0=no error);
  • message: error message returned by the server (null=no error);
  • observedAt: time the observed value was observed;
  • observedOn: thread observing the observed value;

9.3.8.2. Example-02

The [Example02Fragment] class is as follows:


package android.aleas.examples;

import android.aleas.dao.AleasDaoResponse;
import android.aleas.fragments.AleasUiResponse;
import android.aleas.fragments.Request;
import android.aleas.fragments.ResponseFragment;
import android.util.Log;
import org.codehaus.jackson.map.ObjectMapper;
import rx.Observable;
import rx.android.schedulers.AndroidSchedulers;
import rx.functions.Action0;
import rx.functions.Action1;
import rx.functions.Func1;
import rx.schedulers.Schedulers;

import java.io.IOException;

public class Example02Fragment extends ResponseFragment {

    // JSON mappers
    private ObjectMapper mapperAleasUiResponse;

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

    public void createAndExecuteObservables() {
        Log.d("rxjava", "Example02Fragment createAndExecuteObservables");
        // request random numbers
        Observable<AleasDaoResponse> observable = Observable.empty();
        for (int i = 0; i < session.getNbRequests(); i++) {
            // prepare request
            Request request = session.getRequest();
            request.setId(i);
            // only keep observables with an even client number
            observable = observable
                    .mergeWith(session.getActivity().getAleas(request).filter(new Func1<AleasDaoResponse, Boolean>() {
                        @Override
                        public Boolean call(AleasDaoResponse aleasDaoResponse) {
                            return aleasDaoResponse.getClientState().getIdClient() % 2 == 0;
                        }
                    })
                            // execution on I/O thread
                            .subscribeOn(Schedulers.io()));
        }
        // Observe on the event loop thread
        observable = observable.observeOn(AndroidSchedulers.mainThread());
        // execute these observables
        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));
                doAnnul();
            }
        }, new Action0() {
            @Override
            public void call() {
                // end wait
                cancelWaiting();
            }
        }));

    }

    private String getDataFrom(AleasDaoResponse aleasDaoResponse) {
        // extract the data to be displayed
        String data;
        try {
            data = mapperAleasUiResponse.writeValueAsString(new AleasUiResponse(aleasDaoResponse));
        } catch (IOException e) {
            data = String.format("[%s,%s]", e.getClass().getName(), e.getMessage());
        }
        return data;
    }

}

This example is similar to the previous one (line 38). However, from the observables obtained in the previous example, we keep only those with an even customer number (lines 42–46), using the [filter] method (line 41).

The results obtained are as follows (for 10 requests):

Image

9.3.8.3. Example-03

The [Example03Fragment] class is as follows:


package android.aleas.examples;

import android.aleas.dao.AleasDaoResponse;
import android.aleas.fragments.Request;
import android.aleas.fragments.ResponseFragment;
import android.util.Log;
import org.codehaus.jackson.map.ObjectMapper;
import rx.Observable;
import rx.android.schedulers.AndroidSchedulers;
import rx.functions.Action0;
import rx.functions.Action1;
import rx.functions.Func1;
import rx.schedulers.Schedulers;

import java.io.IOException;
import java.util.List;

public class Example03Fragment extends ResponseFragment {

  // JSON mappers
  private ObjectMapper mapper;

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

  public void createAndExecuteObservables() {
    Log.d("rxjava", "Example03Fragment createAndExecuteObservables");
    // request random numbers
    Observable<List<Integer>> observable = Observable.empty();
    for (int i = 0; i < session.getNbRequests(); i++) {
      // prepare request
      Request request = session.getRequest();
      request.setId(i);
      // observable configuration
      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();
        }
      })
        // Execute on I/O thread
        .subscribeOn(Schedulers.io()));
    }
    // Observation on the event loop thread
    observable = observable.observeOn(AndroidSchedulers.mainThread());
    // execute these observables
    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));
            doAnnul();
          }
        },
        new Action0() {
          @Override
          public void call() {
            // end wait
            cancelWaiting();
          }
        }
      ));

  }

  private String getDataFrom(List<Integer> aleas) {
    // retrieve the data to be displayed
    String data;
    try {
      data = mapper.writeValueAsString(aleas);
    } catch (IOException e) {
      data = String.format("[%s,%s]", e.getClass().getName(), e.getMessage());
    }
    return data;
  }

}

This example is similar to Example-02:

  • line 40: we define the same observables as in Example-02;
  • line 45: each value emitted by the previous observables is transformed, using the [map] method, into a List<Integer> type, which is the list of random numbers generated by the server;
  • line 58: the observed value is now of type List<Integer>;

The result obtained for 10 requests is as follows:

Image

9.3.8.4. Example-04

The [Example04Fragment] class is as follows:


package android.aleas.examples;

import android.aleas.dao.AleasDaoResponse;
import android.aleas.fragments.Request;
import android.aleas.fragments.ResponseFragment;
import android.util.Log;
import org.codehaus.jackson.map.ObjectMapper;
import rx.Observable;
import rx.android.schedulers.AndroidSchedulers;
import rx.functions.Action0;
import rx.functions.Action1;
import rx.functions.Func1;
import rx.schedulers.Schedulers;

public class Example04Fragment extends ResponseFragment {

  // JSON mappers
  private ObjectMapper mapper;

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

  public void createAndExecuteObservables() {
    Log.d("rxjava", "Example03Fragment createAndExecuteObservables");
    // request random numbers
    Observable<Integer> observable = Observable.empty();
    for (int i = 0; i < session.getNbRequests(); i++) {
      // prepare request
      Request request = session.getRequest();
      request.setId(i);
      // configure observables
      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());
        }
      })
        // Execute on an I/O thread
        .subscribeOn(Schedulers.io()));
    }
    // Observation on the event loop thread
    observable = observable.observeOn(AndroidSchedulers.mainThread());
    // execute these observables
    subscriptions.add(observable
      .subscribe(new Action1<Integer>() {
                   @Override
                   public void call(Integer alea) {
                     showRandom(String.valueOf(random));
                   }
                 },
        new Action1<Throwable>() {
          @Override
          public void call(Throwable th) {
            showAlea(getMessagesFromThrowable(th));
            doAnnul();
          }
        },
        new Action0() {
          @Override
          public void call() {
            // end wait
            cancelWaiting();
          }
        }
      ));

  }
}

This example is similar to Example-03, except that instead of using the [map] method on line 42, we use the [flatMap] method.

  • line 55: note that the type of the observed value is now Integer;

For 10 requests, we obtain the following results:

Image

This time, there are more observed values than requests.

9.3.8.5. Example-05

We will now outline the procedure for adding a new example of observables to the application.

Suppose we want to reproduce the example [Example22h] from Section 7.6.4:


package dvp.rxjava.observables.examples;

import dvp.rxjava.observables.utils.Process;
import dvp.rxjava.observables.utils.ProcessUtils;
import rx.Observable;
import rx.observables.GroupedObservable;

public class Example22h {
    public static void main(String[] args) throws InterruptedException {
        // process
        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()));
        // subscriptions
        ProcessUtils.subscribe(1, process);
    }
}
  • The values of the observable [Observable.range(1, 10)] are first grouped into even and odd values by the [groupBy] method (line 11) and then combined into a single observable by the [concatMap] method (line 12);

Step 1

We create a new example in the file [examples.xml]:

  

<!-- examples -->
<resources>
  <string-array name="examples">
    <item>Example-01</item>
    <item>Example-02</item>
    <item>Example-03</item>
    <item>Example-04</item>
    <item>Example-05</item>
  </string-array>
</resources>

Above, line 8 has been added. The name given to the example can be anything.

Step 2

Duplicate the [Example04Fragment] class into [Example05Fragment]. Here, the name is fixed.

Step 3

Modify the code in [Example05Fragment] as follows:


package android.aleas.examples;

import android.aleas.dao.AleasDaoResponse;
import android.aleas.fragments.Request;
import android.aleas.fragments.ResponseFragment;
import android.util.Log;
import org.codehaus.jackson.map.ObjectMapper;
import rx.Observable;
import rx.android.schedulers.AndroidSchedulers;
import rx.functions.Action0;
import rx.functions.Action1;
import rx.functions.Func1;
import rx.observables.GroupedObservable;
import rx.schedulers.Schedulers;

public class Example05Fragment extends ResponseFragment {

  // JSON mappers
  private ObjectMapper mapper;

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

  public void createAndExecuteObservables() {
    Log.d("rxjava", "Example05Fragment createAndExecuteObservables");
    // Instantiation of functional interfaces
    // filter
    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();
      }
    };
    // request random numbers
    Observable<Integer> observable = Observable.empty();
    for (int i = 0; i < session.getNbRequests(); i++) {
      // prepare request
      Request request = session.getRequest();
      request.setId(i);
      // observable configuration
      observable = observable.mergeWith(session.getActivity().getAleas(request).filter(filter).flatMap(flatMap))
        .groupBy(groupBy).concatMap(concatMap)
        // execute on an I/O thread
        .subscribeOn(Schedulers.io());
    }
    // Observe on the event loop thread
    observable = observable.observeOn(AndroidSchedulers.mainThread());
    // execute these observables
    subscriptions.add(observable
      .subscribe(new Action1<Integer>() {
                   @Override
                   public void call(Integer alea) {
                     showRandom(String.valueOf(random));
                   }
                 },
        new Action1<Throwable>() {
          @Override
          public void call(Throwable th) {
            showAlea(getMessagesFromThrowable(th));
            doAnnul();
          }
        },
        new Action0() {
          @Override
          public void call() {
            // end wait
            cancelWaiting();
          }
        }
      ));

  }
}
  • line 67: represents the observable from Example 04: a stream of integers;
  • line 68: we group this stream of integers according to a Boolean criterion that we will define. We will obtain an observable of type Observable<GroupedObservable<Boolean, Integer>>, which therefore emits elements of type GroupedObservable<Boolean, Integer>;
  • line 68: the [concatMap] method will produce elements of type Integer from elements of type GroupedObservable<Boolean, Integer>;
  • lines 32–59: to make the creation of the observable in lines 67–69 more readable, we have isolated the instances of functional interfaces required by the various operators [filter, flatMap, groupBy, concatMap];
  • lines 47–52: The [groupBy] method expects a parameter of type Func1<T,K>, where T is the type of the grouped elements and K is the type of the grouping criterion. Given an element T, the Func1<T,K> instance is responsible for producing the grouping key K for that element;
  • lines 48–51: Elements of type Integer will be grouped by parity. The Func1<Integer,Boolean> instance produces the key true or false depending on whether the element should be placed in one group or the other. The result is two groups: the group of even elements with key true and the group of odd elements with key false;
  • lines 53–59: the [concatMap] method expects a parameter of type Func1<T, Observable<R>> and produces an observable of elements of type R. The type T here is the type emitted by the [groupBy] operator, in this case a GroupedObservable<Boolean, Integer>;
  • line 57: from the element of type [GroupedObservable<Boolean, Integer>], we produce a type Observable<Integer>. Since the [groupBy] operator produced two groups, the [concatMap] operator will produce two observables of type [Observable<Integer>]. Like [flatMap], it will flatten them into a single observable. But unlike [flatMap], it does not mix the elements of the flattened observables. We should therefore observe two separate groups: the even random numbers and the odd ones.

Step 4

We run the application:

Image

and we get the following results:

Image

  • in [1], the even random numbers; in [2], the odd ones;

9.3.8.6. To continue

The reader is now invited to create their own examples and also to experiment with various values for the inputs in the form that configures the requests sent to the random number server.

9.3.9. Conclusion

We have created the following architecture in the Android environment:

The Android client:

The [DAO] layer communicates with the server that generates the random numbers displayed by the Android tablet. This server has the following two-layer architecture:

The [DAO] layer made n HTTP requests to the random number server, and the [swing] layer asynchronously awaited the results of these requests to display them. These n HTTP requests were made to the same server, which returned the same types of responses. This allowed us to merge the responses into a single observable.

In reality, Android applications communicate with different servers, and we will likely not merge their responses. HTTP requests to these servers will be handled independently of one another, and their results will be observed using separate methods.