9. RxJava 在 Android 环境中
9.1. Introduction
这里我们将重新探讨一个已在多份文档中讨论过的应用程序:
- [Android pour les développeurs JEE : un modèle asynchrone pour clients Android](第 4 章);
- [Introduction à la programmation de tablettes Android par l'exemple](第9章);
- [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] 启动。随后控制台将显示日志:
- 第 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 架构模式(模型 – 视图 – 控制器):
![]() |
客户端请求的处理流程如下:
- 请求 - 请求的 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);
- 处理
- 所选操作可利用 parami 参数,这些参数由 [Dispatcher Servlet] Servlet 传递而来。这些参数可能来自多个来源:
- URL 的路径 [/param1/param2/...],
- 来自 URL 的 [p1=v1&p2=v2] 参数
- 浏览器随请求发送的参数;
- 在处理用户请求时,该操作可能需要 [metier] 和 [2b] 层。一旦处理了客户端的请求,该操作可能会触发各种响应。一个典型的例子是:
- 如果请求无法正确处理,则返回错误页面
- 否则显示确认页面
- 该操作会要求显示某个视图 [3]。该视图将显示被称为视图模型的数据。这就是 MVC 中的 M。 该操作将创建视图模型 M [2c],并要求显示视图 V [3];
- 响应——选定的视图 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]
用户
服务器
客户端将包含两个组件:
- 一个 [Présentation] 层(视图+活动);
- 一个 [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 模拟器上运行该项目时,初始化过程如下:

要了解应将什么内容填入 [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 模拟器能够连接到随机数服务器。
使用上述信息执行异步请求,结果如下:

每次请求都会返回一个名为 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]需要三个参数:
- 用于查询服务的 HTTP 方法;
- 服务的 URL;
- 待发送的对象类型为 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. 应用程序视图
![]() |
![]() |
该应用程序包含以下两个视图:
请求视图

响应视图

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]
该请求片段包含以下组件:

该应用程序有一个单一视图,该视图包含两个选项卡:
- [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 | 用于调用随机数生成服务的按钮; |
输入错误会提示:

组件 1 至 6 均为 [TextView] 组件,其名称(按顺序)如下: txtErrorRequests、txtErrorIntervalle、txtErrorCount、txtErrorDelay、txtMsgErreurUrlServiceWeb。
9.3.7.3. 响应中的片段 [ResponseFragment]
响应片段包含以下组件:

编号 | 类型 | 姓名 | 角色 |
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] 方法;
通过在关键方法中设置的各种日志,我们可以观察到应用程序启动时发生的情况:
- 第 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 的执行结果类似于以下内容:

列表中的每个元素都是观测值的字符串 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个请求):

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次请求的执行结果如下:

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 个查询,得到以下结果:

此次,观测值数量多于查询次数。
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
运行应用程序:

并得到以下结果:

- 在 [1] 中,随机数为偶数;在 [2] 中,随机数为奇数;
9.3.8.6. 继续
现在请读者尝试创建自己的示例,并尝试在配置随机数服务器请求的表单中输入不同的数值进行实验。
9.3.9. 结论
我们在 Android 环境中构建了以下架构:
Android客户端:
![]() |
[DAO]层与服务器进行通信,该服务器生成由Android平板电脑显示的随机数。该服务器采用以下两层架构:
![]() |
[DAO]层向随机数服务器发送了n次HTTP请求,而[swing]层则以异步方式等待这些请求的结果以进行显示。 这 n 次 HTTP 请求都是发往同一台服务器的,且返回了相同类型的响应。这使我们能够将响应合并(mergeWith)到一个单一的可观察对象中。
实际上,Android 应用会调用不同的服务器,因此通常不会合并这些响应。针对这些服务器的 HTTP 请求将独立处理,其结果也将通过各自的方法进行观察。

















































