Skip to content

9. RxJava 在 Android 环境中

9.1. Introduction

这里我们将重新探讨一个已在多份文档中讨论过的应用程序:

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

该应用属于客户端/服务器架构,其中服务器以异步方式提供随机数,由 Android 客户端进行显示:

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

Android客户端代码如下:

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

客户端向 [web / JSON] 层中的某些 URL 发送查询,并收到 JSON 格式的文本响应(JavaScript 对象表示法)。

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

Web 服务器 / jSON

  • 其 [métier] 层;
  • 其基于 Spring 实现的 [web / JSON] 服务;

Android客户端

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

9.2. Web 服务 / jSON

:Web 服务 / jSON 由 Spring 技术实现 MVC。不熟悉该技术的读者可以:

  • 仅阅读第 9.2.1 节,该节说明了如何启动服务器以及如何查询服务器;
  • 查阅文档 [Spring MVC et Thymeleaf par l'exemple],特别是第 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服务中唯一的URL / 通过操作HTTP GET可获取的jSON。其参数如下:
    • [a,b]:随机数生成间隔;
    • [minCount, maxCount]:生成的随机数个数,其中 count 表示生成的随机数个数,count 表示每个随机数在 [minCount, maxCount] 区间内的取值;
    • [minDelay, maxDelay]:服务等待 delay 毫秒后返回请求的随机数,其中 delay 是 [minDelay, maxDelay] 区间内的一个随机数;

在浏览器中,请求这个 URL:

 

我们请求的是:

  • [100, 200] 区间内的随机数;
  • n 包含 n 的随机数,取值范围为 [10, 20];
  • 等待时间 x 毫秒,且 x 位于区间 [300, 400] 内;

在响应中:

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

9.2.2. 项目的 Gradle 依赖项

  

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


// 由http://start.spring.io/生成(2016年5月)
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]会推断出一个合理或可能的配置。 因此,如果项目 Classpath 中包含 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 应用时将使用该 Tomcat 服务器。 请注意,此处未指定依赖项的版本。将使用导入项目 [spring-boot] 中指定的版本;

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

让我们查看 [4] 文件引入的依赖项 [build.gradle]:

 

这些依赖项非常多。Spring Boot for Web 已包含了 Spring Web 应用 MVC 可能需要的依赖项。这意味着其中某些依赖项可能并不必要。Spring Boot 非常适合用于教程:

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

在 Spring 生态系统网站上可以找到许多使用 Spring Boot 的示例 [http://spring.io/guides]。现在我们已经了解了项目的依赖项,可以开始编写代码了。

9.2.3. [métier] 层

  

[métier] 层将具有以下 [IMetier] 接口:


package dvp.rxjava.server.metier;

public interface IMetier {
  // 在区间 [a,b] 内的随机数
  // 生成 n 个随机数,其中 n 本身是区间 [minCount, maxCount] 内的随机数
  // 在等待 delay 毫秒后生成这些数,
  // 其中 [delay] 本身是区间 [minDelay, maxDelay] 内的随机数
  public AleasMetier getAleas(int a, int b, int minCount, int maxCount, int minDelay, int maxDelay);
}

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


package dvp.rxjava.server.metier;

import java.util.List;

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

  // 构造函数
  public AleasMetier(){

  }

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

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

  // 获取器和设置器
...
}

实现 [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) {
    // 区间内的随机数 [a,b]
    // 生成 n 个随机数,其中 n 本身也是该区间内的随机数 [minCount, maxCount]
    // 在等待 delay 毫秒后生成这些数,
    // 其中 [delay] 本身是区间 [minDelay, maxDelay] 内的随机数

    // 一些验证
    List<String> messages = new ArrayList<>();
    int erreur = 0;
    if (a < 0) {
      messages.add("Le nombre a de l'intervalle [a,b] de génération doit être supérieur à 0");
      erreur |= 2;
    }
    if (a >= b) {
      messages.add("Dans l'intervalle [a,b] de génération, on doit avoir a< b");
      erreur |= 4;
    }
    if (minCount < 0) {
      messages.add("Le nombre min de l'intervalle [min,count] du nombre de valeurs générées doit être supérieur à 0");
      erreur |= 16;
    }
    if (minCount > maxCount) {
      messages.add("Dans l'intervalle [min,count] du nombre de valeurs générées, on doit avoir min<= max");
      erreur |= 32;
    }
    if (minDelay < 0) {
      messages.add("Le nombre min de l'intervalle [min,count] du délai d'attente doit être supérieur à 0");
      erreur |= 64;
    }
    if (minCount > maxCount) {
      messages.add("Dans l'intervalle [min,count] du délai d'attente, on doit avoir min<= max");
      erreur |= 128;
    }
    if (maxDelay > 5000) {
      messages.add("L'attente en millisecondes avant la génération des nombres doit être dans l'intervalle [0,5000]");
      erreur |= 256;
    }
    // 错误?
    if (!messages.isEmpty()) {
      throw new AleasException(String.join(" [---] ", messages), erreur);
    }
    // 随机数生成器
    Random random = new Random();
    // 等待?
    int delay = minDelay + random.nextInt(maxDelay - minDelay + 1);
    if (delay > 0) {
      try {
        Thread.sleep(delay);
      } catch (InterruptedException e) {
        String message = null;
        try {
          message = mapper.writeValueAsString(Arrays.asList(String.format("[%s : %s]", e.getClass().getName(), e.getMessage())));
        } catch (JsonProcessingException e1) {
          throw new AleasException(e1,512);
        }
        throw new AleasException(message, 1024);
      }
    }
    // 生成结果
    int count = minCount + random.nextInt(maxCount - minCount + 1);
    List<Integer> nombres = new ArrayList<Integer>();
    for (int i = 0; i < count; i++) {
      nombres.add(a + random.nextInt(b - a + 1));
    }
    // 返回结果
    return new AleasMetier(delay,nombres);
  }

}

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

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

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


package android.exemples.server.metier;

public class AleaException extends RuntimeException {

  // 错误代码
  private int code;

  // 构造函数
  public AleaException() {
  }

  public AleaException(String detailMessage, int code) {
    super(detailMessage);
    this.code = code;
  }

  public AleaException(Throwable throwable, int code) {
    super(throwable);
    this.code = code;
  }

  public AleaException(String detailMessage, Throwable throwable, int code) {
    super(detailMessage, throwable);
    this.code = code;
  }

  // 获取器和设置器

  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 “路由”到负责处理它的操作。这些操作是名为 [Contrôleurs] 的特定类的方法。此处的 MVC 的 C 部分是字符串 [Dispatcher Servlet, Contrôleur, Action]。 如果未配置任何操作来处理传入的 URL,则 [Dispatcher Servlet] Servlet 将返回响应,指出请求的 URL 未被找到 (404 错误 NOT FOUND);
  1. 处理
  • 所选操作可利用 parami 参数,这些参数由 [Dispatcher Servlet] Servlet 传递而来。这些参数可能来自多个来源:
    • URL 的路径 [/param1/param2/...],
    • 来自 URL 的 [p1=v1&p2=v2] 参数
    • 浏览器随请求发送的参数;
  • 在处理用户请求时,该操作可能需要 [metier] 和 [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 {

  // 错误代码
  private int erreur;
  // 错误消息
  private String message;

  // 构造函数
  public AleasResponse() {

  }

  public AleasResponse(int erreur, String message, AleasMetier aleasMetier) {
    super(aleasMetier);
    this.erreur = erreur;
    this.message = message;
  }
  // getter 和 setter

  public void setAleasMetier(AleasMetier aleasMetier) {
    this.setDelay(aleasMetier.getDelay());
    this.setAleas(aleasMetier.getAleas());
  }
...
}
  • 第 5 行:类 [AleasResponse] 继承自类 [AleasMetier],因此继承了其所有属性(别名、延迟);
  • 第 8 行:一个错误代码(若无错误则为 0);
  • 第10行:若为erreur!=0,则显示错误信息;若无错误,则显示null

[AleasController] 控制器如下:


package dvp.rxjava.server.web;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;

import dvp.rxjava.server.metier.AleasException;
import dvp.rxjava.server.metier.IMetier;

@Controller
public class AleasController {

    // 业务层
    @Autowired
    private IMetier metier;
    @Autowired
    private ObjectMapper mapper;

    // 随机数在 [a,b]
    // 生成 n 个数,其中 n 属于区间 [minCount, maxCount]
    // 在等待 delay 毫秒后生成这些数,
    // 其中 [delay] 是区间 [minDelay, maxDelay] 内的随机数
    @RequestMapping(value = "/{a}/{b}/{minCount}/{maxCount}/{minDelay}/{maxDelay}", method = RequestMethod.GET, produces = "application/json")
    @ResponseBody
    public String getAleas(@PathVariable("a") int a, @PathVariable("b") int b, @PathVariable("minCount") int minCount,
            @PathVariable("maxCount") int maxCount, @PathVariable("minDelay") int minDelay,
            @PathVariable("maxDelay") int maxDelay) throws JsonProcessingException {

        // 准备响应
        AleasResponse response = new AleasResponse();
        // 使用业务层生成随机数
        try {
            response.setAleasMetier(metier.getAleas(a, b, minCount, maxCount, minDelay, maxDelay));
        } catch (AleasException e) {
            // 错误情况(代码和消息)
            response.setErreur(e.getCode());
            response.setMessage(e.getMessage());
        }
        // 返回响应 jSON
        return mapper.writeValueAsString(response);
    }
}
  • 第 16 行:注解 [@Controller] 将类 [AleasController] 设为 Spring 单例。此外,它还表明该类包含用于处理 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的请求 (属性 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 行中的 getAleas 方法将使用参数 a=100(第 31 行)、 b=200(第31行),minCount=10(第31行), maxCount=20(第32行),minDelay=300(第32行),maxDelay=400(第33行);
  • 第39行:向[métier]层请求一组随机数。需注意,方法[metier].getAleas可能会抛出异常;
  • 第42-43行:错误处理;
  • 第 46 行:将类型为 [AleasResponse] 的响应以字符串 jSON 的形式返回;

9.2.5. Spring 项目配置

  

配置 Spring 有多种方式:

  • 使用 XML 文件;
  • 使用 Java 代码;
  • 同时结合上述两种方式;

我们选择使用 Java 代码来配置我们的 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 {
  // -------------------------------- 层配置 [web]
  @Autowired
  private ApplicationContext context;

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

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

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

  // 映射器 jSON
  @Bean
  public ObjectMapper jsonMapper() {
    return new ObjectMapper();
  }
}
  • 第 15 行:告知 Spring 在哪些包中查找待实例化的对象。它将找到两个:
    • 带有 [@Service] 注解的 [Metier] 类;
    • 带有 [@Controller] 注解的 [AleasController] 类;
  • 第 16 行:注解 [@EnableWebMvc] 会为 Spring 框架 MVC 自动生成配置;
  • 第19-20行:注入Spring上下文(Spring对象容器)。此注入是必要的,因为第22-26行的对象需要它;
  • Spring 配置文件可通过带注解的方法 [@Bean] 定义新的 Spring 对象。该方法的返回值即成为一个 Spring 对象;
  • 第22-26行:定义Spring框架的Servlet MVC,该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) {
    // 应用程序运行
    SpringApplication.run(Config.class, args);
  }

}
  • 第 6 行:类 [Application] 是一个可执行类(第 7-10 行);
  • 第 9 行:静态方法 [SpringApplication.run] 是 [spring Boot](第 4 行)的方法,该方法将启动应用程序。其第一个参数是配置该项目的 Java 类。 此处即为我们刚刚描述的 [Config] 类。第二个参数是传递给方法 [main](第 7 行)的参数数组。此处将不包含任何参数;

关于具体的执行过程,请读者参阅第 9.2.1 节。

9.3. Android客户端

注意:下文的 Android 项目较为复杂。它需要具备扎实的 Android 知识,相关内容可参考 [Introduction à la programmation de tablettes Android avec Android Studio ] 等资料

活动

视图

[DAO]

用户

服务器

客户端将包含两个组件:

  1. 一个 [Présentation] 层(视图+活动);
  2. 一个 [DAO] 层,该层面向我们之前研究过的 [web / JSON] 服务。

9.3.1. RxAndroid

为了与随机数服务器进行异步通信,Android客户端将使用RxAndroid库。该库将RxJava扩展到了Android平台。 与Swing应用程序的做法一样,我们将仅使用RxAndroid引入的一个扩展功能,即[AndroidSchedulers.mainThread()]调度程序。Android图形界面遵循与Swing界面相同的规则:

  • 事件处理在名为事件循环(event loop)或 UI 线程的单一线程中进行;
  • 当某个事件触发异步操作时,如果这些操作的结果需要用于更新用户界面,则必须在 UI 线程中获取这些结果;

Android客户端:

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

9.3.2. IntelliJ IDEA 项目

Android 项目名为 [client]:

我们将通过 [2] 来运行它。

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

  • 在 [3] 中,访问项目结构;
  • 在我的机器上,[4-5]、JDK 和 SDK 版本的 Android 均已安装。请注意,JDK 1.8 并非必需。 Android 不支持 Java 8 的某些功能,包括 lambda 表达式。因此,为了实例化函数式接口,我们将使用匿名类。此时,JDK 1.6 版本就足够了。然而,该项目在发布时已配置为使用 JDK 1.8 版本;

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


buildscript {
  repositories {
    mavenCentral()
    mavenLocal()
  }
  dependencies {
    // 替换为当前版本的 Android 插件
    classpath 'com.android.tools.build:gradle:1.5.0'
  }
}
apply plugin: 'com.android.application'
dependencies {
  compile 'com.android.support:appcompat-v7:23.1.1'
  compile 'com.android.support:design:23.1.1'
  compile fileTree(dir: 'libs', include: ['*.jar'])
  compile 'org.springframework.android:spring-android-rest-template:1.0.1.RELEASE'
  compile 'org.codehaus.jackson:jackson-mapper-asl:1.9.9'
  compile 'io.reactivex:rxandroid:1.1.0'
}
repositories {
  jcenter()
}
android {
  compileSdkVersion 23
  buildToolsVersion "23.0.3"
  defaultConfig {
    applicationId "android.aleas"
    minSdkVersion 15
    targetSdkVersion 23
    versionCode 1
    versionName "1.0"
  }
  buildTypes {
    release {
      minifyEnabled false
      proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
    }
  }
  compileOptions {
    sourceCompatibility JavaVersion.VERSION_1_6
    targetCompatibility JavaVersion.VERSION_1_6
  }
  packagingOptions {
    exclude 'META-INF/ASL2.0'
    exclude 'META-INF/NOTICE'
    exclude 'META-INF/LICENSE'
    exclude 'META-INF/NOTICE.txt'
    exclude 'META-INF/LICENSE.txt'
    exclude 'META-INF/notice.txt'
    exclude 'META-INF/license.txt'
  }
}

根据现有的 SDK Android 文件,第 8 行、第 24-25 行以及第 29 行的版本号可能需要修改。

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

该项目已配置为:

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

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


## 此文件由 Android Studio 自动生成。
# 请勿修改此文件 -- YOUR CHANGES WILL BE ERASED!
#
# 此文件必须 *NOT* 提交到版本控制系统,
# 因为它包含您本地配置的特定信息。
#
# SDK 的位置。此文件仅供 Gradle 使用。
# 如需在使用版本控制系统时进行自定义,请阅读
# 页眉注释。
#2016年4月7日 星期四 14:51:14 CEST
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 命令窗口并输入以下命令:


C:\Program Files\Console2>ipconfig

Configuration IP de Windows


Carte Ethernet Ethernet :

   Statut du média. . . . . . . . . . . . : Média déconnecté
   Suffixe DNS propre à la connexion. . . : ad.univ-angers.fr

Carte réseau sans fil Connexion au réseau local* 3 :

   Statut du média. . . . . . . . . . . . : Média déconnecté
   Suffixe DNS propre à la connexion. . . :

Carte Ethernet VirtualBox Host-Only Network :

   Suffixe DNS propre à la connexion. . . :
   Adresse IPv6 de liaison locale. . . . .: fe80::8076:36e6:3b38:5e98%16
   Adresse IPv4. . . . . . . . . . . . . .: 192.168.56.2
   Masque de sous-réseau. . . . . . . . . : 255.255.255.0
   Passerelle par défaut. . . . . . . . . :

Carte Ethernet Ethernet 2 :

   Suffixe DNS propre à la connexion. . . :
   Adresse IPv6 de liaison locale. . . . .: fe80::d0d9:e01f:ddde:1f4b%14
   Adresse IPv4. . . . . . . . . . . . . .: 192.168.95.1
   Masque de sous-réseau. . . . . . . . . : 255.255.255.0
   Passerelle par défaut. . . . . . . . . :

Carte réseau sans fil Wi-Fi :

   Suffixe DNS propre à la connexion. . . :
   Adresse IPv6 de liaison locale. . . . .: fe80::54b3:afe5:e199:2206%10
   Adresse IPv4. . . . . . . . . . . . . .: 192.168.0.13
   Masque de sous-réseau. . . . . . . . . : 255.255.255.0
   Passerelle par défaut. . . . . . . . . : fe80::523d:e5ff:fe0c:4ad9 192.168.0.1


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

使用上述信息执行异步请求,结果如下:

Image

每次请求都会返回一个名为 jSON 的响应,其中包含以下字段:

  • aleas:服务器生成的随机数;
  • idClient:请求编号;
  • on:客户端请求的执行线程;
  • requestAt:请求时间;
  • responseAt:接收响应的时间;
  • delay:服务器在返回响应前观察到的等待时间;
  • error:错误代码——若无错误则为 0;
  • message:错误信息——若无错误则为 null
  • observedAt:响应的观察时间;
  • observedOn:响应的观察线程。此处始终为 [main],表示 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 行的依赖项是 Android 项目的标准依赖项,其中包含 SDK 23;
  • 第5行的依赖项引入了Spring对象[RestTemplate],该对象负责管理[DAO]层与服务器之间的通信;
  • 第6行的依赖引入了应用程序使用的库JSON和[Jackson];
  • 第 7 行的依赖引入了库 RxAndroid(以及随附的库 RxJava),Ui 层使用该库与 [DAO] 层进行交互;

9.3.5. Android 应用程序的清单

  

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
          package="android.aleas">

  <uses-permission android:name="android.permission.INTERNET"/>

  <application
    android:allowBackup="true"
    android:icon="@mipmap/ic_launcher"
    android:label="@string/app_name"
    android:supportsRtl="true"
    android:theme="@style/AppTheme">
    <activity
      android:name="android.aleas.activity.MainActivity"
      android:label="@string/app_name"
      android:theme="@style/AppTheme.NoActionBar">
      <intent-filter>
        <action android:name="android.intent.action.MAIN"/>

        <category android:name="android.intent.category.LAUNCHER"/>
      </intent-filter>
    </activity>
  </application>

</manifest>
  • 第 5 行:必须允许互联网访问;

9.3.6. [DAO] 层

 

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

[DAO] 层的接口如下:


package android.aleas.dao;

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

public interface IDao {

  // 在区间 [a,b]
  // 生成 n 个随机数,其中 n 本身也是该区间内的随机数 [minCount, maxCount]
  // 在等待 delay 毫秒后生成这些数,
  // 其中 [delay] 本身是区间 [minDelay, maxDelay] 内的随机数
  public Observable<AleasDaoResponse> getAleas(final Request request);

  // Web 服务的 URL
  public void setUrlServiceWebJson(String url);

  // 服务器对连接请求的最大响应超时(毫秒)
  // 服务器对请求响应的最大超时时间(毫秒)
  public void setClientTimeouts(int connectTimeout, int readTimeOut);

}
  • 第 12 行:[DAO] 层的方法,用于异步生成随机数;
  • 第 15 行:用于向 [DAO] 实现指定随机数生成服务的 URL;
  • 第 19 行:为 [DAO] 实现设置最大超时时间,以避免服务器无响应时等待时间过长;

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


package android.aleas.fragments;

public class Request {

  // 请求编号
  int id;
  // 用户输入
  private int nbRequests;
  private int a;
  private int b;
  private int minCount;
  private int maxCount;
  private int minDelay;
  private int maxDelay;

  // 构造函数
  public Request() {

  }

  public Request(int id, int nbRequests, int a, int b, int minCount, int maxCount, int minDelay, int maxDelay) {
    this.id = id;
    this.nbRequests = nbRequests;
    this.a = a;
    this.b = b;
    this.minCount = minCount;
    this.maxCount = maxCount;
    this.minDelay = minDelay;
    this.maxDelay = maxDelay;
  }

  // 获取器和设置器
...
}

由此可见,其中包含需查询的服务器 URL 的大部分参数。

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


package android.aleas.dao;

import java.util.List;

public class AleasDaoResponse {

  // 错误代码
  private int erreur;
  // 错误消息
  private String message;
  // 服务器超时
  private int delay;
  // 服务器生成的随机数
  private List<Integer> aleas;
  // 客户端状态
  private ClientState clientState;

  // 构造函数

  public AleasDaoResponse() {
  }

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

  // 获取器和设置器
...
}

类型 [ClientState] 定义如下:


package android.aleas.dao;

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

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

public class ClientState {

  // 执行线程名称
  private String on;
  // 请求时间
  private String requestAt;
  // 响应时间
  private String responseAt;
  // 客户端 ID
  private int idClient;

  // 构造函数
  public ClientState() {
    on = Thread.currentThread().getName();
    requestAt = getTimeStamp();
  }

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

  // 私有方法

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

  // 获取器和设置器
...
}
  • 第 11 行:[DAO] 层的执行线程;
  • 第 13 行:请求时间;
  • 第 15 行:响应时间;
  • 第 17 行:请求编号;

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

9.3.6.2. [DAO] 层的实现

  

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


package android.aleas.dao;

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

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

public class Dao implements IDao {

  // 客户 REST
  private RestTemplate restTemplate;
  // URL 服务
  private String urlServiceWebJson;

  // 映射器 jSON
  private ObjectMapper mapper;

  // 生成器
  public Dao() {
    // 映射器 jSON
    mapper = new ObjectMapper();
  }

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

  @Override
  public void setUrlServiceWebJson(String urlServiceWebJson) {
    // 设置服务 REST 的 URL
    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] 方法如下:


  // 客户端 REST
  private RestTemplate restTemplate;
...

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

[getAleas] 方法是最复杂的方法:


@Override
  public Observable<AleasDaoResponse> getAleas(final Request request) {
    Log.d("rxjava", String.format("service [DAO] pour client n° %s%n", request.getId()));
    // 服务执行
    return Observable.create(new Observable.OnSubscribe<AleasDaoResponse>() {
      @Override
      public void call(Subscriber<? super AleasDaoResponse> subscriber) {
        try {
          // 服务 URL: /{a}/{b}/{minCount}/{maxCount}/{minDelay}/{maxDelay}
          String urlService = String.format("%s/%s/%s/%s/%s/%s/%s",
            urlServiceWebJson, request.getA(), request.getB(), request.getMinCount(),
            request.getMaxCount(), request.getMinDelay(), request.getMaxDelay());
          // 客户信息
          ClientState clientState = new ClientState(request.getId());
          // 同步 HTTP 请求
          String response = executeRestService("get", urlService, null);
          // 对服务器响应 jSON 进行反序列化
          AleasServerResponse aleasServerResponse = mapper.readValue(
            response,
            new TypeReference<AleasServerResponse>() {
            });
          // 错误?
          int erreur = aleasServerResponse.getErreur();
          if (erreur != 0) {
            // 转发异常
            subscriber.onError(new AleasException(aleasServerResponse.getMessage(), erreur));
          } else {
            // 记录接收时间
            clientState.setResponseAt();
            // 将结果转发给订阅者
            subscriber.onNext(
              new AleasDaoResponse(aleasServerResponse.getErreur(), aleasServerResponse.getMessage(),
                aleasServerResponse.getDelay(), aleasServerResponse.getAleas(), clientState));
          }
        } catch (Exception ex) {
          // 将异常转发给订阅者
          subscriber.onError(ex);
        } finally {
          // 报告可观察对象结束
          // 在运行时,若此前已调用过方法 [onError],则该方法将无效——这符合理论——因此该语句可仅置于 try 块中
          subscriber.onCompleted();
        }
      }
    });
  }
  • 第 2 行:需注意必须生成类型 [Observable<AleasResponse>];
  • 第 3 行:在 Android 控制台上输出日志;
  • 第 5 行:[RestTemplate] 对象确保与服务器的同步通信。这意味着发起请求的执行线程将阻塞,直至收到响应。 在 Swing 示例中,我们已经了解如何通过 [Observable.create] 方法将同步操作转换为异步操作。这里我们也将采用同样的方法;
  • 第 7 行:调用第 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 {

  // 错误代码
  private int erreur;
  // 错误信息
  private String message;
  // 服务器超时
  private int delay;
  // 随机数
  private List<Integer> aleas;

  // 获取器和设置器
...
}
  • 第 23 行:获取服务器发送的错误代码;
  • 第24-26行:若发生错误,向用户抛出异常;
  • 第29行:更新[clientState],该对象将作为响应的一部分发送给订阅者;
  • 第31-33行:向用户发送响应。响应格式为[AleasDaoResponse];
  • 第35-37行:对所有错误情况一视同仁地处理。最可能出现的错误是网络错误;
  • 第41行:发送结束通知;

9.3.7. 应用程序视图

  

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

请求视图

Image

响应视图

Image

9.3.7.1. 类 [MyFragment]

共有两个片段:

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

这两个片段扩展了以下类 [MyFragment]:


package android.aleas.fragments;

import android.aleas.activity.MainActivity;
import android.aleas.activity.Session;
import android.support.v4.app.Fragment;

public abstract class MyFragment extends Fragment {

  // ------------- 片段共享数据
  protected MainActivity activity;
  protected Session session;

  public abstract void onRefresh();

}
  • 第 7 行:类 [MyFragment] 继承自 Android 类 [Fragment];
  • 第 10-11 行:所有片段共有的数据;
  • 第 10 行:每个片段都了解应用程序的唯一活动;
  • 第 11 行:片段之间通过会话进行通信;
  • 第 13 行:在显示片段之前,会要求其使用会话内容进行刷新。该方法被声明为抽象方法,因为它由子类实现。因此,该类本身也被声明为抽象类(第 7 行);

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

  

package android.aleas.activity;

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

public class Session {

  // 应用程序活动
  private MainActivity activity;
  // 请求数量
  private int nbRequests;
  // 请求特征
  private int a;
  private int b;
  private int minCount;
  private int maxCount;
  private int minDelay;
  private int maxDelay;
  // URL Web 服务 / jSON
  private String urlWebJson;
  // 操作已开始
  private boolean onAir;
  // 同上,但时间稍晚
  private boolean operationStarted;
  // 用户从示例列表中选择的示例名称
  private String exampleName;
  // 在片段列表中的编号
  private int examplePosition;
  // 查询视图中示例旋转器的适配器
  private ArrayAdapter<CharSequence> spinnerExemplesAdapter;

  // 方法
  public void setInfos(int nbRequests, int a, int b, int minCount, int maxCount, int minDelay, int maxDelay, String urlWebJson, String exampleName, int examplePosition) {
    this.nbRequests = nbRequests;
    this.a = a;
    this.b = b;
    this.minCount = minCount;
    this.maxCount = maxCount;
    this.minDelay = minDelay;
    this.maxDelay = maxDelay;
    this.urlWebJson = urlWebJson;
    this.exampleName = exampleName;
    this.examplePosition = examplePosition;
  }

  public Request getRequest() {
    return new Request(0, nbRequests, a, b, minCount, maxCount, minDelay, maxDelay);
  }

  // getter 和 setter
...
}

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

  

package android.aleas.fragments;

public class Request {

  // 请求编号
  int id;
  // 用户输入
  private int nbRequests;
  private int a;
  private int b;
  private int minCount;
  private int maxCount;
  private int minDelay;
  private int maxDelay;

  // 构造函数
  public Request() {

  }

  public Request(int id, int nbRequests, int a, int b, int minCount, int maxCount, int minDelay, int maxDelay) {
    this.id = id;
    this.nbRequests = nbRequests;
    this.a = a;
    this.b = b;
    this.minCount = minCount;
    this.maxCount = maxCount;
    this.minDelay = minDelay;
    this.maxDelay = maxDelay;
  }

  // getter 和 setter
....
}

9.3.7.2. 查询中的片段 [RequestFragment]

该请求片段包含以下组件:

Image

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

  • [1]:请求标签页;
  • [2]:响应标签页;

片段 [RequestFragment] 的组件如下:

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

输入错误会提示:

Image

组件 1 至 6 均为 [TextView] 组件,其名称(按顺序)如下: txtErrorRequeststxtErrorIntervalletxtErrorCounttxtErrorDelaytxtMsgErreurUrlServiceWeb

9.3.7.3. 响应中的片段 [ResponseFragment]

响应片段包含以下组件:

Image

编号
类型
姓名
角色
1
TextView
infoReponses
收到的回复数量
2
ListView
listReponses
从服务器接收的频道列表 jSON
3
按钮
btnAnnuler
用于取消向服务器的请求

9.3.7.4. Android 活动 [MainActivity]

  

类 [MainActivity] 显示以下视图 []:


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

  <!-- 应用程序栏 -->
  <android.support.design.widget.AppBarLayout
    android:id="@+id/appbar"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:paddingTop="@dimen/appbar_padding_top"
    android:theme="@style/AppTheme.AppBarOverlay">

    <!-- 工具栏 -->
    <android.support.v7.widget.Toolbar
      android:id="@+id/toolbar"
      android:layout_width="match_parent"
      android:layout_height="?attr/actionBarSize"
      android:background="?attr/colorPrimary"
      app:popupTheme="@style/AppTheme.PopupOverlay"
      app:layout_scrollFlags="scroll|enterAlways">

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

    <!-- 标签页容器 -->
    <android.support.design.widget.TabLayout
      android:id="@+id/tabs"
      android:layout_width="match_parent"
      android:layout_height="wrap_content"/>
  </android.support.design.widget.AppBarLayout>

  <!-- 视图容器 -->
  <android.aleas.activity.MyPager
    android:id="@+id/container"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingLeft="20dp"
    android:paddingRight="20dp"
    android:layout_marginBottom="100dp"
    app:layout_behavior="@string/appbar_scrolling_view_behavior"/>
</android.support.design.widget.CoordinatorLayout>

该视图的组件如下:

类型
名称
角色
20-34
工具栏
工具栏
应用程序工具栏
29-34
ProgressBar
loadingPanel
在处理用户请求期间显示的加载图片
37-40
TabLayout
标签页
应用程序的标签栏
44-51
MyPager
容器
用于显示应用程序各个片段的容器

类 [MyPager] 如下所示:


package android.aleas.activity;

import android.content.Context;
import android.support.v4.view.ViewPager;
import android.util.AttributeSet;
import android.view.MotionEvent;

public class MyPager extends ViewPager {

  // 控制滑动
  private boolean isSwipeEnabled;

  // 构造函数
  public MyPager(Context context) {
    super(context);
  }

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

  // 方法重写
  @Override
  public boolean onInterceptTouchEvent(MotionEvent event) {
    // 允许滑动吗?
    if (isSwipeEnabled) {
      return super.onInterceptTouchEvent(event);
    } else {
      return false;
    }
  }

  @Override
  public boolean onTouchEvent(MotionEvent event) {
    // 允许滑动吗?
    if (isSwipeEnabled) {
      return super.onTouchEvent(event);
    } else {
      return false;
    }
  }

  // 设置器
  public void setSwipeEnabled(boolean isSwipeEnabled) {
    this.isSwipeEnabled = isSwipeEnabled;
  }

}
  • 类 [MyPager] 继承了 Android 标准类 [ViewPager]。 我们使用类 [MyPager] 代替类 [ViewPager],仅仅是因为我们希望禁用滑动操作:默认情况下,使用类 [ViewPager] 时,可以通过滑动 (向左或向右滑动)。在此处,我们不希望出现这种行为;
  • 第11行:用于控制滑动功能的布尔变量(第26行和第36行);
  • 第44-46行:用于初始化第11行字段的方法;

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


package android.aleas.activity;

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

public class MainActivity extends AppCompatActivity implements IDao {

  // 层[DAO]
  private IDao dao;
  // 会话
  private Session session;

  // 构造函数
  public MainActivity() {
    // 父级
    super();
    // 会话
    session = new Session();
    // DAO
    dao = new Dao();
  }


  // 获取器

  public Session getSession() {
    return session;
  }

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

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

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

}
  • 第 21 行:类 [MainActivity] 继承了 Android 标准类 [AppCompatActivity]。因此,这是一个标准的 Android 活动;
  • 第 21 行:类 [MainActivity] 实现了接口 [IDao];

回到应用程序的架构:

由于该活动实现了 [DAO] 层的接口,因此视图无需了解 [DAO] 层: 当视图需要与服务器交互时,其事件处理程序将调用 [activité] 层。

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

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


  // 工具栏
  private Toolbar toolbar;
  // 片段管理器
  private MyPager mViewPager;
  // 标签页容器
  private TabLayout tabLayout;
  // 加载图标
  private ProgressBar loadingPanel;
...
  @Override
  public void onCreate(Bundle savedInstanceState) {
    // 经典
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    // 会话
    session.setActivity(this);
    // 配置 QZXW2HTML 层的超时BW0RBT10ZQX
    setClientTimeouts(Constants.CONNECT_TIMEOUT, Constants.READ_TIMEOUT);

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

    // 工具栏
    setSupportActionBar(toolbar);

    // 起初只有一个标签页
    TabLayout.Tab tab = tabLayout.newTab();
    tab.setText("Request");
    tabLayout.addTab(tab);

    // 事件管理器
    tabLayout.setOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {
      @Override
      public void onTabSelected(TabLayout.Tab tab) {
        // 已选中一个标签页——通过片段容器更改显示的片段
        int position = tab.getPosition();
        if (position == 0) {
          // 查询标签页
          showView(0);
        } else {
          // 响应标签页 - 取决于所选示例
          showView(session.getExamplePosition());
        }
      }

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

      }

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

      }
    });

    // 创建响应片段
    createResponseFragments();

    // 加载图片管理
    loadingPanel.setVisibility(View.INVISIBLE);
}

此代码在业务活动中相当常见。让我们详细说明几点:

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

package android.aleas.activity;

abstract public class Constants {

  final static public int VUE_REQUEST = 0;
  final static public int VUE_RESPONSE = 1;
  final static public int CONNECT_TIMEOUT = 1000;
  final static public int READ_TIMEOUT = 6000;
  final static public int DELAY_MAX = 5000;
  final static public String EXAMPLES_PACKAGE = "android.aleas.exemples";
}
  • 第 31-33 行:创建标题为 [Request] 的第一个选项卡。此时,内存中将包含:
    • 片段 [Request];
    • n 个类型为 [ExampleXXFragment] 的片段;

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

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

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


  // 片段管理器 - 必须定义方法 getItem、getCount
  public class SectionsPagerAdapter extends FragmentPagerAdapter {

    // 管理的片段
    private MyFragment[] fragments;

    // 构造函数
    public SectionsPagerAdapter(FragmentManager fm, MyFragment[] fragments) {
      super(fm);
      this.fragments = fragments;
    }

    // 必须渲染第 n 号片段
    @Override
    public MyFragment getItem(int position) {
      // 片段
      return fragments[position];
    }

    // 返回待管理的片段数量
    @Override
    public int getCount() {
      // 片段数量
      return fragments.length;
    }
  }
}
  • 类 [SectionsPagerAdapter] 继承自 Android 类 [FragmentPagerAdapter]。它重定义了父类的两个方法:
    • 方法 [getItem],第 15 行;
    • 方法 [getCount],第 22 行;
  • 类[SectionsPagerAdapter]包含应用程序的所有片段。这些片段存储在第5行。需要注意的是,它们的类型为[MyFragment],该类型已在第9.3.7.1节中介绍;
  • 第 8 行:为了构建自身,类 [SectionsPagerAdapter] 接收其需要管理的片段;
  • 第14-18行:方法[getItem]返回位于位置[position]的片段;
  • 第21-25行:方法[getCount]返回片段总数;

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


private void createResponseFragments() {
    // 示例旋转器
    ArrayAdapter<CharSequence> adapter = ArrayAdapter.createFromResource(this, R.array.exemples, android.R.layout.simple_spinner_item);
    // 指定显示选项列表时使用的布局
    adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
    // 将适配器放入会话中,以便视图 [Request] 能够获取它
    session.setSpinnerExemplesAdapter(adapter);
    ...
  }
  • 第 3 行:为示例的旋转按钮创建适配器,此处为 String 列表,其中包含示例名称。这些名称存在于文件 [layout/exemples.xml] 中:
  

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


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

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

  • 第5行:将一个layout(显示管理器)与适配器关联。现在,适配器既拥有数据,也拥有其显示模式;
  • 第 7 行:将适配器置于会话中。需要它的片段 [RequestFragment] 将在此处获取它;

继续解析 [createResponseFragments] 方法的代码:


private void createResponseFragments() {
    // 示例中的旋转选择器
    ArrayAdapter<CharSequence> adapter = ArrayAdapter.createFromResource(this, R.array.exemples, android.R.layout.simple_spinner_item);
    // 指定显示选项列表时使用的布局
    adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
    // 将适配器放入会话中,以便视图 [Request] 获取它
    session.setSpinnerExemplesAdapter(adapter);
    // 创建片段表(1个查询,n个响应)
    MyFragment[] tFragments = new MyFragment[adapter.getCount() + 1];
    // 查询片段
    tFragments[0] = new RequestFragment();
    // 响应片段
    for (int i = 1; i < tFragments.length; i++) {
      // 构建与用户所选示例对应的待实例化片段名称
      // 该名称必须包含完整包名——此处直接关联到旋转选择器中的示例编号
      String exampleClassName = String.format("%s.Example%02dFragment", Constants.EXAMPLES_PACKAGE, i);
      // 实例化与示例关联的片段
      MyFragment fragment;
      try {
        // 实例化该类
        fragment = (MyFragment) Class.forName(exampleClassName).getConstructors()[0].newInstance(new Object[]{});
      } catch (Exception e) {
        e.printStackTrace();
        return;
      }
      // 片段已创建——将其放入数组
      tFragments[i] = fragment;
    }
    // 使用这些新片段实例化片段管理器
    mSectionsPagerAdapter = new SectionsPagerAdapter(getSupportFragmentManager(), tFragments);
    // 使用分段适配器配置 ViewPager。
    mViewPager.setAdapter(mSectionsPagerAdapter);
    // 页面间导航——此指令很重要
    // 这里说明,在显示视图的两侧,必须保留已初始化的视图
    // 这意味着应用程序使用的所有片段都已加载到内存中并完成初始化
    // 如果不这样做,则默认情况下 [OffscreenPageLimit] 的值为 1
    // 因此,如果显示的是第 3 个片段,则仅会初始化第 2 和第 4 个片段
    // 这是通过调用这两个片段的 [onCreateView] 方法实现的——这意味着在该方法中,必须预留
    // 重现片段上次使用时的视觉效果——此外,该方法中
    // 包含无法支持被执行两次的代码——这会造成巨大的混乱且难以管理
    // 在此我们选择规避这些困难——日志显示,在应用程序启动时,所有片段均已创建
    // 并执行了它们的 [onCreateView] 方法——此后该方法将不再被执行——
    mViewPager.setOffscreenPageLimit(tFragments.length);
    // 我们禁用了片段间的滑动操作
    mViewPager.setSwipeEnabled(false);
  }
  • 第 9 行:创建将包含应用程序所有片段的数组;
  • 第 11 行:第一个片段是请求片段;
  • 第13-28行:将创建与示例数量相同的分片。这些分片均继承自响应分片[ResponseFragment],并仅实现该示例特有的内容:即生成观测值。这些值在不同示例中确实各不相同;
  • 第 16 行:示例片段采用标准命名格式:ExampleXXFragment,其中 XX 是该示例在示例选择器中的序号加 1。 XX 也是该示例片段在片段管理器中的编号;
  • 第 21 行:实例化旋转器中第 i 号示例片段:
    • Class.forName(exampleName):将片段加载到内存中;
    • Class.forName(exampleName).getConstructors()[0]:获取该类第一个构造函数的引用。 类 ExampleXXFragment 只有一个构造函数。因此将获取的是该构造函数的引用;
    • Class.forName(exampleName).getConstructors()[0].newInstance(new Object[]{}) 使用前一步的构造函数实例化一个 ExampleXXFragment 类型的对象。new Object[]{} 代表传递给该构造函数的参数。 由于 ExampleXXFragment 类的构造函数不接受参数,因此传递了一个空对象数组;
  • 第 27 行:该片段被添加到片段数组中;
  • 第 30 行:我们之前看到,片段管理器 [SectionsPagerAdapter] 的构造函数在其参数中需要一个它需要管理的片段数组。现在我们将该数组传递给它;
  • 第 22 行:与活动 [MainActivity] 关联的视图中的片段容器 [mViewPager] 在此与片段管理器关联:片段容器 [mViewPager] 显示片段管理器中的片段;
  • 第43行:请阅读注释——该指令的意思是,无论当前显示的是哪个片段,所有片段都必须保持代码设定的状态。因此,当再次访问该片段时,它将处于被留下的状态;
  • 第45行:片段容器[mViewPager]属于[MyPager]类型,该类型可禁用滑动操作;

方法 [MainActivity.showView] 如下:


  // 显示视图编号 [position]
  private void showView(int position) {
    // 在显示之前刷新片段
    mSectionsPagerAdapter.getItem(position).onRefresh();
    // 显示请求的视图——直接跳转到该视图(第二个参数设为 false)
    // 若未指定此参数,系统将默认跳转至目标视图,并快速显示中间视图——此行为不可取
    mViewPager.setCurrentItem(position, false);
}
  • 第 3 行:要显示编号为 position 的片段;
  • 第 4 行:向片段管理器请求该片段,然后进行刷新。因为自上次显示以来,会话可能已发生变化。因此,片段必须检查会话状态,以确定是否需要更新;
  • 第7行:片段由[ViewPager]显示。由于该片段已与片段管理器关联,因此将显示编号为[position]的片段,即第4行中刚刚刷新过的那个;

最后介绍两种处理等待的方法:


  public void beginWaiting() {
    // 加载等待图片
    loadingPanel.setVisibility(View.VISIBLE);
  }

  public void cancelWaiting() {
    // 加载图片管理
    loadingPanel.setVisibility(View.INVISIBLE);
    // 执行结束
    session.setOnAir(false);
    session.setOperationStarted(false);
}

9.3.7.5. 片段 [RequestFragment]

类 [RequestFragment] 如下所示:


package android.aleas.fragments;

import android.aleas.R;
import android.aleas.activity.Constants;
import android.aleas.activity.MainActivity;
import android.os.Bundle;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.*;

import java.net.URI;
import java.net.URISyntaxException;

public class RequestFragment extends MyFragment {

  // Web 服务的 URL
  private EditText edtUrlServiceRest;
  private TextView txtMsgErreurUrlServiceWeb;
  // 请求数量
  private EditText edtNbRequests;
  private TextView txtErrorRequests;
  // 生成间隔
  private EditText edtA;
  private EditText edtB;
  private TextView txtErrorIntervalle;
  // 延迟
  private EditText edtMinDelay;
  private EditText edtMaxDelay;
  private TextView txtErrorDelay;
  // 生成的值数量
  private EditText edtMinCount;
  private EditText edtMaxCount;
  private TextView txtErrorCount;
  // 按钮
  private Button btnExecuter;
  // 响应列表
  private ListView listReponses;
  private TextView infoReponses;
  // 示例旋转器
  private Spinner spinnerExemples;

  // 输入内容
  private int nbRequests;
  private int a;
  private int b;
  private String urlServiceWebJson;
  private int minDelay;
  private int maxDelay;
  private int minCount;
  private int maxCount;

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

  @Override
  public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
    Log.d("rxjava", "RequestFragment onCreateView");
    // 获取活动和会话
    activity = (MainActivity) getActivity();
    session = activity.getSession();
    // 根据定义创建片段视图 XML
    View view = inflater.inflate(R.layout.request, container, false);
    // 组件
    edtUrlServiceRest = (EditText) view.findViewById(R.id.editTextUrlServiceWeb);
    txtMsgErreurUrlServiceWeb = (TextView) view.findViewById(R.id.textViewErreurUrl);
    edtNbRequests = (EditText) view.findViewById(R.id.edt_nbrequests);
    txtErrorRequests = (TextView) view.findViewById(R.id.txt_error_nbrequests);
    edtA = (EditText) view.findViewById(R.id.edt_a);
    edtB = (EditText) view.findViewById(R.id.edt_b);
    txtErrorIntervalle = (TextView) view.findViewById(R.id.txt_errorIntervalle);
    edtMinDelay = (EditText) view.findViewById(R.id.edt_minDelay);
    edtMaxDelay = (EditText) view.findViewById(R.id.edt_maxDelay);
    txtErrorDelay = (TextView) view.findViewById(R.id.txt_error_delay);
    edtMinCount = (EditText) view.findViewById(R.id.edt_minCount);
    edtMaxCount = (EditText) view.findViewById(R.id.edt_maxCount);
    txtErrorCount = (TextView) view.findViewById(R.id.txt_error_count);
    btnExecuter = (Button) view.findViewById(R.id.btn_Executer);
    listReponses = (ListView) view.findViewById(R.id.lst_reponses);
    infoReponses = (TextView) view.findViewById(R.id.txt_Reponses);
    spinnerExemples = (Spinner) view.findViewById(R.id.spinnerExemples);

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

    // 起初没有错误消息
    txtErrorRequests.setVisibility(View.INVISIBLE);
    txtErrorIntervalle.setVisibility(View.INVISIBLE);
    txtMsgErreurUrlServiceWeb.setVisibility(View.INVISIBLE);
    txtErrorCount.setVisibility(View.INVISIBLE);
    txtErrorDelay.setVisibility(View.INVISIBLE);
    // 示例中的加载图标
    spinnerExemples.setAdapter(session.getSpinnerExemplesAdapter());
    // 结果
    return view;
  }
...
}
  • 第 16 行:类 [RequestFragment] 继承自类 [MyFragment](参见第 9.3.7.1 节);
  • 第 18-42 行:片段的视觉组件(参见第 9.3.7.2 节);
  • 第45-52行:用户在表单中输入的内容;
  • 当 [MainActivity] 活动创建应用程序的所有片段时,将执行构造函数(第 55-58 行)和 [onCreateView] 方法。这仅发生一次;
  • 第 61 行:方法 [onCreateView] 的代码较为常规。请注意第 102 行,示例中的旋转按钮适配器被纳入了会话中。 另请注意第 91 行,[Exécuter] 按钮的点击事件由 [doExecuter] 方法处理;
  • 第 64-65 行:字段 [activity] 和 [session] 属于父类 [MyFragment];

方法 [doExecuter] 如下:


  // 输入内容
  private int nbRequests;
  private int a;
  private int b;
  private String urlServiceWebJson;
  private int minDelay;
  private int maxDelay;
  private int minCount;
  private int maxCount;

...

  private void doExecuter() {
    // 输入有效吗?
    if (isPageValid()) {
      // 将信息存入会话
      session.setInfos(nbRequests, a, b, minCount, maxCount, minDelay, maxDelay, urlServiceWebJson, spinnerExemples.getSelectedItem().toString(), spinnerExemples.getSelectedItemPosition() + 1);
      // 保存 Web 服务的 URL
      activity.setUrlServiceWebJson(session.getUrlWebJson());
      Log.d("rxjava", String.format("RequestFragment doExecuter, session=%s, session.position=%s%n", session, session.getExamplePosition()));
      // 操作进行中
      session.setOnAir(true);
      // 但尚未开始
      session.setOperationStarted(false);
      // 显示响应片段
      activity.selectTab(Constants.VUE_RESPONSE);
      // 开始等待
      beginWaiting();
    }
}
  • 第 15 行:我们不再对方法 [ispageValid] 进行说明。该方法用于验证输入的有效性,仅当所有输入均有效时才返回 true。在此情况下,这些输入将用于初始化第 2-9 行的字段;
  • 第17行:将各项输入数据存入会话中:
    • [spinnerExemples.getSelectedItem().toString()] 是用户选定的示例名称,并存储在 [session.exampleName] 中;
    • [spinnerExemples.getSelectedItemPosition() + 1] 是与该示例关联的片段编号,该片段已由片段管理器存储。该编号存储在 [session.examplePosition] 中;
  • 第19行:Web服务中的URL被传递给业务活动,该活动随后将其传递给[DAO]层;
  • 第21-24行:注意到将启动一项操作;
  • 第26行:响应标签页即将显示。要理解接下来将发生什么,需回顾代码[MainActivity.selectTab]:

  // 选择标签页
  public void selectTab(int position) {
    // 最多有 2 个标签页
    // 初始时仅有一个,即请求中的那个
    // 如果请求的标签页是第1个,且该标签页尚未存在,则需创建它
    if (position == 1 && tabLayout.getTabCount() == 1) {
      // 增加 1 个标签页
      TabLayout.Tab tab = tabLayout.newTab();
      tab.setText("Response");
      tabLayout.addTab(tab);
    }
    // 通过程序选择该标签页,这将触发事件 [onTabSelected]
    // 该事件将把正确的视图与该标签页关联
    tabLayout.getTabAt(position).select();
}
  • 最初,该活动仅创建了请求标签页(标签页编号 0);
  • 第6-11行:如果响应标签页(标签页编号1)尚未创建,则创建该标签页;
  • 第14行:选择编号为position的标签页(0或1)。这将事件[onTabSelected]放入Android应用程序事件循环的队列中;

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


      @Override
      public void onTabSelected(TabLayout.Tab tab) {
        // 选中了一个标签页——通过片段容器更改显示的片段
        int position = tab.getPosition();
        if (position == 0) {
          // 请求标签页
          showView(0);
        } else {
          // 响应选项卡——取决于所选示例
          showView(session.getExamplePosition());
        }
}

对于标签页 [Response],将执行第 9 行代码。 将显示编号为 [session.getExamplePosition()] 的片段。例如,对于示例 [exemple-03],在 [session.examplePosition] 中记录的编号为 3。因此,第 10 行将显示编号为 3 的片段。 该活动最初构建的片段表为 [RequestFragment, Exemple01Fragment, Exemple02Fragment, Exemple03Fragment,..]。因此,实际显示的正是片段 [Exemple03Fragment]。通过以下代码实现:


  // 显示视图编号 [position]
  private void showView(int position) {
    // 在显示片段前刷新片段
    mSectionsPagerAdapter.getItem(position).onRefresh();
    // 显示请求的视图 - 直接跳转到该视图(第二个参数设为 false)
    // 若未指定此参数,系统将默认跳转至目标视图,并快速显示中间视图——此行为不可取
    mViewPager.setCurrentItem(position, false);
}

可以看到,片段在显示(第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 {

  // 响应列表
  private ListView listReponses;
  private TextView infoReponses;
  // 按钮
  private Button btnAnnuler;

  // 映射器 jSON
  private ObjectMapper mapper;

  protected ResponseFragment() {
    super();
    Log.d("rxjava", String.format("ResponseFragment (%s) constructor", this));
    mapper = new ObjectMapper();
  }

  @Override
  public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
    // 获取活动和会话
    activity = (MainActivity) getActivity();
    session = activity.getSession();
    Log.d("rxjava", String.format("ResponseFragment (%s) onCreateView%n", this));
    // 根据定义创建片段视图XML
    View view = inflater.inflate(R.layout.response, container, false);
    // 组件
    listReponses = (ListView) view.findViewById(R.id.lst_reponses);
    infoReponses = (TextView) view.findViewById(R.id.txt_Reponses);
    btnAnnuler = (Button) view.findViewById(R.id.btn_Annuler);
    // 按钮[Annuler]
    btnAnnuler.setVisibility(View.INVISIBLE);
    btnAnnuler.setOnClickListener(new View.OnClickListener() {
      public void onClick(View arg0) {
        doAnnuler();
      }
    });
    // 结果
    return view;
  }
...
  // 在每次显示片段之前需执行的方法(通过显式代码)
  public void onRefresh() {
...
  }
}
  • 第21行:类[ResponseFragment]继承自类[MyFragment];
  • 第 23-27 行:片段的组件;
  • 第 32-36 行:构造函数仅在活动首次创建示例片段时执行一次。实际上,所有示例片段都继承自片段 [ResponseFragment]。 在实例化时,会调用其父类 [ResponseFragment] 的构造函数;
  • 第 35 行:初始化第 30 行中的映射器 jSON,该映射器用于显示异常堆栈中的字符串 jSON;
  • 第38-59行:方法[onCreateView]仅在活动初始化示例片段时执行一次。其中包含Android应用中的经典代码;
  • 第 52-56 行:点击按钮 [Annuler] 时执行的方法是 [doAnnuler];
  • 第 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] 如下:


  // 在每次显示片段之前需执行的方法(通过显式代码)
  public void onRefresh() {
    Log.d("rxjava", String.format("ResponseFragment (%s) onRefresh for %s, sessionIsOnAir=%s session.isOperationStarted=%s%n", this, activity == null ? null : activity.getSession().getExampleName(), session.isOnAir(), session.isOperationStarted()));
    // 正在执行?
    if (session.isOnAir() && !session.isOperationStarted()) {
      // 请求正在执行
      session.setOperationStarted(true);
      doExecuter();
    }
}
  • 第 5 行:检查片段 [RequestFragment] 是否已发出请求(session.isOnAir),以及该请求是否已启动(isOperationStarted)。 如果片段 [RequestFragment] 已发起请求且该请求尚未开始执行,则启动操作(第 7-8 行);
  • 操作启动后,由于其为异步操作,用户可在两个标签页之间切换。若用户再次切换至 [Response] 标签页且此时有操作正在进行,则第 7-8 行代码不会被执行;

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


  private void doExecuter() {
    Log.d("rxjava", String.format("ResponseFragment (%s) doExecuter for %s%n", this, session.getExampleName()));
    // 开始等待
    beginWaiting();
    // 执行准备
    subscriptions.clear();
    reponses.clear();
    nbInfos = 0;
    // 创建并执行所选示例中的可观察对象
    createAndExecuteObservables();
}

// 由子类实现的方法
protected abstract void createAndExecuteObservables();
  • 第 10 行:创建、执行并监听可观察对象。这些对象在每个示例中各不相同。 因此,方法 [createAndExecuteObservables] 被声明为抽象方法(第 14 行)。它将由继承自类 [ResponseFragment] 的片段 [ExampleXXFragment] 来实现;
  • 第 6 行:清空订阅列表;
  • 第 7 行:清空显示回复的列表;
  • 第 8 行:统计已接收的回复数量;

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


  protected void showAlea(String data) {
    // 更多信息
    nbInfos++;
    infoReponses.setText(String.format("Liste des réponses (%s)", nbInfos));
    // 1条回复
    reponses.add(0, data);
    Log.d("rxjava", data);
    // 更新了UI
    listReponses.setAdapter(new ArrayAdapter<String>(getActivity(), android.R.layout.simple_list_item_1, android.R.id.text1, reponses));
}
  • 第 1 行:可见被监视的元素以字符串形式传入。实际上,这将是该元素的 jSON 字符串。这使得我们能够通过单一方法显示被监视的元素,无论其确切的 Java 类型为何;
  • 第 6 行:被监视的 [data] 元素被添加到响应列表的首位。因此,用户在列表开头看到的是最新的响应;

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


  private void beginWaiting() {
    // 显示沙漏
    activity.beginWaiting();
    // 显示按钮 [Annuler]
    btnAnnuler.setVisibility(View.VISIBLE);
  }

  protected void cancelWaiting() {
    // 等待结束
    activity.cancelWaiting();
    // 隐藏按钮 [Annuler]
    btnAnnuler.setVisibility(View.INVISIBLE);
}

它们调用活动中的同名方法,仅负责显示/隐藏 [Annuler] 按钮。

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


  protected void doAnnuler() {
    // 取消所有订阅
    for (Subscription s : subscriptions) {
      if (!s.isUnsubscribed()) {
        s.unsubscribe();
      }
    }
    // 等待结束
    cancelWaiting();
}
  • 第3-7行:依次取消所有订阅;

9.3.8. 可观察对象示例

9.3.8.1. Exemple-01

[ExampleXXFragment]类的功能是创建、执行和监听可观察对象。所观察到的值的显示由父类[ResponseFragment]负责。

[Example01Fragment] 类如下:

  

package android.aleas.exemples;

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

import java.io.IOException;

public class Example01Fragment extends ResponseFragment {

    // 映射器 jSON
    private ObjectMapper mapperAleasUiResponse;

    // 生成器
    public Example01Fragment() {
        super();
        Log.d("rxjava", "Example01Fragment constructor");
        // 过滤器 jSON
        mapperAleasUiResponse = new ObjectMapper();
    }

    @Override
    public void createAndExecuteObservables() {
        Log.d("rxjava", "Example01Fragment createAndExecuteObservables");
        // 请求随机数
        Observable<AleasDaoResponse> observable = Observable.empty();
        for (int i = 0; i < session.getNbRequests(); i++) {
            // 第 i 个可观测量配置
            // 向服务器发送的请求
            Request request = session.getRequest();
            request.setId(i);
            // 在计算线程上执行的可观察对象
            observable = observable.mergeWith(session.getActivity().getAleas(request).subscribeOn(Schedulers.io()));
        }
        // 在事件循环线程上进行观察;
        observable = observable.observeOn(AndroidSchedulers.mainThread());
        // 执行所有这些可观察对象
        subscriptions.add(observable.subscribe(new Action1<AleasDaoResponse>() {
            @Override
            public void call(AleasDaoResponse aleasDaoResponse) {
                showAlea(getDataFrom(aleasDaoResponse));
            }
        }, new Action1<Throwable>() {
...
        }, new Action0() {
...
    }

    private String getDataFrom(AleasDaoResponse aleasDaoResponse) {
        // 提取待显示的信息
        String data;
        try {
            data = mapperAleasUiResponse.writeValueAsString(new AleasUiResponse(aleasDaoResponse));
        } catch (IOException e) {
            data = String.format("[%s,%s]", e.getClass().getName(), e.getMessage());
        }
        return data;
    }
}
  • 第 36 行:将要生成的唯一可观测量;
  • 第 37-44 行:生成并配置各个可观察对象,这些对象在第 43 行被合并到第 36 行的可观察对象中;
  • 第 43 行:该可观察对象在调度器 [Schedulers.io()] 的一个线程中执行。对服务器的调用 HTTP 将在该线程中执行;
  • 第 46 行:最终的可观察对象在事件循环线程上被观察;
  • 第48-57行:执行可观察对象,即向随机数服务器发送请求。Android目前尚不支持Java 8及其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 {

  // 响应 [DAO]
  private AleasDaoResponse aleasDaoResponse;
  // 观察线程
  private String observedOn;
  // 观测时间
  private String observedAt;

  // 构造函数
  public AleasUiResponse() {
    observedOn = Thread.currentThread().getName();
    observedAt = new SimpleDateFormat("hh:mm:ss:SSS").format(Calendar.getInstance().getTime());
  }

  public AleasUiResponse(AleasDaoResponse aleasDaoResponse, String on, String at) {
    this.aleasDaoResponse = aleasDaoResponse;
    this.observedOn = on;
    this.observedAt = at;
  }

  public AleasUiResponse(AleasDaoResponse aleasDaoResponse) {
    this();
    this.aleasDaoResponse = aleasDaoResponse;
  }
// 获取器和设置器
...
}
  • 在 [DAO] 层的响应(第 11 行)中,添加两项信息:
    • 第13行:观测线程;
    • 第15行:观测时间;

回到订阅代码:


    @Override
    public void createAndExecuteObservables() {
...
        // 执行所有这些可观察对象
        subscriptions.add(observable.subscribe(new Action1<AleasDaoResponse>() {
            @Override
            public void call(AleasDaoResponse aleasDaoResponse) {
                showAlea(getDataFrom(aleasDaoResponse));
            }
        }, new Action1<Throwable>() {
            @Override
            public void call(Throwable th) {
                // 显示异常
                showAlea(getMessagesFromThrowable(th));
                // 收到异常后,该可观察对象既不会收到 onNext,也不会收到 onCompleted
                // 必须手动取消订阅
                doAnnuler();
            }
        }, new Action0() {
            @Override
            public void call() {
                // 等待结束
                cancelWaiting();
            }
        }));
}
  • 第 11-18 行:观察者收到异常的情况;
  • 第14行:再次使用父类的[showAlea]方法来显示异常。[getMessagesFromThrowable]方法是父类[ResponseFragment]的一个方法,它根据异常生成一个字符串:

  // 异常消息
  protected String getMessagesFromThrowable(Throwable ex) {
    // 创建包含异常堆栈中错误消息的列表
    List<String> messages = new ArrayList<String>();
    Throwable th = ex;
    while (th != null) {
      messages.add(String.format("[%s, %s]", th.getClass().getName(), th.getMessage()));
      th = th.getCause();
    }
    try {
      return mapper.writeValueAsString(messages);
    } catch (IOException e) {
      return e.getMessage();
    }
}
  • 第 11 行:将字符串 jSON 作为错误消息列表(第 4 行)返回;

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

  • 第 19-25 行:当观察者收到发布结束通知时执行的代码。此时取消等待(第 23 行),从而更新图形界面;

示例 01 的执行结果类似于以下内容:

Image

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

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

9.3.8.2. Exemple-02

[Example02Fragment] 类定义如下:


package android.aleas.exemples;

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

import java.io.IOException;

public class Example02Fragment extends ResponseFragment {

    // 映射器 jSON
    private ObjectMapper mapperAleasUiResponse;

    // 构造函数
    public Example02Fragment() {
        super();
        Log.d("rxjava", "Example02Fragment constructor");
        // 过滤器 jSON
        mapperAleasUiResponse = new ObjectMapper();
    }

    public void createAndExecuteObservables() {
        Log.d("rxjava", "Example02Fragment createAndExecuteObservables");
        // 请求随机数
        Observable<AleasDaoResponse> observable = Observable.empty();
        for (int i = 0; i < session.getNbRequests(); i++) {
            // 准备请求
            Request request = session.getRequest();
            request.setId(i);
            // 仅保留客户编号为偶数的观测值
            observable = observable
                    .mergeWith(session.getActivity().getAleas(request).filter(new Func1<AleasDaoResponse, Boolean>() {
                        @Override
                        public Boolean call(AleasDaoResponse aleasDaoResponse) {
                            return aleasDaoResponse.getClientState().getIdClient() % 2 == 0;
                        }
                    })
                            // 在 I/O 线程上执行
                            .subscribeOn(Schedulers.io()));
        }
        // 在事件循环线程上进行观察
        observable = observable.observeOn(AndroidSchedulers.mainThread());
        // 执行这些可观察对象
        subscriptions.add(observable.subscribe(new Action1<AleasDaoResponse>() {
            @Override
            public void call(AleasDaoResponse aleasDaoResponse) {
                showAlea(getDataFrom(aleasDaoResponse));
            }
        }, new Action1<Throwable>() {
            @Override
            public void call(Throwable th) {
                showAlea(getMessagesFromThrowable(th));
                doAnnuler();
            }
        }, new Action0() {
            @Override
            public void call() {
                // 等待结束
                cancelWaiting();
            }
        }));

    }

    private String getDataFrom(AleasDaoResponse aleasDaoResponse) {
        // 提取待显示的信息
        String data;
        try {
            data = mapperAleasUiResponse.writeValueAsString(new AleasUiResponse(aleasDaoResponse));
        } catch (IOException e) {
            data = String.format("[%s,%s]", e.getClass().getName(), e.getMessage());
        }
        return data;
    }

}

此示例与前一个示例(第 38 行)类似。但从前一个示例中获取的可观测值中,仅保留客户编号为偶数的那些(第 42-46 行),这得益于方法 [filter](第 41 行)。

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

Image

9.3.8.3. Exemple-03

类 [Example03Fragment] 如下:


package android.aleas.exemples;

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

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

public class Example03Fragment extends ResponseFragment {

  // 映射器 jSON
  private ObjectMapper mapper;

  // 构造函数
  public Example03Fragment() {
    super();
    Log.d("rxjava", "Example03Fragment constructor");
    // 过滤器 jSON
    mapper = new ObjectMapper();
  }

  public void createAndExecuteObservables() {
    Log.d("rxjava", "Example03Fragment createAndExecuteObservables");
    // 请求随机数
    Observable<List<Integer>> observable = Observable.empty();
    for (int i = 0; i < session.getNbRequests(); i++) {
      // 请求准备
      Request request = session.getRequest();
      request.setId(i);
      // 可观察配置
      observable = observable.mergeWith(session.getActivity().getAleas(request).filter(new Func1<AleasDaoResponse, Boolean>() {
        @Override
        public Boolean call(AleasDaoResponse aleasDaoResponse) {
          return aleasDaoResponse.getClientState().getIdClient() % 2 == 0;
        }
      }).map(new Func1<AleasDaoResponse, List<Integer>>() {
        @Override
        public List<Integer> call(AleasDaoResponse aleasDaoResponse) {
          return aleasDaoResponse.getAleas();
        }
      })
        // 在 I/O 线程上执行
        .subscribeOn(Schedulers.io()));
    }
    // 在事件循环线程上进行观察
    observable = observable.observeOn(AndroidSchedulers.mainThread());
    // 执行这些可观察对象
    subscriptions.add(observable
      .subscribe(new Action1<List<Integer>>() {
                   @Override
                   public void call(List<Integer> aleas) {
                     showAlea(getDataFrom(aleas));
                   }
                 },
        new Action1<Throwable>() {
          @Override
          public void call(Throwable th) {
            showAlea(getMessagesFromThrowable(th));
            doAnnuler();
          }
        },
        new Action0() {
          @Override
          public void call() {
            // 等待结束
            cancelWaiting();
          }
        }
      ));

  }

  private String getDataFrom(List<Integer> aleas) {
    // 提取待显示的信息
    String data;
    try {
      data = mapper.writeValueAsString(aleas);
    } catch (IOException e) {
      data = String.format("[%s,%s]", e.getClass().getName(), e.getMessage());
    }
    return data;
  }

}

此示例与示例-02类似:

  • 第 40 行:定义了与示例-02中相同的可观测量;
  • 第 45 行:通过方法 [map],将上述可观察量输出的每个值转换为 List<Integer> 类型,该列表包含服务器生成的随机数;
  • 第 58 行:此时观测值的类型为 List<Integer>;

10次请求的执行结果如下:

Image

9.3.8.4. Exemple-04

类 [Example04Fragment] 定义如下:


package android.aleas.exemples;

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

public class Example04Fragment extends ResponseFragment {

  // 映射器 jSON
  private ObjectMapper mapper;

  // 构造函数
  public Example04Fragment() {
    super();
    Log.d("rxjava", "Example04Fragment constructor");
    // 过滤器 jSON
    mapper = new ObjectMapper();
  }

  public void createAndExecuteObservables() {
    Log.d("rxjava", "Example03Fragment createAndExecuteObservables");
    // 请求随机数
    Observable<Integer> observable = Observable.empty();
    for (int i = 0; i < session.getNbRequests(); i++) {
      // 请求准备
      Request request = session.getRequest();
      request.setId(i);
      // 可观察量配置
      observable = observable.mergeWith(session.getActivity().getAleas(request).filter(new Func1<AleasDaoResponse, Boolean>() {
        @Override
        public Boolean call(AleasDaoResponse aleasDaoResponse) {
          return aleasDaoResponse.getClientState().getIdClient() % 2 == 0;
        }
      }).flatMap(new Func1<AleasDaoResponse, Observable<Integer>>() {
        @Override
        public Observable<Integer> call(AleasDaoResponse aleasDaoResponse) {
          return Observable.from(aleasDaoResponse.getAleas());
        }
      })
        // 在 I/O 线程上执行
        .subscribeOn(Schedulers.io()));
    }
    // 在事件循环线程上进行观察
    observable = observable.observeOn(AndroidSchedulers.mainThread());
    // 执行这些可观察对象
    subscriptions.add(observable
      .subscribe(new Action1<Integer>() {
                   @Override
                   public void call(Integer alea) {
                     showAlea(String.valueOf(alea));
                   }
                 },
        new Action1<Throwable>() {
          @Override
          public void call(Throwable th) {
            showAlea(getMessagesFromThrowable(th));
            doAnnuler();
          }
        },
        new Action0() {
          @Override
          public void call() {
            // 等待结束
            cancelWaiting();
          }
        }
      ));

  }
}

此示例与示例-03类似,只是第42行中,不再使用方法 [map],而是使用了方法 [flatMap]。

  • 第 55 行:请注意,此时观察到的值类型已变为 Integer

针对 10 个查询,得到以下结果:

Image

此次,观测值数量多于查询次数。

9.3.8.5. Exemple-05

现在,我们将介绍向应用程序添加新的可观测项示例的操作步骤。

假设我们要重现第7.6.4节中的示例[Exemple22h]


package dvp.rxjava.observables.exemples;

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

public class Exemple22h {
    public static void main(String[] args) throws InterruptedException {
        // 进程
        Observable<GroupedObservable<Boolean, Integer>> obs = Observable.range(1, 10).groupBy(i -> i % 2 == 0);
        Process<Integer> process = new Process<>("process", obs.concatMap(g -> g.asObservable()));
        // 订阅
        ProcessUtils.subscribe(1, process);
    }
}
  • 可观察对象 [Observable.range(1, 10)] 的值首先通过方法 [groupBy](第 11 行)按奇偶分组,然后通过方法 [concatMap](第 12 行)合并为单个可观察对象;

步骤 1

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

  

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

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

步骤 2

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

步骤 3

将 [Example05Fragment] 的代码修改如下:


package android.aleas.exemples;

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

public class Example05Fragment extends ResponseFragment {

  // 映射器jSON
  private ObjectMapper mapper;

  // 生成器
  public Example05Fragment() {
    super();
    Log.d("rxjava", "Example05Fragment constructor");
    // 过滤器 jSON
    mapper = new ObjectMapper();
  }

  public void createAndExecuteObservables() {
    Log.d("rxjava", "Example05Fragment createAndExecuteObservables");
    // 功能接口的实例化
    // 过滤器
    Func1<AleasDaoResponse, Boolean> filter = new Func1<AleasDaoResponse, Boolean>() {
      @Override
      public Boolean call(AleasDaoResponse aleasDaoResponse) {
        return aleasDaoResponse.getClientState().getIdClient() % 2 == 0;
      }
    };
    // flatMap
    Func1<AleasDaoResponse, Observable<Integer>> flatMap = new Func1<AleasDaoResponse, Observable<Integer>>() {
      @Override
      public Observable<Integer> call(AleasDaoResponse aleasDaoResponse) {
        return Observable.from(aleasDaoResponse.getAleas());
      }
    };
    // groupBy
    Func1<Integer, Boolean> groupBy = new Func1<Integer, Boolean>() {
      @Override
      public Boolean call(Integer integer) {
        return integer % 2 == 0;
      }
    };
    // concatMap
    Func1<GroupedObservable<Boolean, Integer>, Observable<Integer>> concatMap = new Func1<GroupedObservable<Boolean, Integer>, Observable<Integer>>() {
      @Override
      public Observable<Integer> call(GroupedObservable<Boolean, Integer> integerIntegerGroupedObservable) {
        return integerIntegerGroupedObservable.asObservable();
      }
    };
    // 请求随机数
    Observable<Integer> observable = Observable.empty();
    for (int i = 0; i < session.getNbRequests(); i++) {
      // 准备请求
      Request request = session.getRequest();
      request.setId(i);
      // 可观察配置
      observable = observable.mergeWith(session.getActivity().getAleas(request).filter(filter).flatMap(flatMap))
        .groupBy(groupBy).concatMap(concatMap)
        // 在 I/O 线程上执行
        .subscribeOn(Schedulers.io());
    }
    // 在事件循环线程上进行观察
    observable = observable.observeOn(AndroidSchedulers.mainThread());
    // 执行这些可观察对象
    subscriptions.add(observable
      .subscribe(new Action1<Integer>() {
                   @Override
                   public void call(Integer alea) {
                     showAlea(String.valueOf(alea));
                   }
                 },
        new Action1<Throwable>() {
          @Override
          public void call(Throwable th) {
            showAlea(getMessagesFromThrowable(th));
            doAnnuler();
          }
        },
        new Action0() {
          @Override
          public void call() {
            // 等待结束
            cancelWaiting();
          }
        }
      ));

  }
}
  • 第 67 行:代表示例 04 中的可观察对象:一个整数流;
  • 第 68 行:我们将根据即将定义的布尔条件对该整数流进行分组。最终将得到一个类型为 Observable<GroupedObservable<Boolean, Integer>> 的可观察对象,该对象因此会发出类型为 GroupedObservable<Boolean, Integer> 的元素;
  • 第 68 行:方法 [concatMap] 将根据类型为 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 请求都是发往同一台服务器的,且返回了相同类型的响应。这使我们能够将响应合并(mergeWith)到一个单一的可观察对象中。

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