Skip to content

9. Android 环境中的 RxJava

9.1. 简介

在此,我们将重新审视一个已在多篇文档中讨论过的应用程序:

  1. [面向 JEE 开发者的 Android:Android 客户端的异步模型](第 4 章);
  2. [通过示例入门 Android 平板编程](第 9 章);
  3. [通过实例学习Android平板编程 - 第2版](第1.11节);

该内容涉及一个客户端/服务器应用程序,其中服务器异步发送随机数,而 Android 客户端负责显示这些随机数:

  • 在文档 1 中,Android 客户端使用了非标准技术;
  • 在文档2中,Android客户端使用Android的标准技术进行异步操作;
  • 在文档3中,Android客户端采用了与文档2相同的技术,但通过使用Android Annotations库中的注解进行了简化;

Android客户端代码如下:

[DAO] 层与生成 Android 平板电脑上显示的随机数的服务器进行通信。该服务器采用以下两层架构:

客户端向 [Web / JSON] 层中的特定 URL 发起请求,并收到以 JSON(JavaScript 对象表示法)格式返回的文本响应。

我们将把应用程序的分析分为两个步骤:

Web/JSON 服务器

  • 其 [业务] 层;
  • 其基于 Spring MVC 实现的 [Web / JSON] 服务;

Android客户端

  • 其 [DAO] 层;
  • 其 Activity;
  • 其视图;

9.2. Web 服务 / JSON

:Web 服务 / JSON 是使用 Spring MVC 技术实现的。不熟悉该技术的读者可以:

  • 直接阅读第 9.2.1 节,该节说明了如何启动服务器以及如何进行查询;
  • 查阅文档《Spring MVC 和 Thymeleaf 实例》,特别是第 4 章,其中介绍了代码中使用的主要注解;

9.2.1. IntelliJ IDEA 项目

Web 服务 / JSON 具有以下架构:

该架构由以下 IntelliJ IDEA 项目 [1] 实现:

通过 [2-3] 启动服务器。随后将显示控制台日志:

2016-05-17 10:47:12.642  INFO 13116 --- [           main] dvp.rxjava.server.boot.Application       : Starting Application on st-PC with PID 13116 (D:\data\istia-1516\projets\rxjava\dvp\android\serveur\build\classes\main started by st in D:\data\istia-1516\projets\rxjava\dvp\android\serveur)
2016-05-17 10:47:12.647  INFO 13116 --- [           main] dvp.rxjava.server.boot.Application       : No active profile set, falling back to default profiles: default
2016-05-17 10:47:12.706  INFO 13116 --- [           main] ationConfigEmbeddedWebApplicationContext : Refreshing org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext@71623278: startup date [Tue May 17 10:47:12 CEST 2016]; root of context hierarchy
2016-05-17 10:47:13.736  INFO 13116 --- [           main] s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat initialized with port(s): 8080 (http)
2016-05-17 10:47:13.749  INFO 13116 --- [           main] o.apache.catalina.core.StandardService   : Starting service Tomcat
2016-05-17 10:47:13.750  INFO 13116 --- [           main] org.apache.catalina.core.StandardEngine  : Starting Servlet Engine: Apache Tomcat/8.0.33
2016-05-17 10:47:13.914  INFO 13116 --- [ost-startStop-1] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring embedded WebApplicationContext
2016-05-17 10:47:13.914  INFO 13116 --- [ost-startStop-1] o.s.web.context.ContextLoader            : Root WebApplicationContext: initialization completed in 1214 ms
2016-05-17 10:47:13.965  INFO 13116 --- [ost-startStop-1] o.s.b.c.e.ServletRegistrationBean        : Mapping servlet: 'dispatcherServlet' to [/*]
2016-05-17 10:47:14.251  INFO 13116 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/{a}/{b}/{minCount}/{maxCount}/{minDelay}/{maxDelay}],methods=[GET],produces=[application/json]}" onto public java.lang.String dvp.rxjava.server.web.AleasController.getAleas(int,int,int,int,int,int) throws com.fasterxml.jackson.core.JsonProcessingException
2016-05-17 10:47:14.342  INFO 13116 --- [           main] s.w.s.m.m.a.RequestMappingHandlerAdapter : Looking for @ControllerAdvice: org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext@71623278: startup date [Tue May 17 10:47:12 CEST 2016]; root of context hierarchy
2016-05-17 10:47:14.485  INFO 13116 --- [           main] s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat started on port(s): 8080 (http)
2016-05-17 10:47:14.489  INFO 13116 --- [           main] dvp.rxjava.server.boot.Application       : Started Application in 2.289 seconds (JVM running for 2.859)
2016-05-17 10:48:37.061  INFO 13116 --- [nio-8080-exec-2] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring FrameworkServlet 'dispatcherServlet'
2016-05-17 10:48:37.061  INFO 13116 --- [nio-8080-exec-2] o.s.web.servlet.DispatcherServlet        : FrameworkServlet 'dispatcherServlet': initialization started
2016-05-17 10:48:37.087  INFO 13116 --- [nio-8080-exec-2] o.s.web.servlet.DispatcherServlet        : FrameworkServlet 'dispatcherServlet': initialization completed in 26 ms
  • 第 12 行:表示该服务可在端口 8080 上访问;
  • 第 10 行:Web 服务 / JSON 的唯一 URL,可通过 HTTP GET 操作访问。其参数如下:
    • [a,b]:生成随机数的范围;
    • [minCount, maxCount]:生成的随机数个数,其中 count 是 [minCount, maxCount] 区间内的随机数;
    • [minDelay, maxDelay]:服务在返回请求的数字前等待 delay 毫秒,其中 delay 是 [minDelay, maxDelay] 区间内的一个随机数;

在浏览器中,让我们请求此 URL:

 

我们请求:

  • 区间 [100, 200] 内的随机数;
  • n 个随机数,其中 n 属于区间 [10, 20];
  • 等待时间为 x 毫秒,其中 x 属于区间 [300, 400];

在响应中:

  • aleas:生成的随机数列表;
  • delay:服务器设定的等待时间(单位为毫秒);
  • error:错误代码——若无错误则为 0;
  • message:错误消息——若无错误则为 null

9.2.2. 该项目的 Gradle 依赖项

  

[server] 项目是一个由以下 [build.gradle] 文件 [1] 配置的 Gradle 项目:


// généré par http://start.spring.io/ (mai 2016)
buildscript {
  ext {
    springBootVersion = '1.3.5.RELEASE'
  }
  repositories {
    mavenCentral()
  }
  dependencies {
    classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
  }
}
 
apply plugin: 'java'
apply plugin: 'spring-boot'
 
jar {
  baseName = 'serveur'
  version = '0.0.1-SNAPSHOT'
}
 
sourceCompatibility = 1.8
targetCompatibility = 1.8
 
repositories {
  mavenCentral()
}
 
dependencies {
  compile('org.springframework.boot:spring-boot-starter-web')
}
  • 第 1 行:一条说明此配置文件生成方式的注释;
  • 第 4 行和第 10 行:对 [Spring Boot] 框架的依赖,该框架是 Spring 生态系统的一个分支。这个 [http://projects.spring.io/spring-boot/] 框架允许进行最简化的 Spring 配置。 基于项目类路径中存在的库,[Spring Boot] 会推断出一个合理或可能的项目配置。因此,如果 Hibernate 位于项目的类路径中,那么 [Spring Boot] 就会推断出所使用的 JPA 实现是 Hibernate,并据此配置 Spring。 开发人员不再需要手动进行这些操作。他们只需配置 [Spring Boot] 默认未配置的设置,或者那些 [Spring Boot] 默认已配置但需要明确指定的设置。无论哪种情况,开发人员所做的配置都具有优先级;
  • 第 14–15 行:两个用于调用此 Gradle 文件内容的 Gradle 插件;
  • 第 17–20 行:定义为本项目生成的归档文件的特性;
  • 第 22–23 行:用于兼容 Java 8;
  • 第 25–27 行:将在全局 Maven 仓库或本机上的本地仓库中搜索依赖项;
  • 第 30 行:定义了对 [spring-boot-starter-web] 工件的依赖。该工件包含 Spring MVC 项目所需的所有归档文件,其中包括 Tomcat 服务器归档文件。该文件将用于部署 Web 应用程序。请注意,此处未指定依赖项的版本,将使用导入的 [spring-boot] 项目中指定的版本;

要更新项目,必须强制下载依赖项 [1-3]:

让我们来看看[4]这个[build.gradle]文件中包含的依赖项:

 

这类依赖有很多。Spring Boot for the web 已经包含了 Spring MVC Web 应用程序可能需要的依赖项。这意味着其中一些可能是不必要的。Spring Boot 非常适合用于教程:

  • 它包含了我们可能需要的依赖项;
  • 我们将看到它极大地简化了 Spring MVC 项目的配置;
  • 它内置了 Tomcat 服务器 [1],省去了我们在外部 Web 服务器上部署应用程序的麻烦;
  • 它允许我们生成一个包含上述所有依赖项的可执行 JAR 文件。该 JAR 文件可在不同平台间迁移,无需重新配置。

您可以在 Spring 生态系统网站 [http://spring.io/guides] 上找到许多使用 Spring Boot 的示例。既然我们已经了解了项目的依赖项,就可以继续研究代码了。

9.2.3. [业务]层

  

[业务]层将实现以下[IMetier]接口:


package dvp.rxjava.server.metier;
 
public interface IMetier {
  // random numbers in the [a,b] interval
  // n numbers are generated with n itself a random number in the interval [minCount, maxCount]
  // numbers are generated after a delay of 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);
}

该接口与第 8.4 节中讨论的 Swing 环境中的接口几乎完全相同。在第 8 行中,[getAleas] 方法返回以下 [AleasMetier] 类型:


package dvp.rxjava.server.metier;
 
import java.util.List;
 
public class AleasMetier {
  // fields
  private int delay;
  private List<Integer> aleas;
 
  // manufacturers
  public AleasMetier(){
 
  }
 
  public AleasMetier(int delay, List<Integer> aleas){
    this.delay=delay;
    this.aleas=aleas;
  }
 
  public AleasMetier(AleasMetier aleasMetier){
    this.delay=aleasMetier.delay;
    this.aleas=aleasMetier.aleas;
  }
 
  // getters and setters
...
}

实现 [IMetier] 接口的 [Metier] 类的代码如下:


package dvp.rxjava.server.metier;
 
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.*;
 
@Service
public class Metier implements IMetier {
 
  @Autowired
  private ObjectMapper mapper;
 
  @Override
  public AleasMetier getAleas(int a, int b, int minCount, int maxCount, int minDelay, int maxDelay) {
    // random numbers in the [a,b] interval
    // n numbers are generated with n itself a random number in the interval [minCount, maxCount]
    // numbers are generated after a delay of milliseconds,
    // where [delay] is itself a random number in the interval [minDelay, maxDelay]
 
    // some checks
    List<String> messages = new ArrayList<>();
    int erreur = 0;
    if (a < 0) {
      messages.add("Le nombre a de l'intervalle [a,b] de génération doit être supérieur à 0");
      erreur |= 2;
    }
    if (a >= b) {
      messages.add("Dans l'intervalle [a,b] de génération, on doit avoir a< b");
      erreur |= 4;
    }
    if (minCount < 0) {
      messages.add("Le nombre min de l'intervalle [min,count] du nombre de valeurs générées doit être supérieur à 0");
      erreur |= 16;
    }
    if (minCount > maxCount) {
      messages.add("Dans l'intervalle [min,count] du nombre de valeurs générées, on doit avoir min<= max");
      erreur |= 32;
    }
    if (minDelay < 0) {
      messages.add("Le nombre min de l'intervalle [min,count] du délai d'attente doit être supérieur à 0");
      erreur |= 64;
    }
    if (minCount > maxCount) {
      messages.add("Dans l'intervalle [min,count] du délai d'attente, on doit avoir min<= max");
      erreur |= 128;
    }
    if (maxDelay > 5000) {
      messages.add("L'attente en millisecondes avant la génération des nombres doit être dans l'intervalle [0,5000]");
      erreur |= 256;
    }
    // mistakes?
    if (!messages.isEmpty()) {
      throw new AleasException(String.join(" [---] ", messages), erreur);
    }
    // random number generator
    Random random = new Random();
    // waiting?
    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);
      }
    }
    // result generation
    int count = minCount + random.nextInt(maxCount - minCount + 1);
    List<Integer> nombres = new ArrayList<Integer>();
    for (int i = 0; i < count; i++) {
      nombres.add(a + random.nextInt(b - a + 1));
    }
    // return result
    return new AleasMetier(delay,nombres);
  }
 
}

我们不再对该类进行详细说明:它与第 8.4 节中 Swing 环境中的类类似。我们仅需注意以下几点:

  • 第 10 行:Spring 注解 [@Service],它会促使 Spring 将该类实例化为单例(singleton),并将其引用提供给其他 Spring 组件。此处本可使用其他 Spring 注解来达到相同效果;
  • 第13–14行:注入了一个JSON映射器。Spring是一个对象容器。该容器在Web应用程序启动时被实例化,随后配置文件中定义的对象会被实例化,默认情况下为单例(singleton)。 一个 Spring 单例可以包含对其他 Spring 对象的引用。此处即为这种情况:[business] 单例(第 10–11 行)将持有对 [mapper] 单例(第 13–14 行)的引用。这被称为依赖注入。将一个单例注入另一个单例有两种方式:
    • 按类型注入:若待注入的单例是该类型下唯一的 Spring 对象,则可采用此方式。此处第 13–14 行中的注入(类型为 ObjectMapper)即属于此情况;
    • 通过名称注入:当多个 Spring 对象具有相同类型时适用。此时,必须添加 @Qualifier("singletonName") 注解来指定单例的名称;

[Metier] 类会抛出 [AleaException] 类型的异常:


package android.exemples.server.metier;
 
public class AleaException extends RuntimeException {
 
  // error code
  private int code;
 
  // manufacturers
  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;
  }
}
  • 第 3 行:[AleasException] 继承自 [RuntimeException] 类。因此,它是一个未处理的异常(无需使用 try/catch 进行处理);
  • 第 6 行:向 [RuntimeException] 类添加了一个错误代码;

9.2.4. Web 服务 / JSON

  

该 Web 服务/JSON 由 Spring MVC 实现。Spring MVC 通过以下方式实现 MVC(模型-视图-控制器)架构模式:

客户端请求的处理流程如下:

  1. 请求 – 请求的 URL 格式为 http://machine:port/contexte/Action/param1/param2/....?p1=v1&p2=v2&... [Dispatcher Servlet] 是 Spring 框架中负责处理传入 URL 的类。它会将 URL “路由”到必须处理该请求的操作(Action)。这些操作是称为 [控制器(Controller)] 的特定类中的方法。 此处 MVC 中的 C 代表 [Dispatcher Servlet、Controller、Action] 这一链条。如果未配置任何 Action 来处理传入的 URL,[Dispatcher Servlet] 将返回请求的 URL 未找到(404 NOT FOUND 错误);
  1. 处理
  • 被选中的 Action 可以使用 [Dispatcher Servlet] 传递给它的参数。这些参数可能来自多个来源:
    • URL 的路径 [/param1/param2/...],
    • URL 参数 [p1=v1&p2=v2]
    • 浏览器随请求提交的参数;
  • 在处理用户请求时,操作可能需要调用 [业务] 层 [2b]。一旦处理完客户端的请求,可能会触发各种响应。一个典型的例子是:
    • 若请求无法正确处理,则返回错误页面
    • 否则则显示确认页面
  • 操作会指示显示特定的视图 [3]。该视图将展示被称为视图模型的数据。这就是 MVC 中的 M。操作将创建这个 M 模型 [2c],并指示显示 V 视图 [3];
  1. 响应——选定的视图 V 使用操作生成的模型 M 来初始化其必须发送给客户端的 HTML 响应中的动态部分,然后发送该响应。

对于 Web 服务/JSON,上述架构稍作修改:

  • 在 [4a] 中,模型(即一个 Java 类)通过 JSON 库转换为 JSON 字符串;
  • 在 [4b] 中,该 JSON 字符串被发送至浏览器;

让我们回到应用程序的 [Web] 层:

在我们的应用程序中,只有一个控制器:

  

该 Web 服务/JSON 将向其客户端发送如下所示的 [AleasResponse] 类型的响应:


package dvp.rxjava.server.web;
 
import dvp.rxjava.server.metier.AleasMetier;
 
public class AleasResponse extends AleasMetier {
 
  // error code
  private int erreur;
  // error message
  private String message;
 
  // manufacturers
  public AleasResponse() {
 
  }
 
  public AleasResponse(int erreur, String message, AleasMetier aleasMetier) {
    super(aleasMetier);
    this.erreur = erreur;
    this.message = message;
  }
  // getters and setters
 
  public void setAleasMetier(AleasMetier aleasMetier) {
    this.setDelay(aleasMetier.getDelay());
    this.setAleas(aleasMetier.getAleas());
  }
...
}
  • 第 5 行:[AleasResponse] 类继承自 [AleasMetier] 类,因此继承了其所有属性(aleas、delay);
  • 第 8 行:一个错误代码(若无错误则为 0);
  • 第 10 行:如果 error != 0,则返回错误消息;若无错误,则返回 null

[AleasController] 控制器如下:


package dvp.rxjava.server.web;
 
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
 
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
 
import dvp.rxjava.server.metier.AleasException;
import dvp.rxjava.server.metier.IMetier;
 
@Controller
public class AleasController {

    // business layer
    @Autowired
    private IMetier metier;
    @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 milliseconds,
    // where [delay] is a random number in the range [minDelay, maxDelay]
    @RequestMapping(value = "/{a}/{b}/{minCount}/{maxCount}/{minDelay}/{maxDelay}", method = RequestMethod.GET, produces = "application/json")
    @ResponseBody
    public String getAleas(@PathVariable("a") int a, @PathVariable("b") int b, @PathVariable("minCount") int minCount,
            @PathVariable("maxCount") int maxCount, @PathVariable("minDelay") int minDelay,
            @PathVariable("maxDelay") int maxDelay) throws JsonProcessingException {
 
        // we prepare the answer
        AleasResponse response = new AleasResponse();
        // the business layer is used to generate random numbers
        try {
            response.setAleasMetier(metier.getAleas(a, b, minCount, maxCount, minDelay, maxDelay));
        } catch (AleasException e) {
            // case of error (code and message)
            response.setErreur(e.getCode());
            response.setMessage(e.getMessage());
        }
        // we return the answer jSON
        return mapper.writeValueAsString(response);
    }
}
  • 第 16 行:[@Controller] 注解将 [AleasController] 类设为 Spring 单例。它还表明该类包含用于处理 Web 应用程序中特定 URL 请求的方法。此处仅在第 29 行有一个;
  • 第 20–21 行:[@Autowired] 注解指示 Spring 将类型为 [IMetier] 的组件注入该字段。这将对应前面的 [Metier] 类。由于我们为其添加了 [@Service] 注解,因此它被视为 Spring 组件;
  • 第 22–23 行:[@Autowired] 注解指示 Spring 将类型为 [ObjectMapper] 的组件注入该字段。我们稍后将定义该组件;
  • 第 31 行:[getAleas] 方法用于生成随机数。其名称无关紧要。当该方法运行时,第 31–33 行的参数已由 Spring MVC 初始化。我们稍后将了解具体实现。此外,该方法之所以运行,是因为 Web 服务器接收到针对第 29 行 URL 的 HTTP GET 请求(method 属性);
  • 第 30 行:[@ResponseBody] 注解表示该方法的结果必须原样发送给客户端。在此,我们将发送一个字符串,该字符串将是类型为 [AleasResponse] 的 JSON 字符串;
  • 第 29 行:处理后的 URL 格式为 /{a}/{b}/{minCount}/{maxCount}/{minDelay}/{maxDelay},其中 {x} 代表一个变量。这些变量在第 32–33 行被赋值给方法的参数。这是通过 @PathVariable("x") 注解实现的。 请注意,{x} 的值是 URL 的组成部分,因此类型为 String。从 String 转换为方法参数类型可能会失败。 此时 Spring MVC 会抛出异常。总结如下:若我在浏览器中请求 URL /100/200/10/20/300/400,第 31 行的 getAlias 方法将执行,其参数分别为 a=100(第 31 行)、 b=200(第 31 行)、minCount=10(第 31 行)、maxCount=20(第 32 行)、minDelay=300(第 32 行)、maxDelay=400(第 33 行);
  • 第 39 行:我们向 [business] 层请求一组随机数。请注意,[business].getRandom 方法可能会抛出异常;
  • 第 42–43 行:错误处理;
  • 第 46 行:将 [AleasResponse] 响应作为 JSON 字符串返回;

9.2.5. Spring 项目配置

  

配置 Spring 有多种方法:

  • 使用 XML 文件;
  • 使用 Java 代码;
  • 同时使用这两种方式;

我们选择使用 Java 代码来配置我们的 Web 应用程序。上文中的 [Config] 类负责处理此配置:


package dvp.rxjava.server.config;
 
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.embedded.EmbeddedServletContainerFactory;
import org.springframework.boot.context.embedded.ServletRegistrationBean;
import org.springframework.boot.context.embedded.tomcat.TomcatEmbeddedServletContainerFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.servlet.DispatcherServlet;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
 
@ComponentScan(basePackages = { "dvp.rxjava.server.metier", "dvp.rxjava.server.web" })
@EnableWebMvc
public class Config {
  // -------------------------------- layer configuration [web]
  @Autowired
  private ApplicationContext context;
 
  @Bean
  public DispatcherServlet dispatcherServlet() {
    DispatcherServlet servlet = new DispatcherServlet((WebApplicationContext) context);
    return servlet;
  }
 
  @Bean
  public ServletRegistrationBean servletRegistrationBean(DispatcherServlet dispatcherServlet) {
    return new ServletRegistrationBean(dispatcherServlet, "/*");
  }
 
  @Bean
  public EmbeddedServletContainerFactory embeddedServletContainerFactory() {
    return new TomcatEmbeddedServletContainerFactory("", 8080);
  }
 
  // mapper jSON
  @Bean
  public ObjectMapper jsonMapper() {
    return new ObjectMapper();
  }
}
  • 第 15 行:我们告诉 Spring 在哪些包中查找要实例化的对象。它将找到两个:
    • 带有 [@Service] 注解的 [Metier] 类;
    • 带有 [@Controller] 注解的 [AleasController] 类;
  • 第 16 行:[@EnableWebMvc] 注解会触发 Spring MVC 框架的自动配置;
  • 第 19–20 行:注入 Spring 上下文(Spring 对象的容器)。此注入是必要的,因为第 22–26 行中的对象需要它;
  • Spring 配置文件可通过带有 [@Bean] 注解的方法定义新的 Spring 对象。该方法的返回值即成为 Spring 对象;
  • 第 22–26 行:定义 Spring MVC 框架的 Servlet,该 Servlet 将 HTTP 请求路由到正确的控制器和方法。[DispatcherServlet] 是 Spring 类;
  • 第 28–31 行:此处指定该 Servlet 处理所有 URL;
  • 第 33–36 行:该 Bean 的存在将激活项目归档中包含的 Tomcat 服务器。它将在 8080 端口监听请求;
  • 第 39–42 行:一个 JSON 映射器。这就是被注入到 Spring 对象 [Metier] 和 [AleasController] 中的那个;

9.2.6. 运行 Web 服务器

  

该项目通过以下可执行类 [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) {
    // application execution
    SpringApplication.run(Config.class, args);
  }
 
}
  • 第 6 行:[Application] 类是一个可执行类(第 7–10 行);
  • 第 9 行:静态方法 [SpringApplication.run] 是 [Spring Boot](第 4 行)中用于启动应用程序的方法。其第一个参数是配置该项目的 Java 类。此处即为我们刚刚描述的 [Config] 类。第二个参数是传递给 [main] 方法(第 7 行)的参数数组。此处将不包含任何参数;

关于实际执行过程,请参阅第 9.2.1 节。

9.3. Android 客户端

注意:以下 Android 项目相当复杂。它需要对 Android 有扎实的理解,相关知识可参考例如 [使用 Android Studio 进行 Android 平板编程入门]。

ActivityViewsLayer[DAO]UserServer

客户端将包含两个组件:

  1. 一个 [展示] 层(视图 + 活动);
  2. 一个 [DAO] 层,用于与我们之前学习的 [Web / JSON] 服务进行通信。

9.3.1. RxAndroid

为了与随机数服务器进行异步通信,Android客户端将使用RxAndroid库。该库将RxJava扩展到了Android生态系统中。与我们之前在Swing应用程序中所做的一样,我们将仅使用RxAndroid提供的一个扩展:调度器[AndroidSchedulers.mainThread()]。Android GUI遵循与Swing界面相同的规则:

  • 事件在称为事件循环或 UI 线程的单线程中处理;
  • 当事件触发异步操作时,若需利用这些操作的结果更新用户界面,则必须在 UI 线程中获取结果;

Android 客户端:

  • 将向随机数服务器发送多个异步请求。这些请求将在客户端使用调度器的线程 [Schedulers.io()] 执行;
  • 这些异步请求将返回可观察对象,并被合并为单个可观察对象;
  • 该可观察对象将在客户端通过 RxAndroid 提供的调度器 [AndroidSchedulers.mainThread()] 进行监听;

9.3.2. IntelliJ IDEA 项目

该 Android 项目命名为 [client]:

它将通过 [2] 运行。

注意:执行结果在很大程度上取决于所用 IntelliJ IDEA IDE 的配置。在非我的机器上,上述执行步骤 [2] 很可能无法一次成功。对于初学者而言,正确配置 IntelliJ IDEA IDE 以运行此项目可能是一项艰巨的任务。以下是几个需要注意的要点:

  • 在 [3] 中,访问项目结构;
  • 在 [4-5] 中,展示了我机器上安装的 JDK 和 Android SDK。请注意,JDK 1.8 并非必需。Android 不支持某些 Java 8 特性,包括 lambda 表达式。因此,为了实例化函数式接口,我们将使用匿名类。因此,JDK 1.6 即可满足需求。不过,分发的项目已配置为使用 JDK 1.8;

用于配置 Android 项目的 [build.gradle] [6] 文件如下:


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

根据已安装的 Android SDK 版本,第 8 行、第 24–25 行以及第 29 行中的版本号可能需要进行修改。

要安装新的 Android SDK,请按以下步骤使用 SDK 管理器 [1]:

该项目已配置为:

  • SDK API 23 [2];
  • SDK 构建工具 23.0.3 [3];
  • SDK 工具 25.1.3 [4]

最后,请在 [local.properties] 文件 [4] 的第 11 行中验证 Android SDK 路径:


## 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. 运行 IntelliJ IDEA 项目

一旦为项目创建了合适的环境,即可按以下方式运行:

  • 在 [1] 中,启动 Genymotion Android 模拟器;
  • 在 [2] 中,运行 [app] 运行配置;
  • 在 [3] 中,创建一个运行配置;
 
  • 在 [1, 3] 中,该配置被命名为 [app];
  • 在[2]中,它对应于名为[app]的模块的执行;
  • 在[4]中,我们指定在执行期间,IDE应为我们提供一个执行设备。在此,该设备始终为Genymotion模拟器;
  • 在 [5] 中,我们指定该设备应用于该配置的所有执行;

在 Genymotion 模拟器上运行该项目需先执行以下初始命令:

Image

要确定在 [1] 中应输入什么,请打开一个 DOS 命令窗口并输入以下 [ipconfig] 命令:


C:\Program Files\Console2>ipconfig
 
Configuration IP de Windows
 
 
Carte Ethernet Ethernet :
 
   Statut du média. . . . . . . . . . . . : Média déconnecté
   Suffixe DNS propre à la connexion. . . : ad.univ-angers.fr
 
Carte réseau sans fil Connexion au réseau local* 3 :
 
   Statut du média. . . . . . . . . . . . : Média déconnecté
   Suffixe DNS propre à la connexion. . . :
 
Carte Ethernet VirtualBox Host-Only Network :
 
   Suffixe DNS propre à la connexion. . . :
   Adresse IPv6 de liaison locale. . . . .: fe80::8076:36e6:3b38:5e98%16
   Adresse IPv4. . . . . . . . . . . . . .: 192.168.56.2
   Masque de sous-réseau. . . . . . . . . : 255.255.255.0
   Passerelle par défaut. . . . . . . . . :
 
Carte Ethernet Ethernet 2 :
 
   Suffixe DNS propre à la connexion. . . :
   Adresse IPv6 de liaison locale. . . . .: fe80::d0d9:e01f:ddde:1f4b%14
   Adresse IPv4. . . . . . . . . . . . . .: 192.168.95.1
   Masque de sous-réseau. . . . . . . . . : 255.255.255.0
   Passerelle par défaut. . . . . . . . . :
 
Carte réseau sans fil Wi-Fi :
 
   Suffixe DNS propre à la connexion. . . :
   Adresse IPv6 de liaison locale. . . . .: fe80::54b3:afe5:e199:2206%10
   Adresse IPv4. . . . . . . . . . . . . .: 192.168.0.13
   Masque de sous-réseau. . . . . . . . . : 255.255.255.0
   Passerelle par défaut. . . . . . . . . : fe80::523d:e5ff:fe0c:4ad9 192.168.0.1
 
 

请输入 [1] 选择您机器的其中一个 IP 地址(第 20、28、32 行)。如果您启用了 Windows 防火墙,可能需要将其禁用,以便 Android 模拟器能够连接到随机数服务器。

使用上述信息执行异步请求将得到以下结果:

Image

每次请求都会返回包含以下字段的 JSON 响应:

  • aleas:服务器生成的随机数;
  • idClient:请求 ID;
  • on:执行该请求的客户端线程;
  • requestAt:请求时间;
  • responseAt:收到响应的时间;
  • delay:服务器返回响应前观察到的等待时间;
  • error:错误代码——若无错误则为 0;
  • message:错误消息——若无错误则为 null
  • observedAt:观察到响应的时间;
  • observedOn:观察响应的线程。在此处,该值始终为 [main],指代 UI 线程;

由于请求是异步的,且服务器端的等待时间是随机的,因此响应返回的顺序是零散的。

9.3.4. 项目的 Gradle 依赖项

该项目需要依赖项,我们在 [app/build.gradle] 文件中进行了指定:

  

dependencies {
  compile 'com.android.support:appcompat-v7:23.1.1'
  compile 'com.android.support:design:23.1.1'
  compile fileTree(dir: 'libs', include: ['*.jar'])
  compile 'org.springframework.android:spring-android-rest-template:1.0.1.RELEASE'
  compile 'org.codehaus.jackson:jackson-mapper-asl:1.9.9'
  compile 'io.reactivex:rxandroid:1.1.0'
}
  • 第 2–3 行的依赖项是使用 SDK 23 的 Android 项目的标准依赖项;
  • 第 5 行的依赖引入了 Spring [RestTemplate] 对象,用于管理 [DAO] 层与服务器之间的通信;
  • 第 6 行的依赖引入了应用程序使用的 JSON 库 [Jackson];
  • 第 7 行的依赖引入了 RxAndroid 库(以及随附的 RxJava 库),UI 层通过该库与 [DAO] 层进行通信;

9.3.5. Android 应用程序清单

  

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
          package="android.aleas">
 
  <uses-permission android:name="android.permission.INTERNET"/>
 
  <application
    android:allowBackup="true"
    android:icon="@mipmap/ic_launcher"
    android:label="@string/app_name"
    android:supportsRtl="true"
    android:theme="@style/AppTheme">
    <activity
      android:name="android.aleas.activity.MainActivity"
      android:label="@string/app_name"
      android:theme="@style/AppTheme.NoActionBar">
      <intent-filter>
        <action android:name="android.intent.action.MAIN"/>
 
        <category android:name="android.intent.category.LAUNCHER"/>
      </intent-filter>
    </activity>
  </application>
 
</manifest>
  • 第5行:必须允许访问互联网;

9.3.6. [DAO] 层

 

9.3.6.1. [DAO]层的[IDao]接口

[DAO]层的接口如下:


package android.aleas.dao;
 
import android.aleas.fragments.Request;
import rx.Observable;
 
public interface IDao {
 
  // random numbers in the [a,b] interval
  // n numbers are generated with n itself a random number in the interval [minCount, maxCount]
  // numbers are generated after a delay of milliseconds,
  // where [delay] is itself a random number in the interval [minDelay, maxDelay]
  public Observable<AleasDaoResponse> getAleas(final Request request);
 
  // URL of the web service
  public void setUrlServiceWebJson(String url);
 
  // max wait time (ms) for server response to connection request
  // max wait time (ms) for server response to a request
  public void setClientTimeouts(int connectTimeout, int readTimeOut);
 
}
  • 第 12 行:[DAO] 层中异步生成随机数的函数;
  • 第 15 行:向 [DAO] 实现提供随机数生成服务的 URL;
  • 第 19 行:为 [DAO] 实现设置最大超时时间,以避免服务器无响应时出现过长的等待;

[getAleas] 方法通过以下 [Request] 对象接收所有参数:


package android.aleas.fragments;
 
public class Request {
 
  // request no
  int id;
  // user input
  private int nbRequests;
  private int a;
  private int b;
  private int minCount;
  private int maxCount;
  private int minDelay;
  private int maxDelay;
 
  // manufacturers
  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
...
}

在这里,我们可以看到服务器 URL 中需要查询的大部分参数。

[getAleas] 方法返回 Observable<AleasDaoResponse> 类型,其中 [AleasDaoResponse] 类的定义如下:


package android.aleas.dao;
 
import java.util.List;
 
public class AleasDaoResponse {
 
  // error code
  private int erreur;
  // error message
  private String message;
  // server waiting time
  private int delay;
  // random numbers delivered by the server
  private List<Integer> aleas;
  // customer status
  private ClientState clientState;
 
  // manufacturers
 
  public AleasDaoResponse() {
  }
 
  public AleasDaoResponse(int erreur, String message, int delay, List<Integer> aleas, ClientState clientState) {
    this.erreur = erreur;
    this.message = message;
    this.delay = delay;
    this.aleas = aleas;
    this.clientState = clientState;
  }
 
  // getters and setters
...
}

[ClientState] 类型的定义如下:


package android.aleas.dao;
 
import org.codehaus.jackson.map.annotate.JsonFilter;
 
import java.text.SimpleDateFormat;
import java.util.Calendar;
 
public class ClientState {
 
  // name of execution thread
  private String on;
  // query time
  private String requestAt;
  // response time
  private String responseAt;
  // customer id
  private int idClient;
 
  // manufacturer
  public ClientState() {
    on = Thread.currentThread().getName();
    requestAt = getTimeStamp();
  }
 
  public ClientState(int idClient) {
    this();
    this.idClient = idClient;
  }
 
  // private methods
 
  private String getTimeStamp() {
    return new SimpleDateFormat("hh:mm:ss:SSS").format(Calendar.getInstance().getTime());
  }
 
  // getters and setters
...
}
  • 第 11 行:[DAO] 层的执行线程;
  • 第 13 行:请求时间;
  • 第15行:响应时间;
  • 第17行:请求编号;

字段 [on, requestAt, idClient] 由客户端在请求开始时初始化。字段 [responseAt] 在客户端收到服务器响应时初始化。

9.3.6.2. [DAO] 层的实现

  

[IDao] 接口由以下 [Dao] 类实现:


package android.aleas.dao;
 
import android.aleas.fragments.Request;
import android.util.Log;
import org.codehaus.jackson.map.ObjectMapper;
import org.codehaus.jackson.map.ser.impl.SimpleBeanPropertyFilter;
import org.codehaus.jackson.map.ser.impl.SimpleFilterProvider;
import org.codehaus.jackson.type.TypeReference;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.http.converter.StringHttpMessageConverter;
import org.springframework.web.client.RestTemplate;
import rx.Observable;
import rx.Subscriber;
 
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
 
public class Dao implements IDao {
 
  // customer REST
  private RestTemplate restTemplate;
  // URL service
  private String urlServiceWebJson;
 
  // mapper jSON
  private ObjectMapper mapper;
 
  // manufacturers
  public Dao() {
    // mapper jSON
    mapper = new ObjectMapper();
  }
 
  @Override
  public Observable<AleasDaoResponse> getAleas(final Request request) {
    ...
  }
 
  @Override
  public void setUrlServiceWebJson(String urlServiceWebJson) {
    // set the URL of the REST service
    this.urlServiceWebJson = urlServiceWebJson;
  }
 
  @Override
  public void setClientTimeouts(int connectTimeout, int readTimeOut) {
...
  }
}
  • 第 22 行:负责与随机数服务器通信的 [RestTemplate] 对象;
  • 第 24 行:生成服务的 URL——由第 41 行的 [setUrlServiceWebJson] 方法设置;
  • 第 27 行:用于反序列化随机数服务器发送的 JSON 字符串的 JSON 映射器;
  • 第 30–33 行:类构造函数;
  • 第 32 行:创建第 27 行中的 JSON 映射器;

[setClientTimeouts] 方法如下:


  // client REST
  private RestTemplate restTemplate;
...
 
  @Override
  public void setClientTimeouts(int connectTimeout, int readTimeOut) {
    // on fixe le timeout des requêtes du client REST
    HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory();
    factory.setReadTimeout(readTimeOut);
    factory.setConnectTimeout(connectTimeout);
    restTemplate = new RestTemplate(factory);
    restTemplate.getMessageConverters().add(new StringHttpMessageConverter());
}
  • 客户端与 Web 服务器/JSON 的通信由第 2 行中的 [RestTemplate] 对象处理。我们尚未对其进行初始化。[setClientTimeouts] 方法负责此操作;
  • 第 8 行:[HttpComponentsClientHttpRequestFactory] 类由 [spring-android-rest-template] 依赖项提供。它允许我们设置服务器响应的最大等待时间(第 9–10 行);
  • 第 11 行:我们创建 [RestTemplate] 对象,它将作为与 Web 服务通信的通道。我们将刚刚创建的 [factory] 对象作为参数传递给它;
  • 第 12 行:客户端与服务器的交互可以采取多种形式。通信通过文本行进行,我们必须告知 [RestTemplate] 对象如何处理这些文本行。为此,我们需要提供转换器——即能够处理文本行的类。 转换器的选择通常基于随文本行附带的 HTTP 头部。根据这些头部信息,[RestTemplate] 对象将从其拥有的转换器中选择最适合当前情况的一个。在此,我们仅使用一个转换器,即 String --> String 转换器,这意味着从服务器接收到的 String 类型数据将不会进行任何转换。

[getAleas] 方法最为复杂:


@Override
  public Observable<AleasDaoResponse> getAleas(final Request request) {
    Log.d("rxjava", String.format("service [DAO] pour client n° %s%n", request.getId()));
    // service execution
    return Observable.create(new Observable.OnSubscribe<AleasDaoResponse>() {
      @Override
      public void call(Subscriber<? super AleasDaoResponse> subscriber) {
        try {
          // URL of the service: /{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());
          // customer information
          ClientState clientState = new ClientState(request.getId());
          // synchronous http request
          String response = executeRestService("get", urlService, null);
          // deserialization of jSON server response
          AleasServerResponse aleasServerResponse = mapper.readValue(
            response,
            new TypeReference<AleasServerResponse>() {
            });
          // mistake?
          int erreur = aleasServerResponse.getErreur();
          if (erreur != 0) {
            // we forward the exception
            subscriber.onError(new AleasException(aleasServerResponse.getMessage(), erreur));
          } else {
            // enter the time of reception
            clientState.setResponseAt();
            // we forward the result to the subscriber
            subscriber.onNext(
              new AleasDaoResponse(aleasServerResponse.getErreur(), aleasServerResponse.getMessage(),
                aleasServerResponse.getDelay(), aleasServerResponse.getAleas(), clientState));
          }
        } catch (Exception ex) {
          // we forward the exception to the subscriber
          subscriber.onError(ex);
        } finally {
          // we signal the end of the observable
          // at runtime, we note that this method has no effect if method [onError] has been called previously - in line with theory - so we could place this instruction only in try
          subscriber.onCompleted();
        }
      }
    });
  }
  • 第 2 行:请注意,我们必须返回类型 [Observable<AleasResponse>];
  • 第 3 行:在 Android 控制台输出日志;
  • 第 5 行:[RestTemplate] 对象确保与服务器的同步通信。这意味着发起请求的执行线程将被阻塞,直到收到响应为止。在 Swing 示例中,我们曾看到如何使用 [Observable.create] 方法将同步操作转换为异步操作。这里我们采用相同的方法;
  • 第 7 行:第 5 行中 [Observable.OnSubscribe<AleasDaoResponse>] 接口的 [call] 方法。当观察者订阅可观察对象时,会调用此方法;
  • 第 10–12 行:构建随机数服务的 URL;
  • 第 14 行:初始化 [ClientState] 对象。此处记录了请求的时间;
  • 第 16 行:同步 HTTP 请求。返回一个 JSON 响应。[executeRestService] 方法期望三个参数:
      1. 用于查询服务的 HTTP 方法;
      2. 服务 URL;
      3. 待提交的对象(类型为 Object),若 HTTP 方法非 POST 则为 null
  • 第18-21行:将接收到的JSON字符串反序列化为[AleasServerResponse]类型。该类型的定义如下:

package android.aleas.dao;
 
import java.util.List;
 
public class AleasServerResponse {
 
  // error code
  private int erreur;
  // error message
  private String message;
  // server waiting time
  private int delay;
  // random numbers
  private List<Integer> aleas;
 
  // getters and setters
...
}
  • 第 23 行:获取服务器发送的错误代码;
  • 第 24–26 行:如果发生错误,将异常转发给订阅者;
  • 第 29 行:更新 [clientState],该值将作为响应的一部分发送给订阅者;
  • 第 31–33 行:将响应发送给订阅者。其类型为 [AleasDaoResponse];
  • 第 35–37 行:统一处理所有错误情况。最可能出现的错误是网络错误;
  • 第 41 行:发送传输结束通知;

9.3.7. 应用程序视图

  

该应用程序包含以下两个视图:

请求视图

Image

响应视图

Image

9.3.7.1. [MyFragment] 类

有两个片段:

  • [RequestFragment] 用于请求;
  • [ResponseFragment] 用于响应;

这两个片段都继承自以下 [MyFragment] 类:


package android.aleas.fragments;
 
import android.aleas.activity.MainActivity;
import android.aleas.activity.Session;
import android.support.v4.app.Fragment;
 
public abstract class MyFragment extends Fragment {
 
  // ------------- data common to all fragments
  protected MainActivity activity;
  protected Session session;
 
  public abstract void onRefresh();
 
}
  • 第 7 行:[MyFragment] 类继承自 Android 的 [Fragment] 类;
  • 第 10–11 行:所有片段共享的数据;
  • 第 10 行:每个片段都了解应用程序的单一活动;
  • 第 11 行:片段之间通过会话进行通信;
  • 第 13 行:在显示片段之前,系统会要求其使用会话内容刷新自身。该方法被声明为抽象方法,因为它由子类实现。因此,该类本身也被声明为抽象类(第 7 行);

[Session] 类包含应用程序中各个片段共享的数据。其代码如下:

  

package android.aleas.activity;
 
import android.aleas.fragments.Request;
import android.widget.ArrayAdapter;
 
public class Session {
 
  // application activity
  private MainActivity activity;
  // number of requests
  private int nbRequests;
  // request characteristics
  private int a;
  private int b;
  private int minCount;
  private int maxCount;
  private int minDelay;
  private int maxDelay;
  // URL web service / jSON
  private String urlWebJson;
  // operation began
  private boolean onAir;
  // idem but a little later in time
  private boolean operationStarted;
  // the name of the example chosen by the user from the list of examples
  private String exampleName;
  // its number in the list of fragments
  private int examplePosition;
  // the example spinner adapter in the query view
  private ArrayAdapter<CharSequence> spinnerExemplesAdapter;
 
  // methods
  public void setInfos(int nbRequests, int a, int b, int minCount, int maxCount, int minDelay, int maxDelay, String urlWebJson, String exampleName, int examplePosition) {
    this.nbRequests = nbRequests;
    this.a = a;
    this.b = b;
    this.minCount = minCount;
    this.maxCount = maxCount;
    this.minDelay = minDelay;
    this.maxDelay = maxDelay;
    this.urlWebJson = urlWebJson;
    this.exampleName = exampleName;
    this.examplePosition = examplePosition;
  }
 
  public Request getRequest() {
    return new Request(0, nbRequests, a, b, minCount, maxCount, minDelay, maxDelay);
  }
 
  // getters and setters
...
}

第 46 行中的方法创建了 [Request] 对象,该对象封装了用户在请求视图中提供的所有信息:

  

package android.aleas.fragments;
 
public class Request {
 
  // request no
  int id;
  // user input
  private int nbRequests;
  private int a;
  private int b;
  private int minCount;
  private int maxCount;
  private int minDelay;
  private int maxDelay;
 
  // manufacturers
  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. 请求的 [RequestFragment] 片段

请求片段包含以下组件:

Image

该应用程序仅有一个视图,该视图包含两个选项卡:

  • [1]:请求选项卡;
  • [2]:响应选项卡;

[RequestFragment] 片段的组件如下:

编号
类型
名称
角色
3
EditText
edtNbRequests
向随机数生成器服务发送的请求次数
4
EditText
edtA, edtB
数字生成区间的边界 [a,b];
5
EditText
edtMinCount, edtMaxCount
该服务生成 count 个数字,其中 count 是区间 [minCount, maxCount] 内的随机数
6
EditText
edtMinDelay, edtMaxDelay
该服务在生成数字前等待 delay 毫秒,其中 delay 是 [minDelay, maxDelay] 范围内的随机数
7
编辑文本
edtUrlServiceRest
随机数生成服务的 URL;
8
旋转按钮
spinnerExamples
示例的下拉列表。每个示例演示了 [Observable] 类的特定方法;
8
按钮
btnExecute
用于触发调用数字生成服务的按钮;

报告输入错误:

Image

组件 1 至 6 是 [TextView] 组件,名称如下(按顺序):txtErrorRequeststxtErrorIntervaltxtErrorCounttxtErrorDelaytxtWebServiceErrorMessage

9.3.7.3. 响应的 [ResponseFragment] 片段

响应片段包含以下组件:

Image

编号
类型
名称
角色
1
TextView
infoResponses
已接收的回复数量
2
ListView
listResponses
从服务器接收到的 JSON 字符串列表
3
按钮
btnCancel
用于取消向服务器的请求

9.3.7.4. Android 活动 [MainActivity]

  

[MainActivity] 类显示以下视图:


<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
                                                 xmlns:tools="http://schemas.android.com/tools"
                                                 xmlns:app="http://schemas.android.com/apk/res-auto"
                                                 android:id="@+id/main_content"
                                                 android:layout_width="match_parent"
                                                 android:layout_height="match_parent"
                                                 android:fitsSystemWindows="true"
                                                 tools:context="android.arduinos.ui.activity.MainActivity">
 
  <!-- application 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">
 
      <!-- waiting 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>

该视图的组件如下:

lines
类型
名称
角色
20-34
工具栏
工具栏
应用程序工具栏
29-34
进度条
加载面板
在处理用户请求时显示的占位图
37-40
TabLayout
标签
应用程序的标签栏
44-51
MyPager
容器
用于显示应用程序各个片段的容器

[MyPager] 类的定义如下:


package android.aleas.activity;
 
import android.content.Context;
import android.support.v4.view.ViewPager;
import android.util.AttributeSet;
import android.view.MotionEvent;
 
public class MyPager extends ViewPager {
 
  // swipe control
  private boolean isSwipeEnabled;
 
  // manufacturers
  public MyPager(Context context) {
    super(context);
  }
 
  public MyPager(Context context, AttributeSet attrs) {
    super(context, attrs);
  }
 
  // method redefinition
  @Override
  public boolean onInterceptTouchEvent(MotionEvent event) {
    // swipe authorized?
    if (isSwipeEnabled) {
      return super.onInterceptTouchEvent(event);
    } else {
      return false;
    }
  }
 
  @Override
  public boolean onTouchEvent(MotionEvent event) {
    // swipe authorized?
    if (isSwipeEnabled) {
      return super.onTouchEvent(event);
    } else {
      return false;
    }
  }
 
  // setter
  public void setSwipeEnabled(boolean isSwipeEnabled) {
    this.isSwipeEnabled = isSwipeEnabled;
  }
 
}
  • [MyPager] 类继承自标准的 Android [ViewPager] 类。我们使用 [MyPager] 类而非 [ViewPager] 类,仅仅是因为我们希望禁用滑动功能:默认情况下,使用 [ViewPager] 类时,可以通过滑动(向左或向右滑动)在标签页之间切换。而在此处,我们不希望出现这种行为;
  • 第 11 行:控制滑动功能的布尔变量(第 26 行和第 36 行);
  • 第 44–46 行:用于初始化第 11 行字段的方法;

Android 活动 [MainActivity] 的骨架如下:


package android.aleas.activity;
 
import android.aleas.R;
import android.aleas.dao.AleasDaoResponse;
import android.aleas.dao.Dao;
import android.aleas.dao.IDao;
import android.aleas.fragments.MyFragment;
import android.aleas.fragments.Request;
import android.aleas.fragments.RequestFragment;
import android.os.Bundle;
import android.support.design.widget.TabLayout;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentPagerAdapter;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.Toolbar;
import android.view.View;
import android.widget.ArrayAdapter;
import android.widget.ProgressBar;
import rx.Observable;
 
public class MainActivity extends AppCompatActivity implements IDao {
 
  // layer [DAO]
  private IDao dao;
  // the session
  private Session session;
 
  // manufacturer
  public MainActivity() {
    // parent
    super();
    // session
    session = new Session();
    // DAO
    dao = new Dao();
  }
 
 
  // getters
 
  public Session getSession() {
    return session;
  }
 
  // implémentation IDao ----------------------------------------
  @Override
  public Observable<AleasDaoResponse> getAleas(Request request) {
    return dao.getAleas(request);
  }
 
  @Override
  public void setUrlServiceWebJson(String url) {
    dao.setUrlServiceWebJson(url);
  }
 
  @Override
  public void setClientTimeouts(int connectTimeout, int readTimeOut) {
    dao.setClientTimeouts(connectTimeout, readTimeOut);
  }
 
}
  • 第 21 行:[MainActivity] 类继承了标准的 Android 类 [AppCompatActivity]。因此,它是一个标准的 Android 活动;
  • 第 21 行:[MainActivity] 类实现了 [IDao] 接口;

回到应用程序架构:

由于 Activity 实现了 [DAO] 层接口,因此视图无需了解 [DAO] 层:当视图需要与服务器交互时,其事件处理程序将直接与 [Activity] 层进行通信。

  • 第 24 行:对 [DAO] 层的引用,该引用由第 35 行的构造函数初始化;
  • 第 26 行:对片段共享的会话的引用,该会话由第 33 行的构造函数初始化;
  • 第 46–59 行:[IDao] 接口的实现;

[MainActivity] 类按以下方式初始化其关联视图的组件:


  // barre d'outils
  private Toolbar toolbar;
  // gestionnaire de fragments
  private MyPager mViewPager;
  // conteneur d'onglets
  private TabLayout tabLayout;
  // image d'attente
  private ProgressBar loadingPanel;
...
  @Override
  public void onCreate(Bundle savedInstanceState) {
    // classique
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
 
    // session
    session.setActivity(this);
    // configuration timeouts de la couche [DAO]
    setClientTimeouts(Constants.CONNECT_TIMEOUT, Constants.READ_TIMEOUT);
 
    // composants
    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);
 
    // au départ on n'a qu'un seul onglet
    TabLayout.Tab tab = tabLayout.newTab();
    tab.setText("Request");
    tabLayout.addTab(tab);
 
    // gestionnaire d'évt
    tabLayout.setOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {
      @Override
      public void onTabSelected(TabLayout.Tab tab) {
        // un onglet a été sélectionné - on change le fragment affiché par le conteneur de fragments
        int position = tab.getPosition();
        if (position == 0) {
          // onglet requête
          showView(0);
        } else {
          // onglet réponse - dépend de l'exemple choisi
          showView(session.getExamplePosition());
        }
      }
 
      @Override
      public void onTabUnselected(TabLayout.Tab tab) {
 
      }
 
      @Override
      public void onTabReselected(TabLayout.Tab tab) {
 
      }
    });
 
    // création des fragments des réponses
    createResponseFragments();
 
    // gestion image d'attente
    loadingPanel.setVisibility(View.INVISIBLE);
}

这段代码在 Activity 中相当常见。让我们来解释几个要点:

  • 第 19 行引用了以下 [Constants] 类:

package android.aleas.activity;
 
abstract public class Constants {
 
  final static public int VUE_REQUEST = 0;
  final static public int VUE_RESPONSE = 1;
  final static public int CONNECT_TIMEOUT = 1000;
  final static public int READ_TIMEOUT = 6000;
  final static public int DELAY_MAX = 5000;
  final static public String EXAMPLES_PACKAGE = "android.aleas.exemples";
}
  • 第 31–33 行:我们创建了第一个标题为 [Request] 的标签页。此时,内存中将包含以下内容:
    • [Request] 片段;
    • n 个类型为 [ExampleXXFragment] 的片段;

第一个标签页将始终显示 [Request] 片段。第二个标签页将显示与用户所选示例对应的 [ExampleXXFragment] 片段。因此,第二个标签页显示的片段会随时间变化;

  • 第 37–48 行:用户点击其中一个标签页时执行的代码;
  • 第 43 行:显示片段 #0;
  • 第 46 行:显示当前正在使用(显示中)的片段。其编号从会话中获取;
  • 第 62 行:为 [RequestFragment] 视图(第 1 个标签页)中的示例选择器中所有示例创建片段;
  • 第 65 行:当前隐藏加载图标;

要理解 [showView] 方法(第 43、46 行)和 [createResponseFragments] 方法,我们首先需要介绍内存中的片段管理器(该类包含在 MainActivity Java 文件中):


  // fragment manager - must define getItem, getCount methods
  public class SectionsPagerAdapter extends FragmentPagerAdapter {
 
    // managed fragments
    private MyFragment[] fragments;

    // manufacturer
    public SectionsPagerAdapter(FragmentManager fm, MyFragment[] fragments) {
      super(fm);
      this.fragments = fragments;
    }
 
    // must render fragment no. position
    @Override
    public MyFragment getItem(int position) {
      // the fragment
      return fragments[position];
    }
 
    // makes the number of fragments to manage
    @Override
    public int getCount() {
      // no. of fragments
      return fragments.length;
    }
  }
}
  • [SectionsPagerAdapter] 类继承自 Android 的 [FragmentPagerAdapter] 类。它重写了父类的两个方法:
    • 第 15 行的 [getItem] 方法;
    • [getCount] 方法,第 22 行;
  • [SectionsPagerAdapter] 类包含应用程序的所有片段。这些片段存储在第 5 行。请注意,它们的类型为 [MyFragment],如第 9.3.7.1 节所述;
  • 第 8 行:为了初始化自身,[SectionsPagerAdapter] 类接收其必须管理的片段;
  • 第 14–18 行:[getItem] 方法返回位于 [position] 位置的片段;
  • 第 21–25 行:[getCount] 方法返回片段的总数;

[createResponseFragments] 方法创建应用程序所需的所有片段:


private void createResponseFragments() {
    // spinner examples
    ArrayAdapter<CharSequence> adapter = ArrayAdapter.createFromResource(this, R.array.exemples, 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);
    // put the adapter in the session so that the [Request] view can retrieve it
    session.setSpinnerExemplesAdapter(adapter);
    ...
  }
  • 第 3 行:我们为示例转盘创建了一个适配器,在此情况下,该适配器是一个字符串列表,其中包含示例的名称。这些名称存在于 [layout/exemples.xml] 文件中:
  

[examples.xml] 文件包含以下代码:


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

第 1 行:此文件是 [createFromResource] 方法的第二个参数。在 [R.array.examples] 中,[examples] 是数组的名称(参见上文第 3 行),而不是文件名。

  • 第 5 行:我们将一个布局(显示管理器)与适配器关联起来。现在,适配器既拥有数据,也拥有其显示模式;
  • 第 7 行:我们将适配器添加到会话中。需要该适配器的 [RequestFragment] 将在此处获取它;

接下来继续查看 [createResponseFragments] 方法的代码:


private void createResponseFragments() {
    // spinner examples
    ArrayAdapter<CharSequence> adapter = ArrayAdapter.createFromResource(this, R.array.exemples, 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);
    // put the adapter in the session so that the [Request] view can retrieve it
    session.setSpinnerExemplesAdapter(adapter);
    // create fragment table (1 query, n responses)
    MyFragment[] tFragments = new MyFragment[adapter.getCount() + 1];
    // query fragment
    tFragments[0] = new RequestFragment();
    // answer fragments
    for (int i = 1; i < tFragments.length; i++) {
      // we construct the name of the fragment to be instantiated, corresponding to the example chosen by the user
      // this name must be the full name with 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 {
        // class instantiation
        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 table
      tFragments[i] = fragment;
    }
    // instantiation of the fragment manager with these new fragments
    mSectionsPagerAdapter = new SectionsPagerAdapter(getSupportFragmentManager(), tFragments);
    // Set up the ViewPager with the sections adapter.
    mViewPager.setAdapter(mSectionsPagerAdapter);
    // page navigation - this instruction is important
    // here we say that on both sides of the displayed view, we must keep [tFragments.length] views initialized
    // this means that all fragments used by the application are in memory and initialized
    // if you don't do this then the default [OffscreenPageLimit] is 1
    // so if the fragment displayed is no. 3, only fragments 2 and 4 will be initialized
    // this is done by calling the [onCreateView] method of these 2 fragments - this means that in this method, you must plan to
    // regenerate the visual appearance of the fragment the last time it was used
    // there's code that can't stand being run twice - it creates a huge mess and is complex to manage
    // here we've chosen to avoid these difficulties - in the logs, we can see that when the application starts, all fragments are created
    // and their method [onCreateView] executed - it's never executed again -
    mViewPager.setOffscreenPageLimit(tFragments.length);
    // inhibit swiping between fragments
    mViewPager.setSwipeEnabled(false);
  }
  • 第 9 行:创建将包含应用所有片段的数组;
  • 第 11 行:第一个片段是查询片段;
  • 第 13–28 行:我们将创建与示例数量相同的片段。这些片段均继承自响应片段 [ResponseFragment],并仅实现该示例特有的功能:生成被观测的值。这些值在不同示例之间各不相同;
  • 第 16 行:一个示例片段有一个标准名称:ExampleXXFragment,其中 XX 是它在示例选择器中的位置加 1。XX 也是该示例在片段管理器中的片段编号;
  • 第 21 行:从选择器中实例化示例 #i 的片段:
    • Class.forName(exampleName):将片段加载到内存中;
    • Class.forName(exampleName).getConstructors()[0]:获取该类第一个构造函数的引用。ExampleXXFragment 类仅有一个构造函数,因此将获取该构造函数的引用;
    • Class.forName(exampleName).getConstructors()[0].newInstance(new Object[]{}) 使用上一步获取的构造函数实例化一个 ExampleXXFragment 类型的对象。new Object[]{} 表示传递给该构造函数的参数。由于 ExampleXXFragment 类的构造函数不期望任何参数,因此传递了一个空对象数组;
  • 第 27 行:将此片段添加到片段数组中;
  • 第 30 行:我们看到片段管理器的构造函数 [SectionsPagerAdapter] 期望将要管理的片段数组作为参数。现在我们将该数组传递给构造函数;
  • 第 22 行:此处将与 [MainActivity] 关联的视图的片段容器 [mViewPager] 与片段管理器关联:片段容器 [mViewPager] 用于显示片段管理器中的片段;
  • 第 43 行:请阅读注释——该指令本质上规定,无论当前显示哪个片段,所有片段都必须保持代码设定的状态。因此当我们返回时,会发现它处于我们离开时的状态;
  • 第 45 行:片段容器 [mViewPager] 的类型为 [MyPager],这会禁用滑动操作;

[MainActivity.showView] 方法如下:


  // display view n° [position]
  private void showView(int position) {
    // refresh the fragment before displaying it
    mSectionsPagerAdapter.getItem(position).onRefresh();
    // displays the requested view - goes directly to the view (second parameter set to false)
    // without this parameter, the user defaults to the desired view, quickly displaying intermediate views - undesirable behavior
    mViewPager.setCurrentItem(position, false);
}
  • 第 3 行:我们希望显示编号为 #position 的片段;
  • 第 4 行:从片段管理器请求该片段,然后对其进行刷新。由于上次显示时会话可能已发生变化,因此该片段必须检查会话以确定是否需要更新;
  • 第 7 行:片段由 [ViewPager] 显示。由于它已与片段管理器关联,因此将显示片段 #[position]——即我们在第 4 行刚刚刷新过的那个;

最后,我们来看两个用于管理等待的方法:


  public void beginWaiting() {
    // gestion image d'attente
    loadingPanel.setVisibility(View.VISIBLE);
  }
 
  public void cancelWaiting() {
    // gestion image d'attente
    loadingPanel.setVisibility(View.INVISIBLE);
    // fin exécution
    session.setOnAir(false);
    session.setOperationStarted(false);
}

9.3.7.5. [RequestFragment] 片段

[RequestFragment] 类的定义如下:


package android.aleas.fragments;
 
import android.aleas.R;
import android.aleas.activity.Constants;
import android.aleas.activity.MainActivity;
import android.os.Bundle;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.*;
 
import java.net.URI;
import java.net.URISyntaxException;
 
public class RequestFragment extends MyFragment {
 
  // URL of the web service
  private EditText edtUrlServiceRest;
  private TextView txtMsgErreurUrlServiceWeb;
  // number of requests
  private EditText edtNbRequests;
  private TextView txtErrorRequests;
  // generation interval
  private EditText edtA;
  private EditText edtB;
  private TextView txtErrorIntervalle;
  // delay
  private EditText edtMinDelay;
  private EditText edtMaxDelay;
  private TextView txtErrorDelay;
  // number of values generated
  private EditText edtMinCount;
  private EditText edtMaxCount;
  private TextView txtErrorCount;
  // button
  private Button btnExecuter;
  // list of answers
  private ListView listReponses;
  private TextView infoReponses;
  // spinner examples
  private Spinner spinnerExemples;
 
  // seizures
  private int nbRequests;
  private int a;
  private int b;
  private String urlServiceWebJson;
  private int minDelay;
  private int maxDelay;
  private int minCount;
  private int maxCount;
 
  // manufacturer
  public RequestFragment() {
    super();
    Log.d("rxjava", "RequestFragment constructor");
  }
 
  @Override
  public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
    Log.d("rxjava", "RequestFragment onCreateView");
    // recover activity and session
    activity = (MainActivity) getActivity();
    session = activity.getSession();
    // create the fragment view from its definition XML
    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);
    txtErrorIntervalle = (TextView) view.findViewById(R.id.txt_errorIntervalle);
    edtMinDelay = (EditText) view.findViewById(R.id.edt_minDelay);
    edtMaxDelay = (EditText) view.findViewById(R.id.edt_maxDelay);
    txtErrorDelay = (TextView) view.findViewById(R.id.txt_error_delay);
    edtMinCount = (EditText) view.findViewById(R.id.edt_minCount);
    edtMaxCount = (EditText) view.findViewById(R.id.edt_maxCount);
    txtErrorCount = (TextView) view.findViewById(R.id.txt_error_count);
    btnExecuter = (Button) view.findViewById(R.id.btn_Executer);
    listReponses = (ListView) view.findViewById(R.id.lst_reponses);
    infoReponses = (TextView) view.findViewById(R.id.txt_Reponses);
    spinnerExemples = (Spinner) view.findViewById(R.id.spinnerExemples);
 
    // execute] button
    btnExecuter.setVisibility(View.VISIBLE);
    btnExecuter.setOnClickListener(new View.OnClickListener() {
      public void onClick(View arg0) {
        doExecuter();
      }
    });
 
    // initially no error messages
    txtErrorRequests.setVisibility(View.INVISIBLE);
    txtErrorIntervalle.setVisibility(View.INVISIBLE);
    txtMsgErreurUrlServiceWeb.setVisibility(View.INVISIBLE);
    txtErrorCount.setVisibility(View.INVISIBLE);
    txtErrorDelay.setVisibility(View.INVISIBLE);
    // spinner examples
    spinnerExemples.setAdapter(session.getSpinnerExemplesAdapter());
    // result
    return view;
  }
...
}
  • 第 16 行:[RequestFragment] 类继承自 [MyFragment] 类(参见第 9.3.7.1 节);
  • 第 18–42 行:片段的视觉组件(参见第 9.3.7.2 节);
  • 第 45–52 行:表单中的用户输入;
  • 当 [MainActivity] 活动创建应用程序中的所有片段时,构造函数(第 55–58 行)和 [onCreateView] 方法会被执行。此过程仅发生一次;
  • 第 61 行:[onCreateView] 方法的代码是标准的。请注意第 102 行,示例中的下拉列表适配器是从会话中获取的。另请注意第 91 行,点击 [执行] 按钮的操作由 [doExecute] 方法处理;
  • 第 64–65 行:[activity] 和 [session] 字段属于父类 [MyFragment];

[doExecute] 方法如下:


  // seizures
  private int nbRequests;
  private int a;
  private int b;
  private String urlServiceWebJson;
  private int minDelay;
  private int maxDelay;
  private int minCount;
  private int maxCount;
 
...
 
  private void doExecuter() {
    // valid entries?
    if (isPageValid()) {
      // we put info in session
      session.setInfos(nbRequests, a, b, minCount, maxCount, minDelay, maxDelay, urlServiceWebJson, spinnerExemples.getSelectedItem().toString(), spinnerExemples.getSelectedItemPosition() + 1);
      // store the URL of the web service
      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 started
      session.setOperationStarted(false);
      // the answer fragment is displayed
      activity.selectTab(Constants.VUE_RESPONSE);
      // we start waiting
      beginWaiting();
    }
}
  • 第 15 行:我们不再对 [ispageValid] 方法进行说明。该方法用于检查条目的有效性,仅当所有条目均有效时才返回 true。在此情况下,这些条目将用于初始化第 2 至 9 行中的字段;
  • 第 17 行:将各项输入保存到会话中:
    • [spinnerExemples.getSelectedItem().toString()] 是用户选中的示例名称,并存储在 [session.exampleName] 中;
    • [spinnerExemples.getSelectedItemPosition() + 1] 是与该示例关联的片段 ID,该片段已由片段管理器存储。此 ID 存储在 [session.examplePosition] 中;
  • 第 19 行:Web 服务 / JSON 的 URL 被传递给 Activity,该 Activity 随后将其传递给 [DAO] 层;
  • 第 21–24 行:请注意,一项操作即将开始;
  • 第 26 行:将显示响应标签页。要理解后续发生的情况,请回顾代码 [MainActivity.selectTab]:

  // sélection d'un onglet
  public void selectTab(int position) {
    // il y a au plus 2 onglets
    // au départ il n'y en a qu'un, celui de la requête
    // si l'onglet demandé est le n° 1 et que celui-ci n'existe pas encore, alors il faut le créer
    if (position == 1 && tabLayout.getTabCount() == 1) {
      // 1 onglet de +
      TabLayout.Tab tab = tabLayout.newTab();
      tab.setText("Response");
      tabLayout.addTab(tab);
    }
    // on sélectionne par programme l'onglet, ce qui va déclencher l'événement [onTabSelected]
    // qui va associer la bonne vue à cet onglet
    tabLayout.getTabAt(position).select();
}
  • 最初,该 Activity 仅创建了请求标签页(标签页 #0);
  • 第 6–11 行:如果响应标签页(标签 #1)尚未创建,则创建它;
  • 第14行:我们选择标签页编号(0或1)。这会将[onTabSelected]事件放入Android应用事件循环的队列中;

[MainActivity] 中 [onTabSelected] 事件的处理程序如下:


      @Override
      public void onTabSelected(TabLayout.Tab tab) {
        // un onglet a été sélectionné - on change le fragment affiché par le conteneur de fragments
        int position = tab.getPosition();
        if (position == 0) {
          // onglet requête
          showView(0);
        } else {
          // onglet réponse - dépend de l'exemple choisi
          showView(session.getExamplePosition());
        }
}

对于 [Response] 选项卡,将执行第 9 行代码。ID 为 [session.getExamplePosition()] 的片段将被显示。例如,对于 [example-03],存储在 [session.examplePosition] 中的 ID 为 3。 随后第 10 行将显示 ID 为 3 的片段。该 Activity 最初创建的片段数组为 [RequestFragment, Example01Fragment, Example02Fragment, Example03Fragment,..]。因此,实际显示的确实是 [Example03Fragment]。其显示逻辑由以下代码实现:


  // display view n° [position]
  private void showView(int position) {
    // refresh the fragment before displaying it
    mSectionsPagerAdapter.getItem(position).onRefresh();
    // displays the requested view - goes directly to the view (second parameter set to false)
    // without this parameter, the user defaults to the desired view, quickly displaying intermediate views - undesirable behavior
    mViewPager.setCurrentItem(position, false);
}

我们可以看到,片段在显示(第 7 行)之前会先被刷新(第 4 行)。

9.3.7.6. [ResponseFragment] 片段

[ResponseFragment] 类用于显示来自服务器的响应。其代码如下:


package android.aleas.fragments;
 
import android.aleas.R;
import android.aleas.activity.MainActivity;
import android.os.Bundle;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.ListView;
import android.widget.TextView;
import org.codehaus.jackson.map.ObjectMapper;
import rx.Subscription;
 
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
 
public abstract class ResponseFragment extends MyFragment {
 
  // list of answers
  private ListView listReponses;
  private TextView infoReponses;
  // button
  private Button btnAnnuler;
 
  // mapper jSON
  private ObjectMapper mapper;
 
  protected ResponseFragment() {
    super();
    Log.d("rxjava", String.format("ResponseFragment (%s) constructor", this));
    mapper = new ObjectMapper();
  }
 
  @Override
  public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
    // recover 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 definition XML
    View view = inflater.inflate(R.layout.response, container, false);
    // components
    listReponses = (ListView) view.findViewById(R.id.lst_reponses);
    infoReponses = (TextView) view.findViewById(R.id.txt_Reponses);
    btnAnnuler = (Button) view.findViewById(R.id.btn_Annuler);
    // cancel] button
    btnAnnuler.setVisibility(View.INVISIBLE);
    btnAnnuler.setOnClickListener(new View.OnClickListener() {
      public void onClick(View arg0) {
        doAnnuler();
      }
    });
    // result
    return view;
  }
...
  // method to be executed (by explicit code) before each fragment display
  public void onRefresh() {
...
  }
}
  • 第 21 行:[ResponseFragment] 类继承自 [MyFragment] 类;
  • 第 23–27 行:片段的组件;
  • 第 32–36 行:构造函数仅在 Activity 初始创建示例片段时执行一次。这是因为所有示例片段都继承自 [ResponseFragment] 片段。当它们被实例化时,会调用其父类 [ResponseFragment] 的构造函数;
  • 第 35 行:初始化第 30 行定义的 JSON 映射器,用于显示异常堆栈的 JSON 字符串;
  • 第 38–59 行:[onCreateView] 方法仅在 Activity 初始创建示例片段时执行一次。其中包含 Android 应用程序中的标准代码;
  • 第 52–56 行:点击 [Cancel] 按钮时执行的方法是 [doCancel] 方法;
  • 第 62–64 行:每次显示 [Response] 选项卡时,都会执行 [onRefresh] 方法;

得益于关键方法中设置的各种日志,我们可以观察到应用启动时发生的情况:

05-17 08:45:05.803 14158-14158/android.aleas D/rxjava: RequestFragment constructor
05-17 08:45:05.804 14158-14158/android.aleas D/rxjava: ResponseFragment (Example01Fragment{c6fd1a7}) constructor
05-17 08:45:05.804 14158-14158/android.aleas D/rxjava: Example01Fragment constructor
05-17 08:45:05.810 14158-14158/android.aleas D/rxjava: ResponseFragment (Example02Fragment{ba75654}) constructor
05-17 08:45:05.810 14158-14158/android.aleas D/rxjava: Example02Fragment constructor
05-17 08:45:05.810 14158-14158/android.aleas D/rxjava: ResponseFragment (Example03Fragment{b8589fd}) constructor
05-17 08:45:05.810 14158-14158/android.aleas D/rxjava: Example03Fragment constructor
05-17 08:45:05.810 14158-14158/android.aleas D/rxjava: ResponseFragment (Example04Fragment{e9506f2}) constructor
05-17 08:45:05.810 14158-14158/android.aleas D/rxjava: Example04Fragment constructor
05-17 08:45:05.934 14158-14158/android.aleas D/rxjava: RequestFragment onCreateView
05-17 08:45:05.962 14158-14158/android.aleas D/rxjava: ResponseFragment (Example01Fragment{c6fd1a7 #1 id=0x7f0d006e android:switcher:2131558510:1}) onCreateView
05-17 08:45:05.969 14158-14158/android.aleas D/rxjava: ResponseFragment (Example02Fragment{ba75654 #2 id=0x7f0d006e android:switcher:2131558510:2}) onCreateView
05-17 08:45:05.972 14158-14158/android.aleas D/rxjava: ResponseFragment (Example03Fragment{b8589fd #3 id=0x7f0d006e android:switcher:2131558510:3}) onCreateView
05-17 08:45:05.978 14158-14158/android.aleas D/rxjava: ResponseFragment (Example04Fragment{e9506f2 #4 id=0x7f0d006e android:switcher:2131558510:4}) onCreateView
  • 第 1 行:创建片段 [RequestFragment];
  • 第 2–9 行:构建应用程序中 4 个示例的片段;
  • 第 10 行:初始化 [RequestFragment] 片段;
  • 第11–14行:初始化应用程序中4个示例的片段;

此后,我们再也没有看到对这些方法的调用。

[ResponseFragment.onRefresh] 方法如下:


  // méthode à exécuter (par code explicite) avant chaque visualisation du fragment
  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()));
    // exécution en cours ?
    if (session.isOnAir() && !session.isOperationStarted()) {
      // exécution requête
      session.setOperationStarted(true);
      doExecuter();
    }
}
  • 第 5 行:我们检查 [RequestFragment] 是否已发起请求(session.isOnAir)以及是否已启动(isOperationStarted)。如果 [RequestFragment] 已发起请求且尚未运行,则启动该操作(第 7–8 行);
  • 一旦操作启动,由于其为异步操作,用户可在两个标签页之间切换。若用户切回 [Response] 标签页时有操作正在进行,则第 7–8 行不会被执行;

第 8 行中的 [doExecute] 方法将执行用户请求的操作:


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

// method implemented by child classes
protected abstract void createAndExecuteObservables();
  • 第 10 行:创建、执行并观察可观察对象。这些操作在每个示例中各不相同。这就是为什么 [createAndExecuteObservables] 方法是抽象的(第 14 行)。它将由继承 [ResponseFragment] 类的 [ExampleXXFragment] 片段来实现;
  • 第 6 行:清空订阅列表;
  • 第 7 行:清空显示响应的列表;
  • 第 8 行:统计已接收的响应数量;

子类 [ExampleXXFragment] 将显示其所观察元素的任务委托给以下 [showAlea] 方法:


  protected void showAlea(String data) {
    // one more piece of information
    nbInfos++;
    infoReponses.setText(String.format("Liste des réponses (%s)", nbInfos));
    // 1 more answer
    reponses.add(0, data);
    Log.d("rxjava", data);
    // maj of UI
    listReponses.setAdapter(new ArrayAdapter<String>(getActivity(), android.R.layout.simple_list_item_1, android.R.id.text1, reponses));
}
  • 第 1 行:我们可以看到,被监视的元素以字符串形式传入。这实际上是该元素的 JSON 字符串。这使得我们可以使用单一方法来显示被监视的元素,而无需考虑其具体的 Java 类型;
  • 第 6 行:被监听的 [data] 元素被添加到响应列表的首位。因此,用户会在列表顶部看到最新的响应;

等待操作由以下 [beginWaiting] 和 [cancelWaiting] 方法管理:


  private void beginWaiting() {
    // we set the hourglass
    activity.beginWaiting();
    // the [Cancel] button is displayed
    btnAnnuler.setVisibility(View.VISIBLE);
  }
 
  protected void cancelWaiting() {
    // end of wait
    activity.cancelWaiting();
    // the [Cancel] button is hidden
    btnAnnuler.setVisibility(View.INVISIBLE);
}

它们在 Activity 中调用同名的方法,仅用于显示或隐藏 [取消] 按钮。

点击 [取消] 按钮由以下代码处理:


  protected void doAnnuler() {
    // on annule tous les abonnements
    for (Subscription s : subscriptions) {
      if (!s.isUnsubscribed()) {
        s.unsubscribe();
      }
    }
    // fin de l'attente
    cancelWaiting();
}
  • 第3–7行:依次取消所有订阅;

9.3.8. 可观察对象的示例

9.3.8.1. 示例-01

[ExampleXXFragment] 类旨在创建、执行和监听可观察对象。监听到的值由父类 [ResponseFragment] 进行显示。

[Example01Fragment] 类如下所示:

  

package android.aleas.exemples;
 
import android.aleas.dao.AleasDaoResponse;
import android.aleas.fragments.AleasUiResponse;
import android.aleas.fragments.Request;
import android.aleas.fragments.ResponseFragment;
import android.util.Log;
import org.codehaus.jackson.map.ObjectMapper;
import org.codehaus.jackson.map.ser.impl.SimpleBeanPropertyFilter;
import org.codehaus.jackson.map.ser.impl.SimpleFilterProvider;
import rx.Observable;
import rx.android.schedulers.AndroidSchedulers;
import rx.functions.Action0;
import rx.functions.Action1;
import rx.schedulers.Schedulers;
 
import java.io.IOException;
 
public class Example01Fragment extends ResponseFragment {
 
    // mappers jSON
    private ObjectMapper mapperAleasUiResponse;
 
    // manufacturer
    public Example01Fragment() {
        super();
        Log.d("rxjava", "Example01Fragment constructor");
        // filters jSON
        mapperAleasUiResponse = new ObjectMapper();
    }
 
    @Override
    public void createAndExecuteObservables() {
        Log.d("rxjava", "Example01Fragment createAndExecuteObservables");
        // we ask for the random numbers
        Observable<AleasDaoResponse> observable = Observable.empty();
        for (int i = 0; i < session.getNbRequests(); i++) {
            // observable configuration n° i
            // request to server
            Request request = session.getRequest();
            request.setId(i);
            // observable executed on computation thread
            observable = observable.mergeWith(session.getActivity().getAleas(request).subscribeOn(Schedulers.io()));
        }
        // observation on event loop thread;
        observable = observable.observeOn(AndroidSchedulers.mainThread());
        // we 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 information 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;
    }
}
  • 第 36 行:将生成的单个可观测量;
  • 第 37–44 行:生成并配置各种可观察对象,这些对象将在第 43 行合并到第 36 行的可观察对象中;
  • 第 43 行:该可观察对象在调度器 [Schedulers.io()] 的一个线程中执行。对服务器的 HTTP 请求将在该线程中执行;
  • 第 46 行:最终的可观察对象在事件循环线程上被观察;
  • 第 48–57 行:可观察对象的执行,即向随机数服务器发送请求。Android 目前尚不支持 Java 8 及其 lambda 表达式。因此,此处使用匿名类来实例化 RxJava 的函数式接口;
  • 第 49–52 行:当观察者从可观察对象接收到 [AleasDaoResponse] 类型的新元素时执行的操作(参见第 9.3.6.1 节);
  • 第 51 行:调用父类的 [showAlea] 方法。请注意,该方法期望接收一个字符串。该字符串由第 59–68 行中的 [getDataFrom] 方法提供;
  • 第 63 行:我们按如下方式返回类型为 [AleasUiResponse] 的 JSON 字符串:

package android.aleas.fragments;
 
import android.aleas.dao.AleasDaoResponse;
 
import java.text.SimpleDateFormat;
import java.util.Calendar;
 
public class AleasUiResponse {
 
  // answer [DAO]
  private AleasDaoResponse aleasDaoResponse;
  // observation thread
  private String observedOn;
  // observation time
  private String observedAt;
 
  // manufacturers
  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
...
}
  • 在 [DAO] 层的响应(第 11 行)中,我们添加了两项信息:
    • 第 13 行:观测线程;
    • 第 15 行:观测时间;

让我们回到订阅代码:


    @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) {
                // exception is displayed
                showAlea(getMessagesFromThrowable(th));
                // after receiving an exception, the observable receives neither onNext nor onCompleted
                // forced to cancel the subscription by hand
                doAnnuler();
            }
        }, new Action0() {
            @Override
            public void call() {
                // end waiting
                cancelWaiting();
            }
        }));
}
  • 第 11–18 行:观察者接收到异常的情况;
  • 第 14 行:我们再次使用父类的 [showAlea] 方法来显示异常。[getMessagesFromThrowable] 方法是父类 [ResponseFragment] 的一个方法,它能根据异常生成字符串:

  // messages d'une exception
  protected String getMessagesFromThrowable(Throwable ex) {
    // on crée une liste avec les msg d'erreur de la pile d'exceptions
    List<String> messages = new ArrayList<String>();
    Throwable th = ex;
    while (th != null) {
      messages.add(String.format("[%s, %s]", th.getClass().getName(), th.getMessage()));
      th = th.getCause();
    }
    try {
      return mapper.writeValueAsString(messages);
    } catch (IOException e) {
      return e.getMessage();
    }
}
  • 第 11 行:返回错误消息列表的 JSON 字符串(第 4 行);

让我们回到可观察对象的订阅代码:

  • 第 19–25 行:当观察者收到发射结束通知时执行的代码。随后我们取消等待(第 23 行),从而更新 GUI;

运行示例 01 会产生类似于以下的输出:

Image

列表中的每个元素都是一个被观测值的 JSON 字符串。该 JSON 字符串的字段如下:

  • aleas:服务器提供的随机数列表;
  • idClient:请求编号(可以看到响应返回的顺序是零散的);
  • on:发出此值的可观察对象的执行线程;
  • requestAt:客户端请求的时间;
  • responseAt:服务器响应的时间;
  • delay:服务器观察到的延迟;
  • error:服务器返回的错误代码(0 表示无错误);
  • message:服务器返回的错误信息(null=无错误);
  • observedAt:观测到该值的时间;
  • observedOn:观察该观测值的主线程;

9.3.8.2. 示例-02

[Example02Fragment] 类的定义如下:


package android.aleas.exemples;
 
import android.aleas.dao.AleasDaoResponse;
import android.aleas.fragments.AleasUiResponse;
import android.aleas.fragments.Request;
import android.aleas.fragments.ResponseFragment;
import android.util.Log;
import org.codehaus.jackson.map.ObjectMapper;
import rx.Observable;
import rx.android.schedulers.AndroidSchedulers;
import rx.functions.Action0;
import rx.functions.Action1;
import rx.functions.Func1;
import rx.schedulers.Schedulers;
 
import java.io.IOException;
 
public class Example02Fragment extends ResponseFragment {

    // mappers jSON
    private ObjectMapper mapperAleasUiResponse;
 
    // manufacturer
    public Example02Fragment() {
        super();
        Log.d("rxjava", "Example02Fragment constructor");
        // filter jSON
        mapperAleasUiResponse = new ObjectMapper();
    }
 
    public void createAndExecuteObservables() {
        Log.d("rxjava", "Example02Fragment createAndExecuteObservables");
        // we ask for the random numbers
        Observable<AleasDaoResponse> observable = Observable.empty();
        for (int i = 0; i < session.getNbRequests(); i++) {
            // request preparation
            Request request = session.getRequest();
            request.setId(i);
            // only observables with an even customer number are kept
            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()));
        }
        // observation on event loop thread
        observable = observable.observeOn(AndroidSchedulers.mainThread());
        // these observables are executed
        subscriptions.add(observable.subscribe(new Action1<AleasDaoResponse>() {
            @Override
            public void call(AleasDaoResponse aleasDaoResponse) {
                showAlea(getDataFrom(aleasDaoResponse));
            }
        }, new Action1<Throwable>() {
            @Override
            public void call(Throwable th) {
                showAlea(getMessagesFromThrowable(th));
                doAnnuler();
            }
        }, new Action0() {
            @Override
            public void call() {
                // end waiting
                cancelWaiting();
            }
        }));
 
    }
 
    private String getDataFrom(AleasDaoResponse aleasDaoResponse) {
        // extract the information 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;
    }
 
}

此示例与前一个示例(第 38 行)类似。不过,从前一个示例中获得的可观测量中,我们仅保留客户编号为偶数的那些(第 42–46 行),并使用 [filter] 方法(第 41 行)。

所得结果如下(针对10个请求):

Image

9.3.8.3. 示例-03

[Example03Fragment] 类的定义如下:


package android.aleas.exemples;
 
import android.aleas.dao.AleasDaoResponse;
import android.aleas.fragments.Request;
import android.aleas.fragments.ResponseFragment;
import android.util.Log;
import org.codehaus.jackson.map.ObjectMapper;
import rx.Observable;
import rx.android.schedulers.AndroidSchedulers;
import rx.functions.Action0;
import rx.functions.Action1;
import rx.functions.Func1;
import rx.schedulers.Schedulers;
 
import java.io.IOException;
import java.util.List;
 
public class Example03Fragment extends ResponseFragment {
 
  // mappers jSON
  private ObjectMapper mapper;
 
  // manufacturer
  public Example03Fragment() {
    super();
    Log.d("rxjava", "Example03Fragment constructor");
    // filter jSON
    mapper = new ObjectMapper();
  }
 
  public void createAndExecuteObservables() {
    Log.d("rxjava", "Example03Fragment createAndExecuteObservables");
    // we ask for the random numbers
    Observable<List<Integer>> observable = Observable.empty();
    for (int i = 0; i < session.getNbRequests(); i++) {
      // request preparation
      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();
        }
      })
        // execution on I/O thread
        .subscribeOn(Schedulers.io()));
    }
    // observation on event loop thread
    observable = observable.observeOn(AndroidSchedulers.mainThread());
    // these observables are executed
    subscriptions.add(observable
      .subscribe(new Action1<List<Integer>>() {
                   @Override
                   public void call(List<Integer> aleas) {
                     showAlea(getDataFrom(aleas));
                   }
                 },
        new Action1<Throwable>() {
          @Override
          public void call(Throwable th) {
            showAlea(getMessagesFromThrowable(th));
            doAnnuler();
          }
        },
        new Action0() {
          @Override
          public void call() {
            // end waiting
            cancelWaiting();
          }
        }
      ));
 
  }
 
  private String getDataFrom(List<Integer> aleas) {
    // extract the information 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;
  }
 
}

此示例与示例-02类似:

  • 第 40 行:我们定义了与示例-02中相同的可观察对象;
  • 第 45 行:使用 [map] 方法将前面的可观察对象发出的每个值转换为 List<Integer> 类型,即服务器生成的随机数列表;
  • 第 58 行:被观察的值现在是 List<Integer> 类型;

对 10 次请求获得的结果如下:

Image

9.3.8.4. 示例-04

[Example04Fragment] 类的定义如下:


package android.aleas.exemples;
 
import android.aleas.dao.AleasDaoResponse;
import android.aleas.fragments.Request;
import android.aleas.fragments.ResponseFragment;
import android.util.Log;
import org.codehaus.jackson.map.ObjectMapper;
import rx.Observable;
import rx.android.schedulers.AndroidSchedulers;
import rx.functions.Action0;
import rx.functions.Action1;
import rx.functions.Func1;
import rx.schedulers.Schedulers;
 
public class Example04Fragment extends ResponseFragment {
 
  // mappers jSON
  private ObjectMapper mapper;
 
  // manufacturer
  public Example04Fragment() {
    super();
    Log.d("rxjava", "Example04Fragment constructor");
    // filter jSON
    mapper = new ObjectMapper();
  }
 
  public void createAndExecuteObservables() {
    Log.d("rxjava", "Example03Fragment createAndExecuteObservables");
    // we ask for the random numbers
    Observable<Integer> observable = Observable.empty();
    for (int i = 0; i < session.getNbRequests(); i++) {
      // request preparation
      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;
        }
      }).flatMap(new Func1<AleasDaoResponse, Observable<Integer>>() {
        @Override
        public Observable<Integer> call(AleasDaoResponse aleasDaoResponse) {
          return Observable.from(aleasDaoResponse.getAleas());
        }
      })
        // execution on an I/O thread
        .subscribeOn(Schedulers.io()));
    }
    // observation on event loop thread
    observable = observable.observeOn(AndroidSchedulers.mainThread());
    // these observables are executed
    subscriptions.add(observable
      .subscribe(new Action1<Integer>() {
                   @Override
                   public void call(Integer alea) {
                     showAlea(String.valueOf(alea));
                   }
                 },
        new Action1<Throwable>() {
          @Override
          public void call(Throwable th) {
            showAlea(getMessagesFromThrowable(th));
            doAnnuler();
          }
        },
        new Action0() {
          @Override
          public void call() {
            // end waiting
            cancelWaiting();
          }
        }
      ));
 
  }
}

此示例与示例-03类似,不同之处在于第42行不再使用 [map] 方法,而是使用了 [flatMap] 方法。

  • 第 55 行:请注意,被观察值的类型现在是 Integer

针对 10 次请求,我们得到以下结果:

Image

这次,观测值的数量多于请求的数量。

9.3.8.5. 示例-05

接下来,我们将概述向应用程序添加新的可观测对象示例的步骤。

假设我们要重现 7.6.4 节中的示例 [Example22h]:


package dvp.rxjava.observables.exemples;
 
import dvp.rxjava.observables.utils.Process;
import dvp.rxjava.observables.utils.ProcessUtils;
import rx.Observable;
import rx.observables.GroupedObservable;
 
public class Exemple22h {
    public static void main(String[] args) throws InterruptedException {
        // 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);
    }
}
  • 可观察对象 [Observable.range(1, 10)] 的值首先通过 [groupBy] 方法(第 11 行)按偶数和奇数分组,然后通过 [concatMap] 方法(第 12 行)合并为一个可观察对象;

步骤 1

我们在文件 [examples.xml] 中创建一个新示例:

  

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

上文已添加第 8 行。示例的名称可以是任意内容。

步骤 2

将 [Example04Fragment] 类复制为 [Example05Fragment]。此处的名称是固定的。

步骤 3

按以下方式修改 [Example05Fragment] 中的代码:


package android.aleas.exemples;
 
import android.aleas.dao.AleasDaoResponse;
import android.aleas.fragments.Request;
import android.aleas.fragments.ResponseFragment;
import android.util.Log;
import org.codehaus.jackson.map.ObjectMapper;
import rx.Observable;
import rx.android.schedulers.AndroidSchedulers;
import rx.functions.Action0;
import rx.functions.Action1;
import rx.functions.Func1;
import rx.observables.GroupedObservable;
import rx.schedulers.Schedulers;
 
public class Example05Fragment extends ResponseFragment {
 
  // mappers jSON
  private ObjectMapper mapper;
 
  // manufacturer
  public Example05Fragment() {
    super();
    Log.d("rxjava", "Example05Fragment constructor");
    // filter jSON
    mapper = new ObjectMapper();
  }
 
  public void createAndExecuteObservables() {
    Log.d("rxjava", "Example05Fragment createAndExecuteObservables");
    // instantiations 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();
      }
    };
    // we ask for the random numbers
    Observable<Integer> observable = Observable.empty();
    for (int i = 0; i < session.getNbRequests(); i++) {
      // request preparation
      Request request = session.getRequest();
      request.setId(i);
      // observable configuration
      observable = observable.mergeWith(session.getActivity().getAleas(request).filter(filter).flatMap(flatMap))
        .groupBy(groupBy).concatMap(concatMap)
        // execution on an I/O thread
        .subscribeOn(Schedulers.io());
    }
    // observation on event loop thread
    observable = observable.observeOn(AndroidSchedulers.mainThread());
    // these observables are executed
    subscriptions.add(observable
      .subscribe(new Action1<Integer>() {
                   @Override
                   public void call(Integer alea) {
                     showAlea(String.valueOf(alea));
                   }
                 },
        new Action1<Throwable>() {
          @Override
          public void call(Throwable th) {
            showAlea(getMessagesFromThrowable(th));
            doAnnuler();
          }
        },
        new Action0() {
          @Override
          public void call() {
            // end waiting
            cancelWaiting();
          }
        }
      ));
 
  }
}
  • 第 67 行:表示示例 04 中的可观察对象:一个整数流;
  • 第 68 行:我们将根据一个待定义的布尔条件对该整数流进行分组。我们将得到一个类型为 Observable<GroupedObservable<Boolean, Integer>> 的可观察对象因此它会发出类型为 GroupedObservable<Boolean, Integer> 的元素;
  • 第 68 行:[concatMap] 方法将从 GroupedObservable<Boolean, Integer> 类型的元素中生成 Integer 类型的元素;
  • 第 32–59 行:为了使第 67–69 行中可观察对象的创建更易于阅读,我们已将各种操作符 [filter, flatMap, groupBy, concatMap] 所需的函数接口实例进行了独立提取;
  • 第 47–52 行:[groupBy] 方法期望一个类型为 Func1<T,K> 的参数,其中 T 是分组元素的类型,K 是分组标准的类型。对于给定元素 T,Func1<T,K> 实例负责为该元素生成分组键 K;
  • 第 48–51 行:类型为 Integer 的元素将按奇偶性分组。Func1<Integer,Boolean> 实例会根据元素应归入哪一组,生成 true false 作为键。结果是两个组:键为 true 的偶数元素组和键为 false 的奇数元素组;
  • 第 53–59 行:[concatMap] 方法期望接收类型为 Func1<T, Observable<R>> 的参数,并生成一个包含类型 R 元素的可观察序列。此处的类型 T 是 [groupBy] 运算符输出的类型,在本例中为 GroupedObservable<Boolean, Integer>
  • 第 57 行:从类型为 [GroupedObservable<Boolean, Integer>] 的元素出发,我们生成一个类型为 Observable<Integer> 的可观察序列。由于 [groupBy] 运算符生成了两个组,因此 [concatMap] 运算符将生成两个类型为 [Observable<Integer>] 的可观察序列。与 [flatMap] 类似,它会将它们扁平化为一个单一的可观察序列。 但与 [flatMap] 不同的是,它不会混合扁平化后可观察对象中的元素。因此,我们应该观察到两个独立的组:偶数随机数和奇数随机数。

步骤 4

我们运行该应用程序:

Image

并得到以下结果:

Image

  • [1] 中为偶数;[2] 中为奇数;

9.3.8.6. 继续

现在邀请读者创建自己的示例,并尝试为配置发送给随机数服务器的请求的输入形式设置各种数值。

9.3.9. 结论

我们在 Android 环境中构建了以下架构:

Android客户端:

[DAO] 层与生成 Android 平板电脑上显示的随机数的服务器进行通信。该服务器采用以下两层架构:

[DAO] 层向随机数服务器发出了 n 次 HTTP 请求,而 [swing] 层则异步等待这些请求的结果以进行显示。这 n 次 HTTP 请求均发送至同一服务器,且服务器返回了相同类型的响应。这使得我们能够将这些响应合并为一个可观察对象。

实际上,Android 应用程序通常会与不同的服务器进行通信,而我们通常不会合并这些服务器的响应。针对这些服务器的 HTTP 请求将独立处理,其结果也将通过各自独立的方法进行观察。