Skip to content

5. 作业 2 - 使用 Android 平板电脑控制 Arduino

接下来我们将学习如何使用平板电脑控制 Arduino 开发板。后续示例将基于本课程中的 [client-android-skel] 项目(参见第 2 段)。

5.1. 项目架构

整个项目将采用以下架构:

  • 模块 [1](Web 服务器/JSON 及 Arduino 设备)将由我们提供;
  • 您需要构建模块 [2],即用于与 Web 服务器/JSON 通信的 Android 平板程序。

5.2. 硬件

您可使用以下组件:

  • 一块配备以太网扩展板、LED 灯和温度传感器的 Arduino 开发板;
  • 一个 miniHub(需与另一名学生共享);
  • 一条用于为 Arduino 供电的 USB 数据线;
  • 两根网线,用于将 Arduino 和电脑连接到同一局域网;
  • 一台安卓平板电脑;

5.2.1. Arduino

以下是将各个组件连接在一起的方法:

  • 将网线从电脑上拔下;
  • 使用网线将电脑与Arduino连接起来;
  • 您手中的 Arduino 已预先编程,其 IP 地址为 [192.168.2.2]。为了让电脑识别 Arduino,您必须为其分配一个位于 [192.168.2] 网络内的 IP 地址。Arduino 已预设为与 IP 地址为 [192.168.2.1] 的电脑进行通信。 操作步骤如下:

进入 [控制面板\网络和互联网\网络和共享中心]:

 
  • 在 [1] 中,单击 [局域网] 链接;
  • 在 [2] 中,点击本地网络的 [属性] 按钮;
  • 在 [3] 中,点击 [本地连接] 适配器的 [IPv4] 属性;
  • 在 [4] 中,为该适配器分配 IP 地址 [192.168.2.1] 和子网掩码 [255.255.255.0];
  • 在 [5] 中,根据需要多次点击 [确定] 以退出向导。

5.2.2. 平板电脑

  • 使用您的 Wi-Fi 适配器,将计算机连接到我们将提供的 Wi-Fi 网络。对您的平板电脑也进行同样的操作;
  • 在命令提示符窗口中输入 [ipconfig],查看电脑的 Wi-Fi IP 地址。您将看到类似 [192.168.x.y] 的地址;

dos>ipconfig
 
Configuration IP de Windows
 
Carte réseau sans fil Wi-Fi :
 
   Suffixe DNS propre à la connexion. . . :
   Adresse IPv6 de liaison locale. . . . .: fe80::39aa:47f6:7537:f8e1%2
   Adresse IPv4. . . . . . . . . . . . . .: 192.168.1.25
   Masque de sous-réseau. . . . . . . . . : 255.255.255.0
   Passerelle par défaut. . . . . . . . . : 192.168.1.1
  • 请检查平板电脑的 Wi-Fi IP 地址。如果不确定如何操作,请咨询您的讲师。您会看到类似 [192.168.x.z] 的地址;
  • 如果电脑的防火墙处于启用状态,请将其禁用 [控制面板\系统和安全\Windows 防火墙];
  • 在命令提示符窗口中,输入命令 [ping 192.168.x.z](其中 [192.168.x.z] 是您的平板电脑 IP 地址)来验证电脑和平板电脑能否通信。此时平板电脑应作出响应:
dos>ping 192.168.1.26

Envoi d'une requête 'Ping'  192.168.1.26 avec 32 octets de données :
Réponse de 192.168.1.26 : octets=32 temps=102 ms TTL=64
Réponse de 192.168.1.26 : octets=32 temps=134 ms TTL=64
Réponse de 192.168.1.26 : octets=32 temps=168 ms TTL=64
Réponse de 192.168.1.26 : octets=32 temps=208 ms TTL=64

Statistiques Ping pour 192.168.1.26:
    Paquets : envoyés = 4, reçus = 4, perdus = 0 (perte 0%),
Durée approximative des boucles en millisecondes :
    Minimum = 102ms, Maximum = 208ms, Moyenne = 153ms

您的系统网络配置现已就绪。

5.2.3. [Genymotion] 模拟器

[Genymotion] 模拟器(参见第 6.9 节)是平板电脑的绝佳替代方案。它的运行速度几乎与平板电脑相当,且无需 Wi-Fi 连接。我们建议使用此方法。您可以在应用程序的最终测试阶段使用平板电脑。

5.3. Arduino编程

本节重点介绍为 Arduino 编写 C 代码:

另请参阅

  • 安装 Arduino 开发 IDE(参见第 6.1 节);
  • 使用 JSON 库(附录,第 6.6 节);
  • 在 Arduino IDE 中,测试 TCP 服务器示例(例如 Web 服务器)和 TCP 客户端示例(例如 Telnet 客户端);
  • 6.1 节中关于 Arduino 编程环境的附录。

Arduino 是一组连接到硬件的引脚。这些引脚可作为输入或输出,其值可以是二进制或模拟的。要控制 Arduino,有两种基本操作:

  • 二进制/模拟写入通过编号标识的引脚;
  • 从指定编号的引脚读取二进制/模拟

除了这两项基本操作外,我们还将添加第三项:

  • 使 LED 在特定时长内以特定频率闪烁。该操作可通过反复调用前两个基本操作来实现。然而,我们在测试中将看到,[DAO] 层与 Arduino 之间的交互耗时约一秒。因此,无法实现例如每 100 毫秒闪烁一次 LED 的效果。所以,我们将直接在 Arduino 上实现这一闪烁功能。

Arduino 的工作原理如下:

  • [DAO]层与Arduino之间的通信通过TCP-IP网络进行,采用JSON(JavaScript对象表示法)格式的文本行进行交换;
  • 启动时,Arduino 会连接到位于 [DAO] 层的一个注册服务器的 100 号端口。它向服务器发送一行文本:
{"id":"cuisine","desc":"duemilanove","mac":"90:A2:DA:00:1D:A7","port":102}

这是一段描述正在连接的 Arduino 的 JSON 字符串:

  • id:Arduino的标识符;
  • desc:Arduino的功能描述。此处仅指定了Arduino的型号;
  • mac:Arduino的MAC地址;
  • 端口:Arduino 将通过该端口等待来自 [DAO] 层的命令。

除端口号(为整数)外,所有信息均为字符串格式。

  • Arduino 在向注册服务器完成注册后,将开始在其向服务器指定的端口(上文中的 102)上监听。它将等待以下格式的 JSON 命令:
{"id":"identifiant","ac":"une_action","pa":{"param1":"valeur1","param2":"valeur2",...}}

这是一个包含以下元素的 JSON 字符串:

  • id:命令的标识符。可以是任意字符串;
  • ac:操作。共有三种:
  • pw引脚写入)用于向引脚写入值,
  • pr引脚读取)用于从引脚读取值,
  • cl闪烁):使 LED 闪烁;
  • pa:操作的参数。这些参数取决于具体操作。
  • Arduino 总是会向其客户端返回响应。这是一个格式如下所示的 JSON 字符串:
{"id":"1","er":"0","et":{"pinx":"valx"}}

其中

  • id:被响应的命令的标识符;
  • er(错误):若发生错误则返回错误代码,否则返回 0;
  • et (状态):一个字典,除读取命令 pr 外始终为空。此时该字典包含所请求的引脚编号 x 的值。

以下是一些示例,用于阐明上述规范:

让第 8 号 LED 以 100 毫秒的间隔闪烁 10 次:

命令
{"id":"1","ac":"cl","pa":{"pin":"8","dur":"100","nb":"10"}}
响应
{"id":"1","er":"0","et":{}}

cl 命令的参数包括:闪光持续时间(dur,单位为毫秒、闪光次数(nb)以及 LED 的引脚编号。

将二进制值 1 写入第 7 引脚:

命令
{"id":"2","ac":"pw","pa":{"pin":"7","mod":"b","val":"1"}}
响应
{"id":"2","er":"0","et":{}}

pw 命令的 pa 参数包括:写入模式 modb 表示二进制,a 表示模拟)、待写入的值 val 以及引脚编号。对于二进制写入,val 为 0 或 1;对于模拟写入,val 的取值范围为 [0,255]。

将模拟值 120 写入引脚 2:

命令
{"id":"3","ac":"pw","pa":{"pin":"2","mod":"a","val":"120"}}
响应
{"id":"3","er":"0","et":{}}

从引脚 0 读取模拟值:

命令
{"id":"4","ac":"pr","pa":{"pin":"0","mod":"a"}}
响应
{"id":"4","er":"0","et":{"pin0":"1023"}}

pr 命令的 pa 参数包括:读取模式(二进制或模拟)以及引脚编号。如果没有错误,Arduino 会将请求引脚的值放入响应的 "et" 键中。在此处,pin0 表示请求了第 0 引脚的值,而 1023 即为该值。在读取模式下,模拟值将在 [0, 1024] 范围内。

我们已经介绍了 clpwpr 这三个命令。有人可能会疑惑,为什么不在 JSON 字符串中使用更明确的字段——例如用 action 代替 ac,用 pinwrite 代替 pw,用 parameters 代替 pa。Arduino 的内存非常有限。然而,与 Arduino 交换的 JSON 字符串会占用内存。因此,我们选择尽可能地缩短它们。

现在让我们来看几个错误情况:

命令
xx
响应
{"id":"","er":"100","et":{}}

发送的命令不符合 JSON 格式。Arduino 返回了错误代码 100。

命令
{"id":"4","ac":"pr","pa":{"mod":"a"}}
响应
{"id":"4","er":"302","et":{}}

发送的 pr 命令未包含 pin 参数。Arduino 返回了错误代码 302。

命令
{"id":"4","ac":"pinread","pa":{"pin":"0","mod":"a"}}
响应
{"id":"4","er":"104","et":{}}

我们发送了一个未知的 pinread 命令(即 pr)。Arduino 返回了错误代码 104。

我们将不再继续演示这些示例。规则很简单:无论发送什么命令,Arduino 都绝不能崩溃。在执行 JSON 命令之前,它会确保该命令有效。一旦发生错误,Arduino 就会停止执行该命令,并向客户端返回 JSON 错误字符串。同样,由于内存空间有限,我们返回的是错误代码而非完整的错误信息。

本文档示例中提供了在 Arduino 上运行的程序代码:

  

将其传输到 Arduino:

  • 将其连接到电脑;
  • 在 [1] 中,打开文件 [arduino_uno.ino]。Arduino IDE 将启动并加载该文件;

:该代码最初是在 Arduino IDE 1.5.x 版本下编写并测试的。此后,IDE 已发布了其他版本。该代码在 Arduino IDE 1.6.x 版本下无法运行。看来 1.6 版与 1.5 版之间存在向后兼容性问题。

  • 在[2-4]中,请指定所使用的Arduino型号;
  • 在 [5-7] 中,指定其连接到 PC 的哪个串口;
  • 在 [8] 中,将 [arduino_uno] 程序上传至 Arduino;

该程序代码注释详尽。感兴趣的读者可自行查阅。我们仅重点标出配置 Arduino 与 PC 之间双向客户端/服务器通信的代码行:


#include <SPI.h>
#include <Ethernet.h>
#include <ajSON.h>
 
// ---------------------------------- CONFIGURATION DE L'ARDUINO UNO
// adresse MAC de l'Arduino UNO
byte macArduino[] = { 
  0x90, 0xA2, 0xDA, 0x0D, 0xEE, 0xC7 };
char * strMacArduino="90:A2:DA:0D:EE:C7";
// l'adresse IP de l'Arduino
IPAddress ipArduino(192,168,2,2);
// son identifiant
char * idArduino="cuisine";
// port du serveur Arduino
int portArduino=102;
// description de l'Arduino
char * descriptionArduino="contrôle domotique";
// le serveur Arduino travaillera sur le port 102
EthernetServer server(portArduino);
// IP du serveur d'enregistrement
IPAddress ipServeurEnregistrement(192,168,2,1); 
// port du serveur d'enregistrement
int portServeurEnregistrement=100;
// le client Arduino du serveur d'enregistrement
EthernetClient clientArduino;
// la commande du client
char commande[100];
// la réponse de l'Arduino
char message[100];
 
// initialisation
void setup() {
  // Le moniteur série permettra de suivre les échanges
  Serial.begin(9600);
  // démarrage de la connection Ethernet
  Ethernet.begin(macArduino,ipArduino);  
  // mémoire disponible
  Serial.print(F("Memoire disponible : "));
  Serial.println(freeRam());
}
 
// boucle infinie
void loop()
{
  ...
}
  • 第 8 行:Arduino 的 MAC 地址。这里并不太重要,因为 Arduino 将与一台 PC 以及一个或多个 Arduino 共同位于一个私有网络中。 MAC 地址只需在该私有网络中保持唯一即可。通常,Arduino 的网络板上有贴纸标注了板的 MAC 地址。如果该贴纸丢失且您不知道板的 MAC 地址,只要遵循私有网络中 MAC 地址唯一性的规则,您可以在第 8 行输入任意值;
  • 第 11 行:网卡的 IP 地址。同样,您可以输入 [192.168.2.x] 格式的任意值,并根据私有网络中不同 Arduino 的情况调整 x 的数值;
  • 第 13 行:Arduino 标识符。必须在同一私有网络中的 Arduino 标识符中保持唯一;
  • 第 15 行:Arduino 的服务端口。可输入任意值;
  • 第 17 行:Arduino 功能的描述。可输入任意内容。由于 Arduino 内存有限,请注意避免使用过长的字符串;
  • 第 21 行:PC 上 Arduino 日志服务器的 IP 地址。请勿修改
  • 第 23 行:此日志服务的端口。不得更改

5.4. Web/JSON 服务器

5.4.1. 安装

Image

Web/JSON 服务器的 Java 二进制文件如下:

 

打开命令提示符,并输入以下命令:

dos>java -jar arduinos-server-01-all-1.0.jar

如果 [java.exe] 不在命令提示符的 PATH 环境变量中,您需要输入 [java.exe] 的完整路径(通常为 C:\Program Files\java\...)。

此时将打开一个 DOS 窗口并显示日志:


.   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::             (v0.5.0.M6)
 
2014-01-06 11:11:35.550  INFO 8408 --- [           main] arduino.rest.metier.Application          : Starting Application on Gportpers3 with PID 8408 (C:\Users\SergeTahÚ\Desktop\part2\server.jar started by ST)
2014-01-06 11:11:35.587  INFO 8408 --- [           main] ationConfigEmbeddedWebApplicationContext : Refreshing org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext@6a4ba620: startup date [Mon Jan 06 11:11:35 CET 2014]; root of context hierarchy
2014-01-06 11:11:36.765  INFO 8408 --- [           main] o.apache.catalina.core.StandardService   : Starting service Tomcat
2014-01-06 11:11:36.766  INFO 8408 --- [           main] org.apache.catalina.core.StandardEngine  : Starting Servlet Engine: Apache Tomcat/7.0.42
2014-01-06 11:11:36.876  INFO 8408 --- [ost-startStop-1] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring embedded WebApplicationContext
2014-01-06 11:11:36.877  INFO 8408 --- [ost-startStop-1] o.s.web.context.ContextLoader            : Root WebApplicationContext: initialization completed in 1293 ms
2014-01-06 11:11:37.084  INFO 8408 --- [ost-startStop-1] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring FrameworkServlet 'dispatcherServlet'
2014-01-06 11:11:37.084  INFO 8408 --- [ost-startStop-1] o.s.web.servlet.DispatcherServlet        : FrameworkServlet 'dispatcherServlet': initialization started
2014-01-06 11:11:37.184  INFO 8408 --- [ost-startStop-1] o.s.w.s.handler.SimpleUrlHandlerMapping  : Mapped URL path [/**/favicon.ico] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2014-01-06 11:11:37.386  INFO 8408 --- [ost-startStop-1] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/arduinos/blink/{idCommande}/{idArduino}/{pin}/{duree}/{nombre}],methods=[GET],params=[],headers=[],consumes=[],produces=[],custom=[]}" onto public java.lang.String arduino.rest.metier.RestMetier.faireClignoterLed(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,javax.servlet.http.HttpServletResponse)
2014-01-06 11:11:37.388  INFO 8408 --- [ost-startStop-1] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/arduinos/commands/{idArduino}],methods=[POST],params=[],headers=[],consumes=[],produces=[],custom=[]}" onto public java.lang.String arduino.rest.metier.RestMetier.sendCommandesJson(java.lang.String,java.lang.String,javax.servlet.http.HttpServletResponse)
2014-01-06 11:11:37.388  INFO 8408 --- [ost-startStop-1] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/arduinos/],methods=[GET],params=[],headers=[],consumes=[],produces=[],custom=[]}" onto public java.lang.String arduino.rest.metier.RestMetier.getArduinos(javax.servlet.http.HttpServletResponse)
2014-01-06 11:11:37.389  INFO 8408 --- [ost-startStop-1] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/arduinos/pinRead/{idCommande}/{idArduino}/{pin}/{mode}],methods=[GET],params=[],headers=[],consumes=[],produces=[],custom=[]}" onto public java.lang.String arduino.rest.metier.RestMetier.pinRead(java.lang.String,java.lang.String,java.lang.String,java.lang.String,javax.servlet.http.HttpServletResponse)
2014-01-06 11:11:37.390  INFO 8408 --- [ost-startStop-1] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/arduinos/pinWrite/{idCommande}/{idArduino}/{pin}/{mode}/{valeur}],methods=[GET],params=[],headers=[],consumes=[],produces=[],custom=[]}" onto public java.lang.String arduino.rest.metier.RestMetier.pinWrite(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,javax.servlet.http.HttpServletResponse)
2014-01-06 11:11:37.463  INFO 8408 --- [ost-startStop-1] o.s.w.s.handler.SimpleUrlHandlerMapping  : Mapped URL path [/**] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2014-01-06 11:11:37.464  INFO 8408 --- [ost-startStop-1] o.s.w.s.handler.SimpleUrlHandlerMapping  : Mapped URL path [/webjars/**] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2014-01-06 11:11:37.881  INFO 8408 --- [ost-startStop-1] o.s.web.servlet.DispatcherServlet        : FrameworkServlet 'dispatcherServlet': initialization completed in 796 ms
Serveur d'enregistrement lancÚ sur 192.168.2.1:100
2014-01-06 11:11:38.101  INFO 8408 --- [       Thread-4] arduino.dao.Recorder                  : Recorder : [11:11:38:101] : [Serveur d'enregistrement : attente d'un client]
2014-01-06 11:11:38.142  INFO 8408 --- [           main] arduino.rest.metier.Application : Started Application in 3.257 seconds
  • 第 11 行:启动了一个嵌入式 Tomcat 服务器;
  • 第 15 行:加载并执行 Spring MVC Servlet [dispatcherServlet];
  • 第 18 行:检测到 REST URL [/arduinos/blink/{commandId}/{ArduinoId}/{pin}/{duration}/{count}];
  • 第 19 行:检测到 REST URL [/arduinos/commands/{idArduino}];
  • 第 20 行:检测到 REST URL [/arduinos/];
  • 第 21 行:检测到 REST URL [/arduinos/pinRead/{commandId}/{ArduinoId}/{pin}/{mode}];
  • 第 22 行:检测到 REST URL [/arduinos/pinWrite/{commandId}/{ArduinoId}/{pin}/{mode}/{value}];
  • 第 26 行:启动 Arduino 日志服务器;

若尚未连接,请将 Arduino 连接至电脑。必须禁用电脑的防火墙。随后,在网页浏览器中输入 URL [http://localhost:8080/arduinos]:

此时应能看到已连接 Arduino 的 ID。如果没有任何显示,请尝试重置 Arduino。它有一个专门用于此目的的重置按钮。

Web/JSON 服务器现已安装完毕。

5.4.2. Web/JSON 服务提供的 URL

参见:项目 [Example-15](参见第 1.16.1 节);

该 Web/JSON 服务采用 Spring MVC 实现,并提供以下 URL:


@Controller
public class WebController {
 
  // business layer
  @Autowired
  private IMetier métier;
 
  // list of arduinos
  @RequestMapping(value = "/arduinos", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
  @ResponseBody
  public String getArduinos() throws JsonProcessingException {
    ...
  }
 
  // flashing
  @RequestMapping(value = "/arduinos/blink/{idCommande}/{idArduino}/{pin}/{duree}/{nombre}", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
  @ResponseBody
  public String faireClignoterLed(@PathVariable("idCommande") String idCommande, @PathVariable("idArduino") String idArduino, @PathVariable("pin") int pin, @PathVariable("duree") int duree, @PathVariable("nombre") int nombre) throws JsonProcessingException {
...
  }
 
  // order dispatch JSON
  @RequestMapping(value = "/arduinos/commands/{idArduino}", method = RequestMethod.POST, produces = MediaType.APPLICATION_JSON_VALUE, consumes = MediaType.APPLICATION_JSON_VALUE)
  @ResponseBody
  public String sendCommandesJson(@PathVariable("idArduino") String idArduino, HttpServletRequest request) throws IOException {
    ...
  }
 
  // pin reading
  @RequestMapping(value = "/arduinos/pinRead/{idCommande}/{idArduino}/{pin}/{mode}", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
  @ResponseBody
  public String pinRead(@PathVariable("idCommande") String idCommande, @PathVariable("idArduino") String idArduino, @PathVariable("pin") int pin, @PathVariable("mode") String mode) throws JsonProcessingException {
    ....
  }
 
  // writing pin
  @RequestMapping(value = "/arduinos/pinWrite/{idCommande}/{idArduino}/{pin}/{mode}/{valeur}", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
  @ResponseBody
  public String pinWrite(@PathVariable("idCommande") String idCommande, @PathVariable("idArduino") String idArduino, @PathVariable("pin") int pin, @PathVariable("mode") String mode, @PathVariable("valeur") int valeur) throws JsonProcessingException {
  ...
  }
}

服务器发送的响应是以下 [Response<T>] 类的 JSON 表示形式:


package client.android.dao.service;
 
import java.util.List;
 
public class Response<T> {
 
    // ----------------- properties
    // operation status
    private int status;
    // any status messages
    private List<String> messages;
    // the body of the reply
    private T body;
 
    // manufacturers
    public Response() {
 
    }
 
    public Response(int status, List<String> messages, T body) {
        this.status = status;
        this.messages = messages;
        this.body = body;
    }
 
    // getters and setters
...
}

URL [/arduinos] 返回类型为 [Response<List<Arduino>>] 的响应,其中 [Arduino] 是以下类:


package android.arduinos.entities;
 
import java.io.Serializable;
 
public class Arduino implements Serializable {
  // data
  private String id;
  private String description;
  private String mac;
  private String ip;
  private int port;
 
// getters and setters
...
}
  • 第 7 行:[id] 是 Arduino 的标识符;
  • 第 8 行:其描述;
  • 第 9 行:其 MAC 地址;
  • 第 10 行:其 IP 地址;
  • 第 11 行:它用于监听命令的端口;

URL:

  • [/arduinos/blink/{commandId}/{ArduinoId}/{pin}/{duration}/{count}];
  • [/arduinos/pinRead/{commandId}/{ArduinoId}/{pin}/{mode}] ;
  • [/arduinos/pinWrite/{commandId}/{ArduinoId}/{pin}/{mode}/{value}] ;
  • [/arduinos/commands/{ArduinoID}];

返回类型为 [Response<ArduinoResponse>] 的响应,其中 [ArduinoResponse] 类表示标准的 Arduino 响应:


public class ArduinoResponse implements Serializable {
 
  private String json;
  private String id;
  private String erreur;
  private Map<String, Object> etat;
 
  // getters and setters
...
}
  • [json]: Arduino 发送的无法解码的 JSON 字符串(发生错误时),否则为 null
  • [id]: Arduino 响应的命令标识符;
  • [error]: 错误代码,若成功则为 0,否则为其他值;
  • [status]:一个包含该命令特定响应的字典。除非该命令请求从Arduino读取某个值,否则该字典通常为空;若请求读取值,该值将被放入此字典中;

5.4.3. Web 服务 / JSON 测试

通过测试以下 URL 熟悉 Web 服务器 / JSON:

URL
角色
http://localhost:8080/arduinos/
返回已连接的Arduino列表
http://localhost:8080/arduinos/
blink/1/kitchen/8/100/20/
使第 8 引脚上的 LED
 (标识为“cuisine”)的LED灯
 每100毫秒闪烁20次。
http://localhost:8080/arduinos/
pinRead/1/cuisine/0/a/
从被Arduino识别为“kitchen”的
 被识别为“厨房”
http://localhost:8080/arduinos/
pinRead/1/kitchen/5/b/
第 5 引脚的二进制读数
 通过“cuisine”标识的Arduino
http://localhost:8080/arduinos/
pinWrite/1/cuisine/8/b/1/
将二进制值 1 写入由
 cuisine
http://localhost:8080/arduinos/
pinWrite/1/kitchen/4/a/100/
将数值 100 以模拟方式写入已识别的 Arduino 的第 4 引脚
 (通过 cuisine 标识)

以下是您应该看到的效果截图:

获取已连接的Arduino列表

从 Web 服务器 /JSON 接收到的 JSON 字符串是一个包含以下字段的对象:

  • [status]:0 表示无错误,否则表示发生错误;
  • [messages]:若发生错误,则为解释该错误的消息列表:
  • [body]:若未发生错误,则为 Arduino 列表。每个 Arduino 由一个包含以下字段的对象描述:
    • [id]: Arduino 的标识符。两个 Arduino 不能拥有相同的标识符;
    • [description]:Arduino功能的简要描述;
    • [mac]:Arduino的MAC地址;
    • [ip]:Arduino的IP地址;
    • [port]:用于监听命令的端口;

让标识为 [cuisine] 的 Arduino 第 8 引脚上的 LED 每 100 毫秒闪烁 20 次

 

从 Web 服务器 /JSON 接收到的 JSON 字符串是一个包含以下字段的对象:

  • [status]:0 表示无错误,否则表示发生错误;
  • [messages]:若发生错误,此处为解释错误原因的消息列表:
  • [body]:若未发生错误,则为 Arduino 的响应:
    • [id]: 命令标识符。该标识符即 [/blink/1] 中的 1。Arduino 会在其响应中包含此命令标识符;
    • [error]: 错误编号。非 0 的值表示发生错误;
    • [state]:仅用于读取引脚。其值为引脚的当前状态;
    • [json]:仅在客户端与服务器之间发生 JSON 错误时使用。其值为 Arduino 发送的错误 JSON 字符串;

通过 [kitchen] 标识的 Arduino 引脚 0 的模拟读取值

 

从 Web 服务器 /json 接收到的 JSON 字符串与前一个类似,唯一不同之处在于 [state] 字段,它表示第 0 号引脚的值。

通过 [kitchen] 标识的 Arduino 引脚 5 的二进制读取结果

 

从 /json Web 服务器接收到的 JSON 字符串与之前的类似。

将数值 1 以二进制形式写入标识为 [kitchen] 的 Arduino 的第 8 引脚

 

从 /json Web 服务器接收到的 JSON 字符串与前一个类似。

测试 URL [http://localhost:8080/arduinos/commands/cuisine] 则更为复杂。处理此 URL 的 Web 服务器 /json 方法需要 POST 请求,而这无法通过浏览器轻松模拟。要测试此 URL,您可以使用安装了 [Advanced REST Client] 扩展程序的 Chrome 浏览器(参见第 6.13 节):

 
  • 在 [1] 中,待测试的 Web/JSON 方法的 URL;
  • 在 [2] 中,用于发送请求的 POST 方法;
  • 在 [3-4] 中,提交的值是 JSON;
  • 在 [5] 中,即要提交的 JSON 字符串。请注意列表起始和结束处的方括号。此处,该列表仅包含一条 JSON 指令,该指令使第 8 引脚每 100 毫秒闪烁 10 次;
  • 在 [6] 中,发送请求;
 
  • 在 [7] 中,服务器发送的 JSON 响应。该对象包含一个包含两个常规字段 [status] 和 [messages] 的对象,以及一个 [body] 字段,其值为对所发送的每个 JSON 命令的 Arduino 响应列表

让我们看看当向 Arduino 发送语法错误的 JSON 命令时会发生什么:

随后我们收到以下响应:

 

我们可以看到,在 Arduino 的响应中,错误代码为 [104],这表明未识别 [xx] 命令。

5.5. Android 客户端测试

以下是已完成的 Android 客户端可执行文件:

  

使用鼠标将上方的 [app-debug.apk] 文件拖放到平板模拟器 [GenyMotion] 上。该文件将被保存并执行。如果尚未启动,请同时启动 web/jSON 服务器。将连接了 LED 的 Arduino 连接到电脑上。Android 客户端允许您远程管理 Arduino。它会向用户显示以下界面。

通过 [CONFIG] 选项卡,您可以连接到服务器并获取已连接的 Arduino 列表:

Image

  • 在 [1] 中,输入分配给您电脑的 IP 地址 [192.168.2.1](参见第 5.2 节)。

[PINWRITE] 选项卡允许您向 Arduino 引脚写入一个值:

Image

Image

[PINREAD] 选项卡允许您从 Arduino 引脚读取值:

Image

[BLINK] 选项卡允许您使 Arduino LED 闪烁:

Image

[COMMAND] 选项卡允许您向 Arduino 发送 JSON 命令:

Image

5.6. Web 服务 / JSON 的 Android 客户端

接下来我们将讨论如何编写 Android 客户端。

5.6.1. 客户端架构

Android 客户端的架构将采用 [Example-15] 项目中的架构(参见第 1.16.2 节);

  • [DAO] 层与 Web/JSON 服务器进行通信;

Android 客户端必须能够同时控制多个 Arduino。例如,我们希望让两个 Arduino 上的两个 LED 灯同时闪烁,而不是一个接一个地闪烁。因此,我们的 Android 客户端将为每个 Arduino 创建一个异步任务,这些任务将并行运行。

5.6.2. Android Studio 客户端项目

将 [client-android-skel] 项目(参见第 2 节)复制为 [client-arduinos-01] 项目(如有必要,请参阅第 1.15 节了解如何复制 Gradle 项目):

5.6.3. 五个 XML 视图

  

将有五个 XML 视图:

  • [blink]:用于让 Arduino LED 闪烁。它与 [BlinkFragment] 片段相关联;
  • [commands]:向 Arduino 发送 JSON 命令。该视图关联 [CommandsFragment] 片段;
  • [config]:用于配置 Web 服务/JSON URL 并获取已连接 Arduino 的初始列表。该视图关联 [ConfigFragment] 片段;
  • [pinread]:读取 Arduino 引脚的二进制或模拟值。它与 [PinReadFragment] 片段相关联;
  • [pinwrite]:向 Arduino 引脚写入二进制或模拟值。它与 [PinWriteFragment] 片段相关联;

目前,这五个 XML 视图的内容均为空:


<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
            android:id="@+id/scrollView1"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content">
 
  <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
                  android:layout_width="match_parent"
                  android:layout_height="match_parent">
  </RelativeLayout>
</ScrollView>
  • 该视图位于 [RelativeLayout] 容器内(第 7–10 行),而该容器本身又位于 [ScrollView] 容器内(第 2–11 行)。这确保了当视图超出平板电脑屏幕尺寸时,我们可以对其进行滚动;

作业:创建这五个 XML 视图。


5.6.4. 片段菜单

我们知道,在 [client-android-skel] 构建的项目中,片段必须与一个菜单相关联,即使该菜单为空。在此,应用将不包含菜单。该项目中已存在一个空菜单;

  

5.6.5. 该应用的五个模块

 

任务:将 [DummyFragment] 片段复制到应用程序的五个片段中,如图 [2] 所示。


[ConfigFragment] 片段具有以下骨架:


package client.android.fragments.behavior;
 
import client.android.R;
import client.android.architecture.core.AbstractFragment;
import client.android.architecture.custom.CoreState;
import client.android.fragments.state.DummyFragmentState;
import org.androidannotations.annotations.EFragment;
import org.androidannotations.annotations.OptionsMenu;
 
@EFragment
@OptionsMenu(R.menu.menu_vide)
public class ConfigFragment extends AbstractFragment {
 
  // fields inherited from parent class -------------------------------------------------------
...

将第 10 行替换为以下内容:


@EFragment(R.layout.config)

任务:对其他四个片段也进行同样的操作,通过调整类中的 [@EFragment] 属性来实现。


片段
视图
ConfigFragment

R.layout.config
PinReadFragment

R.layout.pinread
PinWriteFragment

R.layout.pinwrite
命令片段

R.layout.commands
闪烁片段

R.layout.blink

5.6.6. 片段状态

每个片段都会有一个状态。


任务:将 [DummyFragmentState] 类复制五次,以创建 [2] 中所示的五个状态。


5.6.7. 项目自定义

 

[architecture / custom] 包包含应用程序架构中的可自定义元素。

5.6.7.1. [IMainActivity] 接口

[IMainActivity] 接口定义了片段可以向该活动请求的内容以及应用程序常量。该接口如下所示:


package client.android.architecture.custom;
 
import client.android.architecture.core.ISession;
import client.android.dao.service.IDao;
 
public interface IMainActivity extends IDao {
 
  // session access
  ISession getSession();
 
  // change of view
  void navigateToView(int position, ISession.Action action);
 
  // wait management
  void beginWaiting();
 
  void cancelWaiting();
 
  // constant application -------------------------------------
 
  // debug mode
  boolean IS_DEBUG_ENABLED = true;
 
  // maximum time to wait for server response
  int TIMEOUT = 1000;
 
  // waiting time before executing customer request
  int DELAY = 000;
 
  // basic authentication
  boolean IS_BASIC_AUTHENTIFICATION_NEEDED = false;
 
  // fragment adjacency
  int OFF_SCREEN_PAGE_LIMIT = 1;
 
  // tab bar
  boolean ARE_TABS_NEEDED = true;
 
  // waiting image
  boolean IS_WAITING_ICON_NEEDED = true;
 
  // number of fragments
  int FRAGMENTS_COUNT = 5;
 
  // view n°s
  int VUE_CONFIG = 0;
  int VUE_BLINK = 1;
  int VUE_PINREAD = 2;
  int VUE_PINWRITE = 3;
  int VUE_COMMANDS = 4;
}
  • 第 25、28、31、40 行:[DAO] 层的配置。该应用程序查询一个 Web/JSON 服务器;
  • 第 37 行:该应用程序具有标签页;
  • 第 43 行:该应用程序包含五个片段;
  • 第 46–50 行:五个片段的编号;
  • 第 34 行:片段邻接关系。开发者可在此处设置一个值,范围为 [1, FRAGMENTS_COUNT-1];

5.6.7.2. [CoreState] 类

[CoreState] 类是片段状态的父类:


package client.android.architecture.custom;
 
import client.android.architecture.core.MenuItemState;
import client.android.fragments.state.*;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
 
@JsonIgnoreProperties(ignoreUnknown = true)
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY)
@JsonSubTypes({
  @JsonSubTypes.Type(value = ConfigFragmentState.class),
  @JsonSubTypes.Type(value = BlinkFragmentState.class),
  @JsonSubTypes.Type(value = PinReadFragmentState.class),
  @JsonSubTypes.Type(value = PinWriteFragmentState.class),
  @JsonSubTypes.Type(value = CommandsFragmentState.class)}
)
public class CoreState {
  // fragment visited or not
  protected boolean hasBeenVisited = false;
  // status of any fragment menu
  protected MenuItemState[] menuOptionsState;
 
  // getters and setters
...
}
  • 第 12–16 行:五个片段的状态类必须在此处声明;

5.6.8. [MainActivity] 类

  

[MainActivity] 类的代码如下:


package client.android.activity;
 
import android.support.design.widget.TabLayout;
import android.util.Log;
import client.android.R;
import client.android.architecture.core.AbstractActivity;
import client.android.architecture.core.AbstractFragment;
import client.android.architecture.core.ISession;
import client.android.architecture.custom.IMainActivity;
import client.android.architecture.custom.Session;
import client.android.dao.entities.Arduino;
import client.android.dao.entities.ArduinoCommand;
import client.android.dao.entities.ArduinoResponse;
import client.android.dao.service.Dao;
import client.android.dao.service.IDao;
import client.android.dao.service.Response;
import client.android.fragments.behavior.*;
import org.androidannotations.annotations.Bean;
import org.androidannotations.annotations.EActivity;
import org.androidannotations.annotations.OptionsMenu;
import rx.Observable;
 
import java.util.List;
import java.util.Locale;
 
@EActivity
@OptionsMenu(R.menu.menu_main)
public class MainActivity extends AbstractActivity {
 
  // layer [DAO]
  @Bean(Dao.class)
  protected IDao dao;
  // session
  private Session session;
 
  // methods parent class -----------------------
  @Override
  protected void onCreateActivity() {
    // log
    if (IS_DEBUG_ENABLED) {
      Log.d(className, "onCreateActivity");
    }
    // session
    this.session = (Session) super.session;
    // creation of the five tabs
    for (int i = 0; i < 5; i++) {
      TabLayout.Tab newTab = tabLayout.newTab();
      newTab.setText(getFragmentTitle(i));
      tabLayout.addTab(newTab);
    }
  }
 
  @Override
  protected IDao getDao() {
    return dao;
  }
 
  @Override
  protected AbstractFragment[] getFragments() {
    return new AbstractFragment[]{new ConfigFragment_(), new BlinkFragment_(), new PinReadFragment_(), new PinWriteFragment_(), new CommandsFragment_()};
  }
 
  @Override
  protected CharSequence getFragmentTitle(int position) {
    Locale l = Locale.getDefault();
    switch (position) {
      case 0:
        return getString(R.string.config_titre).toUpperCase(l);
      case 1:
        return getString(R.string.blink_titre).toUpperCase(l);
      case 2:
        return getString(R.string.pinread_titre).toUpperCase(l);
      case 3:
        return getString(R.string.pinwrite_titre).toUpperCase(l);
      case 4:
        return getString(R.string.commands_titre).toUpperCase(l);
    }
    return null;
  }
 
  @Override
  protected void navigateOnTabSelected(int position) {
    // fragment n° position is displayed
    navigateToView(position, ISession.Action.NAVIGATION);
  }
 
  @Override
  protected int getFirstView() {
    return IMainActivity.VUE_CONFIG;
  }
 
  // implémentation IDao -----------------------------------------
}
  • 第 46–50 行:创建应用程序的五个选项卡;
  • 第 48 行:标签页标题由第 63–79 行中的方法提供;
  • 第 60 行实例化了五个片段。由于 AA 注解的存在,片段类即为之前定义的类,后缀为下划线;
  • 第 63–79 行:为每个片段定义标题。这些标题将从文件 [res/values/strings.xml] 中获取
  

[strings.xml] 的内容如下:


<?xml version="1.0" encoding="utf-8"?>
<resources>
 
  <!-- application name -->
  <string name="app_name">[arduinos-client-01]</string>
  <!-- Fragments and tabs -->
  <string name="config_titre">[Config]</string>
  <string name="blink_titre">[Blink]</string>
  <string name="pinread_titre">[PinRead]</string>
  <string name="pinwrite_titre">[PinWrite]</string>
  <string name="commands_titre">[Commands]</string>
 
</resources>

任务:创建上述元素并编译项目。编译过程中不应出现任何错误。


运行该项目。您应在模拟器上看到以下视图:

Image

检查第一个视图显示时生成的日志,并追踪已执行的各个步骤。在标签页之间切换,并继续关注日志。

5.6.9. [config] XML视图

[config] XML 视图将如下所示:

上面的视图由以下 XML 代码生成:


<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
            android:id="@+id/scrollView1"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content">
 
  <RelativeLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent">
 
    <TextView
      android:id="@+id/txt_TitreConfig"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:layout_alignParentTop="true"
      android:layout_centerHorizontal="true"
      android:layout_marginTop="150dp"
      android:text="@string/txt_TitreConfig"
      android:textSize="@dimen/titre"/>
 
    <TextView
      android:id="@+id/txt_UrlServiceRest"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:layout_alignParentLeft="true"
      android:layout_below="@+id/txt_TitreConfig"
      android:layout_marginTop="50dp"
      android:text="@string/txt_UrlServiceRest"
      android:textSize="20sp"/>
 
    <EditText
      android:id="@+id/edt_UrlServiceRest"
      android:layout_width="300dp"
      android:layout_height="wrap_content"
      android:layout_alignBaseline="@+id/txt_UrlServiceRest"
      android:layout_alignBottom="@+id/txt_UrlServiceRest"
      android:layout_marginLeft="20dp"
      android:layout_toRightOf="@+id/txt_UrlServiceRest"
      android:ems="10"
      android:hint="@string/hint_UrlServiceRest"
      android:inputType="textUri">
 
      <requestFocus/>
    </EditText>
 
    <TextView
      android:id="@+id/txt_MsgErreurIpPort"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:layout_alignParentLeft="true"
      android:layout_below="@+id/txt_UrlServiceRest"
      android:layout_marginTop="20dp"
      android:text="@string/txt_MsgErreurUrlServiceRest"
      android:textColor="@color/red"
      android:textSize="20sp"/>
 
    <TextView
      android:id="@+id/txt_arduinos"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:layout_alignParentLeft="true"
      android:layout_below="@+id/txt_MsgErreurIpPort"
      android:layout_marginTop="40dp"
      android:text="@string/titre_list_arduinos"
      android:textColor="@color/blue"
      android:textSize="20sp"/>
 
    <Button
      android:id="@+id/btn_Rafraichir"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:layout_alignBaseline="@+id/txt_arduinos"
      android:layout_alignBottom="@+id/txt_arduinos"
      android:layout_marginLeft="20dp"
      android:layout_toRightOf="@+id/txt_arduinos"
      android:text="@string/btn_rafraichir"/>
 
    <Button
      android:id="@+id/btn_Annuler"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:layout_alignBaseline="@+id/txt_arduinos"
      android:layout_alignBottom="@+id/txt_arduinos"
      android:layout_marginLeft="20dp"
      android:layout_toRightOf="@+id/txt_arduinos"
      android:text="@string/btn_annuler"
      android:visibility="invisible"/>
 
    <ListView
      android:id="@+id/ListViewArduinos"
      android:layout_width="match_parent"
      android:layout_height="200dp"
      android:layout_alignParentLeft="true"
      android:layout_below="@+id/txt_arduinos"
      android:layout_marginTop="30dp"
      android:background="@color/wheat">
    </ListView>
 
  </RelativeLayout>
</ScrollView>

该视图使用了在 [res/values/strings] 文件中定义的字符串(第 15、25、37、50、61、73 行中的 android:text):

  

<?xml version="1.0" encoding="utf-8"?>
<resources>
 
    <string name="app_name">android-domotique</string>
 
    <!-- Fragments and tabs -->
    <string name="config_titre">[Config]</string>
    <string name="blink_titre">[Blink]</string>
    <string name="pinread_titre">[PinRead]</string>
    <string name="pinwrite_titre">[PinWrite]</string>
    <string name="commands_titre">[Commands]</string>
 
    <!-- Config -->
    <string name="txt_TitreConfig">Se connecter au serveur</string>
    <string name="txt_UrlServiceRest">Url du service web / jSON</string>
    <string name="txt_MsgErreurUrlServiceRest">L\'Url du service doit être entrée sous la forme Ip1.Ip2.Ip3.IP4:Port/contexte</string>
    <string name="hint_UrlServiceRest">ex (192.168.1.120:8080/rest)</string>
    <string name="btn_annuler">Annuler</string>
    <string name="btn_rafraichir">Rafraîchir</string>
    <string name="titre_list_arduinos">Liste des Arduinos connectés</string>
 
</resources>

该视图使用了在 [res/values/colors] 文件中定义的颜色(第 51 行和第 62 行的 android:textColor):

  

<?xml version="1.0" encoding="utf-8"?>
<resources>
  <color name="colorPrimary">#3F51B5</color>
  <color name="colorPrimaryDark">#303F9F</color>
  <color name="colorAccent">#FF4081</color>
  <color name="floral_white">#FFFAF0</color>
  <!-- app -->
  <color name="red">#FF0000</color>
  <color name="blue">#0000FF</color>
  <color name="wheat">#FFEFD5</color>
</resources>

该视图使用了在 [res/values/dimens] 文件中定义的尺寸(第 16 行中的 android:textSize):

  

<resources>
  <!-- Default screen margins, per the Android Design guidelines. -->
  <dimen name="activity_horizontal_margin">16dp</dimen>
  <dimen name="activity_vertical_margin">16dp</dimen>
  <dimen name="fab_margin">16dp</dimen>
  <dimen name="appbar_padding_top">8dp</dimen>
  <!-- appli -->
  <dimen name="titre">30dp</dimen>
</resources>

并非所有尺寸都采用了此方法。不过,这是推荐的做法。它允许您在单一位置修改尺寸。


任务:创建上述元素。


再次运行您的项目。您应看到以下视图:

Image

5.6.10. [ConfigFragment] 片段

为了处理新的 [config] 视图,[ConfigFragment] 片段的代码如下所示:


package client.android.fragments.behavior;
 
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ListView;
import android.widget.TextView;
import client.android.R;
import client.android.architecture.core.AbstractFragment;
import client.android.architecture.custom.CoreState;
import client.android.architecture.custom.IMainActivity;
import client.android.fragments.state.ConfigFragmentState;
import org.androidannotations.annotations.Click;
import org.androidannotations.annotations.EFragment;
import org.androidannotations.annotations.OptionsMenu;
import org.androidannotations.annotations.ViewById;
 
@EFragment(R.layout.config)
@OptionsMenu(R.menu.menu_vide)
public class ConfigFragment extends AbstractFragment {
 
  // visual interface elements
  @ViewById(R.id.btn_Rafraichir)
  protected Button btnRafraichir;
  @ViewById(R.id.btn_Annuler)
  protected Button btnAnnuler;
  @ViewById(R.id.edt_UrlServiceRest)
  protected EditText edtUrlServiceRest;
  @ViewById(R.id.txt_MsgErreurIpPort)
  protected TextView txtMsgErreurUrlServiceRest;
  @ViewById(R.id.ListViewArduinos)
  protected ListView listArduinos;
 
  @Click(R.id.btn_Rafraichir)
  protected void doRafraichir() {
  }
 
  // fragment lifecycle management -------------------------------------
 
  @Override
  public CoreState saveFragment() {
    return new ConfigFragmentState();
  }
 
  @Override
  protected int getNumView() {
    return IMainActivity.VUE_CONFIG;
  }
 
  @Override
  protected void initFragment(CoreState previousState) {
 
  }
 
  @Override
  protected void initView(CoreState previousState) {
    // 1st visit?
    if(previousState==null){
      txtMsgErreurUrlServiceRest.setVisibility(View.INVISIBLE);
    }
  }
 
  @Override
  protected void updateOnSubmit(CoreState previousState) {
 
  }
 
  @Override
  protected void updateOnRestore(CoreState previousState) {
  }
 
  @Override
  protected void notifyEndOfUpdates() {
    // buttons
    initButtons();
  }
 
  @Override
  protected void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled) {
  }
 
  // méthodes privées --------------------------------------------
 
  private void initButtons() {
    // the [Execute] button replaces the [Cancel] button
    btnAnnuler.setVisibility(View.INVISIBLE);
    btnRafraichir.setVisibility(View.VISIBLE);
  }
}
  • 第 23–32 行:视觉界面元素;
  • 第 58–60 行:首次访问片段时,隐藏错误消息;
  • 第 73–76 行:每次显示片段时,[取消] 按钮会被隐藏(第 82 行),而 [刷新] 按钮会被显示(第 86–87 行)。实际上,在此应用中,当异步操作正在进行时无法显示片段,因此 [取消] 按钮是可见的;

任务:实现上述功能。


运行此新版本。此时初始视图应如下所示:

Image

5.6.10.1. [刷新] 按钮

目前,我们将按以下方式处理 [刷新] 按钮的点击事件:


@Click(R.id.btn_Rafraichir)
  protected void doRafraichir() {
    // we're going to launch a task - we're preparing the wait
    beginWaiting(1);
  }
 
  @Click(R.id.btn_Annuler)
  protected void doAnnuler() {
    if (isDebugEnabled) {
      Log.d(className, "Annulation demandée");
    }
    // asynchronous tasks are cancelled
    cancelRunningTasks();
  }
 
  protected void beginWaiting(int numberOfRunningTasks) {
    // prepare to wait for tasks
    beginRunningTasks(numberOfRunningTasks);
    // the [Cancel] button replaces the [Refresh] button
    btnRafraichir.setVisibility(View.INVISIBLE);
    btnAnnuler.setVisibility(View.VISIBLE);
}
  // fragment lifecycle management -------------------------------------
...
  @Override
  protected void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled) {
    // buttons in their original state
    initButtons();
  }
 
  // méthodes privées --------------------------------------------
 
  private void initButtons() {
    // the [Execute] button replaces the [Cancel] button
    btnAnnuler.setVisibility(View.INVISIBLE);
    btnRafraichir.setVisibility(View.VISIBLE);
  }
  • 第 1-5 行:点击 [刷新] 按钮时执行的方法;
  • 第 4 行:开始等待;
  • 第 18 行:我们将要启动的异步任务数量传递给父类。此时将显示加载图标;
  • 第 20-21 行:此等待操作将导致 [Cancel] 按钮出现、[Refresh] 按钮消失,并显示加载图标。除此之外不会发生其他情况。不过,用户可以点击 [Cancel] 按钮。此时将执行第 7-14 行中的方法;
  • 第 13 行:请求父类取消所有任务。父类将执行此操作,并进而调用第 25–29 行中的方法,以通知所有任务已完成。参数 [runningTasksHaveBeenCanceled] 的值将设为 true,表示任务已被取消;
  • 第35–36行:[取消]按钮将消失,而[刷新]按钮将重新出现。

任务:进行上述修改后运行项目。验证 [Refresh] 按钮是否能启动等待,[Cancel] 按钮是否能停止等待。观察日志。


5.6.10.2. 输入验证

在之前的版本中,我们未对输入的 URL 进行验证。为进行验证,我们在 [ConfigFragment] 中添加以下代码:


// entered values
  private String urlServiceRest;
 
  @Click(R.id.btn_Rafraichir)
  protected void doRafraichir() {
    // check entries
    if (!pageValid()) {
      return;
    }
    // we're going to launch a task - we're preparing the wait
    beginWaiting(1);
  }
 
  // input verification
  private boolean pageValid() {
    // initially no error msg
    txtMsgErreurUrlServiceRest.setVisibility(View.INVISIBLE);
    // retrieve server IP and port
    urlServiceRest = String.format("http://%s", edtUrlServiceRest.getText().toString().trim());
    // we check its validity
    try {
      URI uri = new URI(urlServiceRest);
      String host = uri.getHost();
      int port = uri.getPort();
      if (host == null || port == -1) {
        throw new Exception();
      }
    } catch (Exception ex) {
      // error msg display
      txtMsgErreurUrlServiceRest.setVisibility(View.VISIBLE);
      // back to UI
      return false;
    }
    // it's good
    return true;
  }
  • 第 2 行:输入的 URL;
  • 第 7–9 行:在执行任何操作之前,我们先检查输入的有效性;
  • 第 19 行:获取输入的 URL 并为其添加前缀 [http://];
  • 第 22 行:尝试使用该 URL 构建一个 URI(统一资源标识符)对象。如果输入的 URL 语法错误,将抛出异常;
  • 第 23–27 行:如果 URI 有效,但 [host==null] 且 [port==-1],则抛出异常。这是可能发生的情况;
  • 第 30 行:已发生异常。显示错误信息;
  • 第 32 行:返回 [false] 以表示页面无效;
  • 第 35 行:未发生错误。返回 [true] 表示页面有效;

作业:实现上述功能。


测试此新版本,并验证无效的 URL 是否被正确标记。

5.6.10.3. 显示 Arduino 列表

  

不同的视图需要显示已连接的 Arduino 列表。为此,我们将定义不同的类和一个 XML 视图:

  • Arduino 将由 [Arduino] 类 [1] 表示;
  • [CheckedArduino]类[1]继承自[Arduino]类,我们在其中添加了一个布尔变量,用于指示该Arduino在列表中是否已被选中;

[Arduino]类是服务器已使用的类,并在第5.4.2节中介绍过。其定义如下:


package android.arduinos.entities;
 
import java.io.Serializable;
 
public class Arduino implements Serializable {
  // data
  private String id;
  private String description;
  private String mac;
  private String ip;
  private int port;
 
// getters and setters
...
}
  • 第 7 行:[id] 是 Arduino 的标识符;
  • 第 8 行:其描述;
  • 第 9 行:其 MAC 地址;
  • 第 10 行:其 IP 地址;
  • 第 11 行:它用于监听命令的端口;

该类对应于向服务器请求已连接Arduino列表时收到的JSON字符串:

[CheckedArduino] 类继承自 [Arduino] 类:


package android.arduinos.entities;
 
public class CheckedArduino extends Arduino {
    private static final long serialVersionUID = 1L;
    // an Arduino can be selected
    private boolean isChecked;
 
    // manufacturer
    public CheckedArduino(Arduino arduino, boolean isChecked) {
        // parent
        super(arduino.getId(), arduino.getDescription(), arduino.getMac(), arduino.getIp(), arduino.getPort());
        // local
        this.isChecked = isChecked;
    }
 
    // getters and setters
    public boolean isChecked() {
        return isChecked;
    }
 
    public void setChecked(boolean isChecked) {
        this.isChecked = isChecked;
    }
 
}
  • 第 3 行:[CheckedArduino] 类继承自 [Arduino] 类;
  • 第 6 行:我们添加了一个布尔变量,用于指示是否已从显示的 Arduino 列表中选定了一个 Arduino;

在 [ConfigFragment] 中,我们将模拟获取已连接 Arduino 的列表。

  

  @ViewById(R.id.ListViewArduinos)
  protected ListView listArduinos;
..
  @Click(R.id.btn_Rafraichir)
  protected void doRafraichir() {
    // check entries
    if (!pageValid()) {
      return;
    }
    // we're going to launch a task - we're preparing the wait
    beginWaiting(1);
    // we clean up the Arduinos list
    clearArduinos();
    // request the list of Arduinos running in the background
    getArduinosInBackground();
  }
 
  private void getArduinosInBackground() {
   ...
  }
 
  // raz list of Arduinos
  private void clearArduinos() {
    // create an empty list
    List<String> strings = new ArrayList<>();
    // we display it
    listArduinos.setAdapter(new ArrayAdapter<String>(activity, android.R.layout.simple_list_item_1, android.R.id.text1, strings));
}
  • 第 2 行:显示连接到服务器的 Arduino 的 ListView
  • 第 5 行:获取已连接 Arduino 列表的方法;
  • 第 11 行:告知父类我们将启动一个异步任务;
  • 第 12 行:清空当前显示的 Arduino 列表;
  • 第 15 行:作为后台任务请求已连接 Arduino 的列表;
  • 第 23–28 行:清除当前显示的 Arduino 列表的方法;

[getArduinosInBackground] 方法如下:


  private void getArduinosInBackground() {
    // create a fictitious arduino list
    List<Arduino> arduinos = new ArrayList<>();
    for (int i = 0; i < 20; i++) {
      arduinos.add(new Arduino("id" + i, "desc" + i, "mac" + i, "ip" + i, i));
    }
    // we simulate a server response
    Response<List<Arduino>> response = new Response<>();
    response.setBody(arduinos);
    // we cancel the wait
    cancelWaitingTasks();
    // change the buttons
    initButtons();
    // we consume the answer
    consumeArduinosResponse(response);
}
  • 第 3–6 行:创建一个包含 20 个 Arduino 的列表;
  • 第 8–9 行:构建类型为 [Response<List<Arduino>>](第 5.4.2 节)的响应,该响应将封装已创建的 Arduino 列表;
  • 第 11 行:取消等待;
  • 第 13 行:将按钮重置为初始状态;
  • 第 15 行:处理响应;

[consumeArduinosResponse] 方法如下:


  // response display
  private void consumeArduinosResponse(Response<List<Arduino>> response) {
    // mistake?
    if (response.getStatus() != 0) {
      // display
      showAlert(response.getMessages());
      // back to Ui
      return;
    }
    // we create a list of [CheckedArduino]
    List<CheckedArduino> checkedArduinos = new ArrayList<>();
    for (Arduino arduino : response.getBody()) {
      checkedArduinos.add(new CheckedArduino(arduino, false));
    }
    // we display them
    showArduinos(checkedArduinos);
}
  • 第 4-11 行:检查服务器发送的响应中的错误代码:
  • 第 4 行:如果错误代码不为零;
  • 第 6 行:显示服务器存储在响应 [messages] 字段中的消息;
  • 第 8 行:返回用户界面;
  • 第 11-16 行:如果没有错误,将接收到的 Arduino 列表转换为 List<CheckedArduino> 类型后显示;

[showArduinos] 方法如下:


  private void showArduinos(List<CheckedArduino> checkedArduinos) {
    // create a list of Strings from the list of Arduinos
    List<String> strings = new ArrayList<>();
    for (CheckedArduino checkedArduino : checkedArduinos) {
      strings.add(checkedArduino.toString());
    }
    // we display it
    listArduinos.setAdapter(new ArrayAdapter<>(activity, android.R.layout.simple_list_item_1, android.R.id.text1, strings));
}

任务:进行上述修改并运行你的项目。


点击 [刷新] 按钮后,您应看到以下视图:

Image

[1] 处的输入内容不会被使用。因此,只要符合预期格式,您可以输入任意内容。

5.6.10.4. Arduino 显示模板

目前,已连接的 Arduino 在 [配置] 视图中显示如下:

Image

现在,我们希望将其显示为如下形式:

Image

  • 在 [1] 处,一个复选框,用于选择 Arduino。当需要显示不可选的 Arduino 列表时,该复选框将被隐藏;
  • 在 [2] 处,显示 Arduino 的 ID;
  • 在 [3] 处,显示其描述;

以下内容基于第 1.20 节中的 [example-19] 和 [example-19B] 项目所发展的概念。如有必要,请复习相关内容。

首先,我们创建一个视图,用于显示 Arduino 列表中的某个项目:

 

上述 [listarduinos_item] 视图的代码如下:


<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/RelativeLayout1"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@color/wheat"
    android:orientation="vertical" >
 
    <CheckBox
        android:id="@+id/checkBoxArduino"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentLeft="true"
        android:layout_alignParentTop="true"
        android:layout_toRightOf="@+id/txt_arduino_description" />
 
    <TextView
        android:id="@+id/TextView1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignBaseline="@+id/checkBoxArduino"
        android:layout_marginLeft="40dp"
        android:text="@string/txt_arduino_id" />
 
    <TextView
        android:id="@+id/txt_arduino_id"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignBaseline="@+id/checkBoxArduino"
        android:layout_alignParentTop="true"
        android:layout_toRightOf="@+id/TextView1"
        android:text="@string/dummy"
        android:textColor="@color/blue" />
 
    <TextView
        android:id="@+id/TextView2"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignBaseline="@+id/checkBoxArduino"
        android:layout_alignParentTop="true"
        android:layout_marginLeft="20dp"
        android:layout_toRightOf="@+id/txt_arduino_id"
        android:text="@string/txt_arduino_description" />
 
    <TextView
        android:id="@+id/txt_arduino_description"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignBaseline="@+id/checkBoxArduino"
        android:layout_alignTop="@+id/TextView2"
        android:layout_toRightOf="@+id/TextView2"
        android:text="@string/dummy"
        android:textColor="@color/blue" />
 
</RelativeLayout>
  • 第 9–15 行:复选框;
  • 第 17–23 行:文本 [Id: ];
  • 第 25–33 行:此处将输入 Arduino ID;
  • 第 35–43 行:文本 [Description: ];
  • 第 45–53 行:此处将输入 Arduino 描述;

此视图使用了在 [res/values/strings.xml] 中定义的文本(第 23、32、43 行):


    <string name="dummy">XXXXX</string>
 
    <!--  listarduinos_item -->
    <string name="txt_arduino_id">Id : </string>
<string name="txt_arduino_description">Description : </string>

该视图还使用了在 [res / values / colors.xml] 中定义的颜色(第 33 行、第 53 行):


<?xml version="1.0" encoding="utf-8"?>
<resources>
 
    <color name="red">#FF0000</color>
    <color name="blue">#0000FF</color>
    <color name="wheat">#FFEFD5</color>
    <color name="floral_white">#FFFAF0</color>
 
</resources>

Arduino 列表中某项的视图管理器

  

[ListArduinosAdapter] 类是 [ListView] 调用以显示 Arduino 列表中每个项目的类。其代码如下:


package istia.st.android.vues;
 
import istia.st.android.R;
...
 
public class ListArduinosAdapter extends ArrayAdapter<CheckedArduino> {
 
    // the arduino board
    private List<CheckedArduino> arduinos;
    // execution context
    private Context context;
    // the layout id for displaying a line in the arduino list
    private int layoutResourceId;
    // whether or not the line contains a checkbox
    private Boolean selectable;
 
    // manufacturer
    public ListArduinosAdapter(Context context, int layoutResourceId, List<CheckedArduino> arduinos, Boolean selectable) {
        // parent
        super(context, layoutResourceId, arduinos);
        // memorize information
        this.arduinos = arduinos;
        this.context = context;
        this.layoutResourceId = layoutResourceId;
        this.selectable = selectable;
    }
 
    @Override
    public View getView(final int position, View convertView, ViewGroup parent) {
...
    }
}
  • 第 18 行:类构造函数接受四个参数:当前运行的活动、用于显示数据源中每个项的视图 ID、填充列表的数据源,以及一个布尔值,用于指示是否显示与每个 Arduino 关联的复选框;
  • 第 8–15 行:这四项信息被保存在本地;

第 29 行:[getView] 方法负责在 [ListView] 中生成第 [position] 个视图并处理其事件。其代码如下:


@Override
    public View getView(int position, View convertView, ViewGroup parent) {
        // the current arduino
        final CheckedArduino arduino = arduinos.get(position);
        // create the current line
        View row = ((Activity) context).getLayoutInflater().inflate(layoutResourceId, parent, false);
        // retrieve references on [TextView]
        TextView txtArduinoId = (TextView) row.findViewById(R.id.txt_arduino_id);
        TextView txtArduinoDesc = (TextView) row.findViewById(R.id.txt_arduino_description);
        // fill in the line
        txtArduinoId.setText(arduino.getId());
        txtArduinoDesc.setText(arduino.getDescription());
        // the CheckBox is not always visible
        CheckBox ck = (CheckBox) row.findViewById(R.id.checkBoxArduino);
        ck.setVisibility(selectable ? View.VISIBLE : View.INVISIBLE);
        if (selectable) {
            // we assign its value
            ck.setChecked(arduino.isChecked());
            // we manage the click
            ck.setOnCheckedChangeListener(new OnCheckedChangeListener() {
 
                public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
                    arduino.setChecked(isChecked);
                }
            });
        }
        // we return the line
        return row;
    }
  • 第 2 行:第一个参数是要创建的行在 [ListView] 中的位置。它也是本地存储的 Arduino 列表中的位置;
  • 第 4 行:我们获取将与该行关联的 Arduino 的引用;
  • 第 6 行:根据 [listarduinos_item.xml] 视图构建当前行;
  • 第 8–9 行:获取两个 [TextView] 的引用;
  • 第 11–12 行:为两个 [TextView] 设置其值;
  • 第 14 行:获取复选框的引用;
  • 第 15 行:根据最初传递给构造函数的 [selectable] 值,决定是否显示该复选框;
  • 第 16 行:如果复选框存在;
  • 第 18 行:将当前 Arduino 的 [isChecked] 值赋给该复选框;
  • 第 20–26 行:处理复选框的点击事件;
  • 第 23 行:将复选框的值存储到当前 Arduino 中;

管理 Arduino 列表

Arduino列表的显示目前由[ConfigFragment]类的两个方法处理:

  • [clearArduinos]:显示一个空列表;
  • [showArduinos]:显示服务器返回的列表;

这两个方法的工作原理如下:


  // raz list of Arduinos
  private void clearArduinos() {
    // an empty list is displayed
    ListArduinosAdapter adapter = new ListArduinosAdapter(getActivity(), R.layout.listarduinos_item, new ArrayList<CheckedArduino>(), false);
    listArduinos.setAdapter(adapter);
  }
 
  // arduinos list display
  private void showArduinos(List<CheckedArduino> checkedArduinos) {
    // display Arduinos
    ListArduinosAdapter adapter = new ListArduinosAdapter(getActivity(), R.layout.listarduinos_item, checkedArduinos, false);
    listArduinos.setAdapter(adapter);
}

任务:进行这些修改并测试新应用。


Image

5.6.10.5. 会话

会话是存储片段与活动之间共享信息的地方。所有片段都需要显示已连接的 Arduino 列表。因此,会话的初始版本如下所示:


package client.android.architecture.custom;
 
import client.android.activity.CheckedArduino;
import client.android.architecture.core.AbstractSession;
 
import java.util.ArrayList;
import java.util.List;
 
public class Session extends AbstractSession {
  // data to be shared between fragments themselves and between fragments and activities
  // elements that cannot be serialized as jSON must be annotated with @JsonIgnore
  // don't forget the getters and setters required for serialization / deserialization jSON
 
  // the Arduinos list
  private List<CheckedArduino> checkedArduinos = new ArrayList<>();
 
  // getters and setters
...
}

任务:创建上文所示的 [Session] 类。


要创建此会话,我们需要按以下方式修改现有代码:


  // response display
  private void consumeArduinosResponse(Response<List<Arduino>> response) {
    // mistake?
    if (response.getStatus() != 0) {
      // display
      showAlert(response.getMessages());
      // cancellation
      doAnnuler();
      // back to Ui
      return;
    }
    // we create a list of [CheckedArduino]
    List<CheckedArduino> checkedArduinos = new ArrayList<>();
    for (Arduino arduino : response.getBody()) {
      checkedArduinos.add(new CheckedArduino(arduino, false));
    }
    // we put it in session
    session.setCheckedArduinos(checkedArduinos);
    // we display them
    showArduinos(checkedArduinos);
    // we cancel the wait
    cancelWaitingTasks();
}
  • 第 18 行:将前几行创建的 Arduino 列表放入会话中;

5.6.10.6. 片段状态管理

当设备旋转时,视图的视觉组件(默认情况下)将以视图设计时的状态进行渲染:

  • [ListView] 包含设计者放置其中的项目;
  • 错误消息处于设计者放置时的可见或不可见状态;

在恢复片段时,设计时视觉组件的状态可能合适,也可能不合适。这里的情况是怎样的?

  • [ListView] 必须显示已连接的 Arduino 列表。因此,设计时 [ListView] 的值无法被使用;
  • 用于显示错误消息的 [TextView] 必须恢复到保存时的可见或隐藏状态。其在设计时的值可能不适用于这两种情况;

因此,在保存片段的状态时,我们必须保存这两个组件的状态:

  • 已连接的 Arduino 列表;
  • 输入 Web 服务 URL/JSON 时错误消息的可见性(显示/隐藏);

由于 Arduino 列表存在于会话中,因此会自动保存。错误消息的可见性将存储在以下 [ConfigFragmentState] 类中:

  

package client.android.fragments.state;
 
import client.android.architecture.custom.CoreState;
 
public class ConfigFragmentState extends CoreState {
 
  // visibility error message
  private boolean txtMsgErreurUrlServiceRestVisible;
 
  // getters and setters
...
}

任务:创建上文所示的 [ConfigFragmentState] 类。


要正确恢复片段的状态,必须修改其 [getNumView] 和 [saveFragment] 方法。例如,[BlinkFragment] 片段的相应方法目前如下所示:


  @Override
  public CoreState saveFragment() {
    // save the fragment
    DummyFragmentState state=new DummyFragmentState();
    // ...
    return state;
    // if there's nothing to save, do [return new CoreState();] and delete class [DummyFragmentState]
  }
 
  @Override
  protected int getNumView() {
    // return the fragment number in the table of fragments managed by the activity (cf MainActivity)
    return 0;
}

如果不执行任何操作,第 6 行渲染的状态将保存在 [AbstractSession] 类(下文第 5 行)的 CoreState[] coreStates 数组的第 0 个元素(第 13 行)中:


public class AbstractSession implements ISession {
  ...
 
  // view status
  private CoreState[] coreStates = new CoreState[0];
...

但是,它必须保存在 [MainActivity] 类中定义的片段数组中与片段 ID [BlinkFragment] 对应的元素中(如下第 9 行):


@EActivity
@OptionsMenu(R.menu.menu_main)
public class MainActivity extends AbstractActivity {
 
  ...
 
  @Override
  protected AbstractFragment[] getFragments() {
    return new AbstractFragment[]{new ConfigFragment_(), new BlinkFragment_(), new PinReadFragment_(), new PinWriteFragment_(), new CommandsFragment_()};
  }
 
 

片段 ID 已在 [IMainActivity] 接口中定义:


public interface IMainActivity extends IDao {
 
  ...
 
  // view n°s
  int VUE_CONFIG = 0;
  int VUE_BLINK = 1;
  int VUE_PINREAD = 2;
  int VUE_PINWRITE = 3;
  int VUE_COMMANDS = 4;
}

最终,如果我们编写如下代码,[BlinkFragment] 片段的状态将得到正确管理:


  @Override
  public CoreState saveFragment() {
    // save the fragment
    DummyFragmentState state=new DummyFragmentState();
    // ...
    return state;
    // if there's nothing to save, do [return new CoreState();] and delete class [DummyFragmentState]
  }
 
  @Override
  protected int getNumView() {
    // return the fragment number in the table of fragments managed by the activity (cf MainActivity)
    return IMainActivity.VUE_BLINK;
}
  • 第 14 行:返回该 Activity 管理的片段数组中的片段 ID [BlinkFragment];

此外,作为片段状态父类的 [CoreState] 类目前如下所示(参见第 5.6.7.2 节):


package client.android.architecture.custom;
 
import client.android.architecture.core.MenuItemState;
import client.android.fragments.state.*;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
 
@JsonIgnoreProperties(ignoreUnknown = true)
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY)
@JsonSubTypes({
  @JsonSubTypes.Type(value = ConfigFragmentState.class),
  @JsonSubTypes.Type(value = BlinkFragmentState.class),
  @JsonSubTypes.Type(value = PinReadFragmentState.class),
  @JsonSubTypes.Type(value = PinWriteFragmentState.class),
  @JsonSubTypes.Type(value = CommandsFragmentState.class)}
)
public class CoreState {
  // fragment visited or not
  protected boolean hasBeenVisited = false;
  // status of any fragment menu
  protected MenuItemState[] menuOptionsState;
 
  // getters and setters
....
}
  • 第 12–16 行:[DummyFragmentState] 类未列在 [CoreState] 类的子类中。 然而,[BlinkFragment] 类的 [saveFragment] 方法目前返回的是 [DummyFragmentState] 类型。若保持现状,会话的序列化/反序列化将失败,导致会话无法恢复,进而引发应用程序崩溃;

必须将 [BlinkFragment] 片段的 [saveFragment] 方法重写为如下形式:


  @Override
  public CoreState saveFragment() {
    // save the fragment
    BlinkFragmentState state=new BlinkFragmentState();
    // ...
    return state;
    // if there's nothing to save, do [return new CoreState();] and delete class [DummyFragmentState]
}

任务:在每个片段中,修改 [getNumView] 方法使其返回片段编号,并修改 [saveFragment] 方法使其返回片段状态类的实例(如上所示)。


5.6.10.7. 片段生命周期管理

这里我们将重点关注 [ConfigFragment] 片段的生命周期,特别是以下四个方法:

  • [saveFragment]:必须保存片段的状态,以便日后恢复;
  • [initFragment]:在必要时初始化片段的某些字段。该方法在应用程序启动时以及设备每次旋转时都会被调用。更准确地说,当片段在前两个事件之一发生后变得可见时,该方法会被调用;
  • [initView]:在必要时初始化片段的某些视图组件。该方法在每次调用 [initFragment] 之后,以及当片段在某个时刻移出显示区域导致视图需要重绘时被调用。与前文类似,该方法在上述事件发生后片段再次可见时被调用;
  • [updateOnRestore]:该方法在设备旋转后,以及发生导航时,于前两个方法之后执行。其作用是恢复片段的先前状态;

这些方法如下:


// arduinos list adapter
  private ListArduinosAdapter adapterListArduinos;
 
...
  // fragment lifecycle management -------------------------------------
 
  @Override
  public CoreState saveFragment() {
    ConfigFragmentState state = new ConfigFragmentState();
    state.setTxtMsgErreurUrlServiceRestVisible(txtMsgErreurUrlServiceRest.getVisibility() == View.VISIBLE);
    return state;
  }
 
  @Override
  protected void initFragment(CoreState previousState) {
    // adapter listArduinos
    adapterListArduinos = new ListArduinosAdapter(activity, R.layout.listarduinos_item, session.getCheckedArduinos(), false);
 
  }
 
  @Override
  protected void initView(CoreState previousState) {
    // listview / adapter connection
    listArduinos.setAdapter(adapterListArduinos);
    // 1st visit?
    if (previousState == null) {
      // ListView empty - made by [initFragment]
      // hidden error message
      txtMsgErreurUrlServiceRest.setVisibility(View.INVISIBLE);
    } else {
      // error message visibility is restored
      ConfigFragmentState state = (ConfigFragmentState) previousState;
      txtMsgErreurUrlServiceRest.setVisibility(state.isTxtMsgErreurUrlServiceRestVisible() ? View.VISIBLE : View.INVISIBLE);
    }
  }
 
 
  @Override
  protected void updateOnSubmit(CoreState previousState) {
 
  }
 
  @Override
  protected void updateOnRestore(CoreState previousState) {
  }
 
 
  @Override
  protected void notifyEndOfUpdates() {
    // buttons
    initButtons();
}
  • 第 2 行:Arduinos 的 ListView 适配器。由于它在不同的方法中都会用到,因此被定义为全局变量;
  • 第 7–12 行:[saveFragment] 方法将 TextView txtMsgErreurUrlServiceRestVisible 的可见性(第 10 行)保存到 [ConfigFragmentState] 类型中
  • 第 14–19 行:[initFragment] 方法使用当前会话中的 Arduino 列表(第 17 行)初始化第 2 行中的适配器。请注意,[initFragment] 的作用是初始化片段的字段。在此处,无论是否为首次访问(previousState == null),都必须执行此初始化操作;
  • 第 17 行:可见适配器绑定到了数据源 [session.getCheckedArduinos]。该值绝不能为 null。因此,在会话中,字段 [session.checkedArduinos] 被初始化为一个空列表:

  // la liste des Arduinos
private List<CheckedArduino> checkedArduinos = new ArrayList<>();
  • 第 21–35 行:[initView] 方法负责初始化视觉界面的某些组件,特别是那些在设备旋转时其值不会被保留的组件;
  • 第 24 行:Arduino ListView 绑定到第 2 行定义的适配器;
  • 第 28–32 行:区分首次访问与其他访问;
  • 第 29 行:首次访问时,必须显示一个空的 [ListView]。这是因为在首次访问时,[ListView] 适配器关联的是一个空列表(第 17 行);
  • 第 31 行:隐藏错误消息;
  • 第 32–36 行:处理非首次访问的情况;
  • 由于第24行已将[ListView]置于正确状态,因此无需进行其他操作;
  • 第 34–35 行:将错误消息恢复到片段上次保存时的状态;
  • 第 31–36 行:[updateOnRestore] 方法必须将片段恢复到初始状态。我们通过两种方式进入 [updateOnRestore] 方法:
    • 要么是因为设备已旋转。在此情况下,所有必要的初始化已在 [initView] 中完成;
    • 或是因为我们正从某个标签页导航至 [Config] 标签页。如果自上次离开 [Config] 片段以来,该片段一直处于显示片段列表中,则 [initView] 方法已执行完毕,片段已处于预期状态。如果自退出 [Config] 片段以来,该片段一直未离开显示片段列表,则其视觉组件状态未发生变化,无需执行任何操作;

我们可以看到 [updateOnRestore] 方法无需执行任何操作。这种情况有时成立,有时则不然。区别在于 [updateOnSubmit] 方法:如果该方法执行的操作使得 [initView] 中进行的某些初始化变得多余,那么这些初始化就应移至 [updateOnRestore] 方法中完成。 让我们以一个具有三个值(V1、V2 和 V3)的单选按钮为例。在与 [SUBMIT] 操作相关的导航场景中,选中的单选按钮必须始终是值为 V1 的那个。 在这种情况下,在 [initView] 方法中恢复单选按钮的值是多余的,因为在 [SUBMIT] 操作发生时,该值会被 [updateOnSubmit] 方法提供的值所替换。因此,最好将此恢复操作移至 [updateOnRestore] 方法中,以避免执行不必要的操作。

  • 第 48–52 行:[notifyEndOfUpdates] 方法在所有前述方法执行完毕后被调用;
  • 第 51 行:按钮被设置为初始状态:显示 [Refresh] 按钮,并隐藏 [Cancel] 按钮:

任务:将上述代码添加到 [ConfigFragment] 中,然后运行应用。请注意,当旋转设备时,[Config] 标签页会保留其状态(错误信息、Arduino 列表)。请验证当您仅从 [Config] 标签页导航至 [Commands] 标签页 --> [Config] 标签页时,是否也会出现相同的行为。 在后一种情况下,若您已在 [IMainActivity] 中将片段邻接性设置为 1,则切换至 [Commands] 标签页时 [ConfigFragment] 视图会被销毁,返回 [Config] 标签页时则会重新创建。测试过程中请检查日志。


5.6.10.8. 代码优化

[ConfigFragment] 片段的代码尚有优化空间。例如,我们编写了:


// arduinos list adapter
  private ListArduinosAdapter adapterListArduinos;
 
...
 
  // arduinos list display
  private void showArduinos(List<CheckedArduino> checkedArduinos) {
    // display Arduinos
    ListArduinosAdapter adapter = new ListArduinosAdapter(getActivity(), R.layout.listarduinos_item, checkedArduinos, false);
    listArduinos.setAdapter(adapter);
  }
 
  // raz list of Arduinos
  private void clearArduinos() {
    // an empty list is displayed
    ListArduinosAdapter adapter = new ListArduinosAdapter(getActivity(), R.layout.listarduinos_item, new ArrayList<CheckedArduino>(), false);
    listArduinos.setAdapter(adapter);
  }
  • 我们可以看到,在第 9 行和第 16 行中,我们使用了一个与第 2 行字段无关的局部变量,尽管我们试图操作的是同一个实体;

我们将代码更新如下:


  // arduinos list adapter
  private ListArduinosAdapter adapterListArduinos;
 
  @Click(R.id.btn_Rafraichir)
  protected void doRafraichir() {
  ...
  }
 
  private void getArduinosInBackground() {
 ...
    // it is consumed
    consumeArduinosResponse(response);
  }
 
  // response display
  private void consumeArduinosResponse(Response<List<Arduino>> response) {
    // mistake?
    if (response.getStatus() != 0) {
      // display
      showAlert(response.getMessages());
      // cancellation
      doAnnuler();
      // back to Ui
      return;
    }
    // we create a list of [CheckedArduino]
    List<CheckedArduino> checkedArduinos = session.getCheckedArduinos();
    checkedArduinos.clear();
    for (Arduino arduino : response.getBody()) {
      checkedArduinos.add(new CheckedArduino(arduino, false));
    }
    // we display them
    adapterListArduinos.notifyDataSetChanged();
    // we cancel the wait
    cancelWaitingTasks();
}
 
  @Override
  protected void initFragment(CoreState previousState) {
    // adapt listArduinos
    adapterListArduinos = new ListArduinosAdapter(activity, R.layout.listarduinos_item, session.getCheckedArduinos(), false);
 
  }
 
  @Override
  protected void initView(CoreState previousState) {
    // listview / adapter connection
    listArduinos.setAdapter(adapterListArduinos);
    ...
}
  • 当第 5 行中的方法被执行时,片段的生命周期已经结束。因此:
    • 第 2 行的适配器已与其数据源关联(第 41 行);
    • 已将连接的Arduinos的[ListView]与该适配器关联(第48行);

若要更改 [ListView] 的显示内容,我们需要执行两项操作:

  • 修改数据源 [session.checkedArduinos] 的内容;
  • 使用 [adapterListArduinos.notifyDataSetChanged()] 指令通知适配器此变更;

重要的是要修改数据源的内容,而不是数据源本身。如果我们修改了数据源本身,[adapterListArduinos.notifyDataSetChanged()] 操作将仍然显示旧的数据源。届时,我们需要将适配器与新的数据源关联起来。

代码如下:

  • 第 27 行:我们获取数据源;
  • 第 28 行:清空该数据源。因此,我们已移除了 [clearArduinos] 方法;
  • 第 29–31 行:我们将新项目添加到这个现在为空的列表中;
  • 第 33 行:我们通知适配器刷新。这将刷新关联的 [ListView] 的显示;

任务:进行这些修改,并验证应用程序是否仍能正常运行。


5.6.11. 视图间的通信

为了验证视图之间的通信,我们将让所有其他视图显示由 [Config] 视图获取的 Arduino 列表。让我们从 [blink.xml] 视图开始。虽然它之前什么也没显示,但现在将显示已连接的 Arduino 列表:

Image

 

[blink.xml] 视图的 XML 代码如下:


<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
            android:id="@+id/scrollView1"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content">
 
  <?xml version="1.0" encoding="utf-8"?>
  <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
                  xmlns:tools="http://schemas.android.com/tools"
                  android:id="@+id/RelativeLayout1"
                  android:layout_width="match_parent"
                  android:layout_height="match_parent">
 
    <TextView
      android:id="@+id/txt_arduinos"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:layout_alignParentLeft="true"
      android:layout_marginTop="150dp"
      android:text="@string/titre_list_arduinos"
      android:textColor="@color/blue"
      android:textSize="20sp" />
 
    <ListView
      android:id="@+id/ListViewArduinos"
      android:layout_width="match_parent"
      android:layout_height="200dp"
      android:layout_alignParentLeft="true"
      android:layout_below="@+id/txt_arduinos"
      android:layout_marginTop="30dp"
      android:background="@color/wheat">
    </ListView>
 
  </RelativeLayout>
</ScrollView>

此代码直接摘自 [config.xml] 视图。我们仅修改了第 19 行中的顶部边距。


任务:将此代码复制到 [commands.xml、pinread.xml、pinwrite.xml] 视图中。


与 [blink.xml] 视图关联的 [BlinkFragment] 片段的代码也在发生变化:

  

  // visual components
  @ViewById(R.id.ListViewArduinos)
  protected ListView listArduinos;
 
  // arduinos list adapter
  private ListArduinosAdapter adapterListArduinos;
...
 
  // methods imposed by the parent class -------------------------------------------------------
 
...
  @Override
  protected void initFragment(CoreState previousState) {
    // adapter listArduinos
    adapterListArduinos = new ListArduinosAdapter(activity, R.layout.listarduinos_item, session.getCheckedArduinos(), true);
 
  }
 
  @Override
  protected void initView(CoreState previousState) {
    // listview / adapter connection
    listArduinos.setAdapter(adapterListArduinos);
  }
...
  • 第 2-3 行:用于已连接 Arduino 的 [ListView] 组件;
  • 第 6 行:此 [ListView] 的适配器;
  • 第 12-23 行:[initFragment] 和 [initView] 方法的代码与 [ConfigFragment] 片段中已使用的代码相同;
  • 第 15 行:当需要重置片段时,我们将第 2 行中的适配器与会话中存储的 Arduino 列表关联,从而重置适配器。[ListArduinosAdapter] 构造函数的最后一个参数 [true] 表示我们希望在每个 Arduino 旁边显示一个复选框;
  • 第 22 行:当需要重置片段视图时,我们将已连接 Arduino 的 [ListView] 绑定到第 6 行中的适配器;

任务:将此代码复制到其他片段中 [CommandsFragment, PinReadFragment, PinWriteFragment]。运行应用程序并验证每个标签页现在是否显示已连接的 Arduino 列表。同时验证:若在某个标签页中勾选了 Arduino,然后导航至另一个标签页,这些 Arduino 在后者中仍保持勾选状态。


:Arduino 设备保持选中状态的原因如下。第 5.6.10.4 节中介绍了 [ListArduinosAdapter] 类。与复选框相关的代码如下:


        // the current arduino
        final CheckedArduino arduino = arduinos.get(position);
...
        // the CheckBox is not always visible
        CheckBox ck = (CheckBox) row.findViewById(R.id.checkBoxArduino);
        ck.setVisibility(selectable ? View.VISIBLE : View.INVISIBLE);
        if (selectable) {
            // we assign its value
            ck.setChecked(arduino.isChecked());
            // we manage the click
            ck.setOnCheckedChangeListener(new OnCheckedChangeListener() {
 
                public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
                    arduino.setChecked(isChecked);
                }
            });
}
  • 第 11–15 行:如果选项卡 X 中的复选框被选中,则第 2 行中 Arduino 的 [checked] 属性被设置为 true(第 14 行);
  • 切换到标签页 Y 时,该标签页中的 Arduino [ListView] 会被显示出来。在第 9 行,我们可以看到,如果第 2 行中的 Arduino 的 [checked] 属性设置为 true,那么第 5 行中的 [ck] 复选框就会被勾选;

5.6.12. [DAO] 层

注意:本节内容请参考 [example-16B] 项目中 [DAO] 层的实现(参见第 2.8.3 节)。

到目前为止,我们都是手动生成已连接 Arduino 的列表。现在我们将从 Web 服务器 / JSON 中获取该列表。为此,我们将构建 [DAO] 层:

  

5.6.12.1. IDao 接口

[DAO] 层的 [IDao] 接口如下所示:


package client.android.dao.service;
 
import client.android.dao.entities.Arduino;
import client.android.dao.entities.Response;
import rx.Observable;
 
import java.util.List;
 
public interface IDao {
  // Web service url
  void setUrlServiceWebJson(String url);
 
  // user
  void setUser(String user, String mdp);
 
  // customer timeout
  void setTimeout(int timeout);
 
  // basic authentication
  void setBasicAuthentification(boolean isBasicAuthentificationNeeded);
 
  // debug mode
  void setDebugMode(boolean isDebugEnabled);
 
  // client wait time in milliseconds before request
  void setDelay(int delay);
 
  // spécifique ----------------------------------------
  // list of arduinos
  Observable<Response<List<Arduino>>> getArduinos();
}
  • 第 11-26 行:这些代码行已在 [client-android-skel] 模板项目的 [IDao] 接口中存在;
  • 第 30 行:[getArduinos] 方法将已连接的 Arduino 列表作为类型为 Observable<[Response<List<Arduino>>>] 的可观察对象返回;

请注意,[Response<T>] 是服务器以 JSON 字符串形式发送的所有响应的类型:


package client.android.dao.entities;
 
import java.util.List;
 
public class Response<T> {
 
    // ----------------- properties
    // operation status
    private int status;
    // any error messages
    private List<String> messages;
    // the body of the reply
    private T body;
 
    // manufacturers
    public Response() {
 
    }
 
    public Response(int status, List<String> messages, T body) {
        this.status = status;
        this.messages = messages;
        this.body = body;
    }
 
    // getters and setters
...
}

5.6.12.2. [WebClient] 接口

  

[WebClient] 接口是 AA 库提供实现的一个接口。该接口如下所示:


package client.android.dao.service;
 
import client.android.dao.entities.Arduino;
import client.android.dao.entities.Response;
import org.androidannotations.rest.spring.annotations.Get;
import org.androidannotations.rest.spring.annotations.Path;
import org.androidannotations.rest.spring.annotations.Rest;
import org.androidannotations.rest.spring.api.RestClientRootUrl;
import org.androidannotations.rest.spring.api.RestClientSupport;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.client.RestTemplate;
 
import java.util.List;
 
@Rest(converters = {MappingJackson2HttpMessageConverter.class})
public interface WebClient extends RestClientRootUrl, RestClientSupport {
 
  // RestTemplate
  void setRestTemplate(RestTemplate restTemplate);
 
  // spécifique --------------------------------------
  // list of arduinos
  @Get("/arduinos")
  Response<List<Arduino>> getArduinos();
}
  • 第 15-19 行:这些行默认包含在 [client-android-skel] 模板项目的 [WebClient] 接口中;
  • 第 23 行:用于通过 GET 请求获取 Arduino 列表的服务器 URL。请注意,此 URL 是相对于第 16 行中的根 URL [RestClientRootUrl] 的;
  • 第 24 行:服务器返回一个类型为 [Response<List<Arduino>>] 的 JSON 字符串。该 JSON 字符串会通过第 15 行中的 JSON 转换器 [MappingJackson2HttpMessageConverter] 自动反序列化为 [Response<List<Arduino>>] 类型;

5.6.12.3. [Dao] 类

[Dao] 类如下所示实现了 [IDao] 接口:


package client.android.dao.service;
 
import android.util.Log;
import client.android.dao.entities.Arduino;
import client.android.dao.entities.Response;
import org.androidannotations.annotations.AfterInject;
import org.androidannotations.annotations.Bean;
import org.androidannotations.annotations.EBean;
import org.androidannotations.rest.spring.annotations.RestService;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.client.RestTemplate;
import rx.Observable;
 
import java.util.ArrayList;
import java.util.List;
 
@EBean(scope = EBean.Scope.Singleton)
public class Dao extends AbstractDao implements IDao {
 
  // web service customer
  @RestService
  protected WebClient webClient;
  // safety
  @Bean
  protected MyAuthInterceptor authInterceptor;
  // on RestTemplate
  private RestTemplate restTemplate;
  // factory du RestTemplate
  private SimpleClientHttpRequestFactory factory;
 
  @AfterInject
  public void afterInject() {
    // log
    Log.d(className, "afterInject");
    // we build the restTemplate
    factory = new SimpleClientHttpRequestFactory();
    restTemplate = new RestTemplate(factory);
    // set the jSON converter
    restTemplate.getMessageConverters().add(new MappingJackson2HttpMessageConverter());
    // set the restTemplate of the web client
    webClient.setRestTemplate(restTemplate);
  }
 
  @Override
  public void setUrlServiceWebJson(String url) {
    // set the URL of the web service
    webClient.setRootUrl(url);
  }
 
  @Override
  public void setUser(String user, String mdp) {
    // the user is registered in the interceptor
    authInterceptor.setUser(user, mdp);
  }
 
  @Override
  public void setTimeout(int timeout) {
    if (isDebugEnabled) {
      Log.d(className, String.format("setTimeout thread=%s, timeout=%s", Thread.currentThread().getName(), timeout));
    }
    // factory configuration
    factory.setReadTimeout(timeout);
    factory.setConnectTimeout(timeout);
  }
 
  @Override
  public void setBasicAuthentification(boolean isBasicAuthentificationNeeded) {
    if (isDebugEnabled) {
      Log.d(className, String.format("setBasicAuthentification thread=%s, isBasicAuthentificationNeeded=%s", Thread.currentThread().getName(), isBasicAuthentificationNeeded));
    }
    // authentication interceptor?
    if (isBasicAuthentificationNeeded) {
      // add the authentication interceptor
      List<ClientHttpRequestInterceptor> interceptors = new ArrayList<ClientHttpRequestInterceptor>();
      interceptors.add(authInterceptor);
      restTemplate.setInterceptors(interceptors);
    }
  }
 
  // méthodes privées -------------------------------------------------
  private void log(String message) {
    if (isDebugEnabled) {
      Log.d(className, message);
    }
  }
 
  // specific IDao implementation -----------------------------------------------
 
  @Override
  public Observable<Response<List<Arduino>>> getArduinos() {
    // web client execution
    return getResponse(new IRequest<Response<List<Arduino>>>() {
      @Override
      public Response<List<Arduino>> getResponse() {
        return webClient.getArduinos();
      }
    });
  }
}
  • 第 19–87 行:这些行属于 [client-android-skel] 项目中的 [Dao] 类;
  • 第 91–100 行:[getArduinos] 方法的实现;
  • 第 94 行:调用父类的 [getResponse] 方法。该方法的唯一参数是 [IRequest<T>] 接口的实例;
  • 第 95–99 行:[IRequest<T>] 接口的唯一方法是 [T getResponse()] 方法;
  • 第 94 行:[IRequest<T>] 的类型 T 必须是第 92 行方法返回的 Observable<T> 结果的类型 T,因此这里应为类型 [Response<List<Arduino>>];
  • 第 97 行:方法 [IRequest.getResponse()] 将工作委托给我们之前引入的方法 [webClient.getArduinos()]。在第 24 行定义的 [webClient] 由 AA 库实例化,是 [WebClient] 接口的实例;

5.6.13. [MainActivity]

  

我们在第 5.6.8 节中已经介绍了 [MainActivity] 活动。它继承了 [AbstractActivity] 类,因此实现了 [IMainActivity] 接口,而该接口本身又继承了 [IDao] 接口。每当向 [IDao] 接口添加一个方法时,都必须在 [MainActivity] 类中实现该方法。 添加到 [IDao] 接口中的 [IDao.getArduinos] 方法将在 [MainActivity] 中实现如下:


...
@EActivity
@OptionsMenu(R.menu.menu_main)
public class MainActivity extends AbstractActivity {
 
  // layer [DAO]
  @Bean(Dao.class)
  protected IDao dao;
  // session
  private Session session;
 
...
 
  // implémentation IDao -----------------------------------------
  @Override
  public Observable<Response<List<Arduino>>> getArduinos() {
    return dao.getArduinos();
  }
}
  • 第 15–18 行:[getArduinos] 方法通过将工作委托给第 8 行中引用的、我们刚刚引入的 [Dao] 类来实现;

5.6.14. 重新审视 [ConfigFragment] 片段

在 [ConfigFragment] 类中,点击 [Refresh] 按钮时执行的代码目前如下:


  @Click(R.id.btn_Rafraichir)
  protected void doRafraichir() {
    ...
    // request the list of Arduinos running in the background
    getArduinosInBackground();
  }
 
  private void getArduinosInBackground() {
    // create a fictitious arduino list
    List<Arduino> arduinos = new ArrayList<>();
    for (int i = 0; i < 20; i++) {
      arduinos.add(new Arduino("id" + i, "desc" + i, "mac" + i, "ip" + i, i));
    }
    // we simulate a server response
    Response<List<Arduino>> response = new Response<>();
    response.setBody(arduinos);
    // it is consumed
    consumeArduinosResponse(response);
  }
 
  // response display
  private void consumeArduinosResponse(Response<List<Arduino>> response) {
    ...
}

我们需要重写第 10–16 行代码,这些代码硬编码了一个类型为 [Response<List<Arduino>>] 的响应。现在,我们需要通过 Activity 从 [DAO] 层请求该列表。代码如下:


  @Click(R.id.btn_Rafraichir)
  protected void doRafraichir() {
    // check entries
    if (!pageValid()) {
      return;
    }
    // save input
    mainActivity.setUrlServiceWebJson(urlServiceRest);
    // we prepare to wait
    beginWaiting(1);
    // the asynchronous task is executed
    executeInBackground(mainActivity.getArduinos(), new Action1<Response<List<Arduino>>>() {
 
      @Override
      public void call(Response<List<Arduino>> response) {
        // we consume the answer
        consumeArduinosResponse(response);
      }
    });
}
  • 第 8 行:用户输入的 Web 服务 / JSON 的根 URL 通过 Activity 传递给 [DAO] 层。这将成为 [WebClient] 接口的根 URL(参见第 5.6.12.2 节);
  • 第 10 行:通知父类即将启动一个异步任务;
  • 第12–19行:启动异步任务,该任务将返回连接到服务器的Arduino列表;
  • 第 12 行:调用父类的 [executeInBackground] 方法。该方法需要两个参数:
    • 第 12 行:待监视的进程。此处通过 [mainActivity.getArduinos()] 方法提供该进程;
    • 第 12–19 行:[Action1<T>] 接口的实例,其中类型 T 由进程提供,此处为 [Response<List<Arduino>>] 类型;
  • 第 14–18 行:当异步任务返回类型为 [Response<List<Arduino>>] 的结果时调用的方法;
  • 第 17 行:将接收到的响应传递给之前编写的 [consumeArduinosResponse] 方法;

任务:按照第 5.4 节所述启动服务器。将一个或多个 Arduino 连接到运行服务器的电脑上。然后启动 Android 客户端,并验证能否成功获取已连接 Arduino 的列表。观察日志。


Image

  • 输入[1]中指定的URL。这是您服务器的IP地址之一;
  • 点击 [2] 按钮;
  • 您应该会在[3]处看到已连接的Arduino列表;

请确认该列表在其他选项卡中是否也显示。

5.7. 后续步骤


按照与 [Config] 视图相同的步骤,依次实现并测试应用程序的其他四个视图:[Blink]、[PinRead]、[PinWrite] 和 [Commands]。


待创建的视图已在第 5.5 节中列出。

对于每个视图,您必须:

  • 绘制 XML 视图(参见第 5.6.9 节);
  • 构建相关的片段(参见第 5.6.10 节);
  • 向 [WebClient] 接口添加一个方法(参见第 5.6.12.2 节);
  • 向 [IDao] 接口添加一个方法(参见第 5.6.12.2 节);
  • 向 [Dao] 类添加一个方法(参见第 5.6.12.3 节);
  • 向 [MainActivity] 活动添加一个方法(参见第 5.6.13 节);
  • 编写片段的事件处理程序(参见第 5.6.14 节);
  • 测试并观察日志;

注 1:后续示例将参考课程中的 [Example-16B] 项目(参见第 2.8.3 节)。

注 2:待查询的 URL 及其响应类型已在第 5.4.2 节中给出。

注 3

[CommandsFragment] 类会发送一个包含单条命令的列表,供一个或多个 Arduino 执行。该命令将被封装在以下 [ArduinoCommand] 类中:


package android.arduinos.dao;
 
import java.util.Map;
 
public class ArduinoCommand {
 
  // data
  private String id;
  private String ac;
  private Map<String, Object> pa;
 
  // manufacturers
  public ArduinoCommand() {
 
  }
 
  public ArduinoCommand(String id, String ac, Map<String, Object> pa) {
    this.id = id;
    this.ac = ac;
    this.pa = pa;
  }
 
  // getters and setters
...
}

在 [WebClient] 接口中,执行此命令列表的方法如下:


  // envoi de commandes JSON
  @Post("/arduinos/commands/{idArduino}")
Response<List<ArduinoResponse>> sendCommands(@Body List<ArduinoCommand> commands, @Path String idArduino);
  • 第 2 行:通过 HTTP POST 请求发送请求;
  • 第 3 行:提交的值必须带有 [@Body] 注解;

注 4:建议按以下方式处理此任务:

  • 只有在当前视图创建并测试通过后,才继续处理下一个视图;
  • 仅在获得正常条件下可正常运行的应用程序后,才管理视图的状态。随后,针对每个视图,在设备上循环测试该视图的不同状态,并记录任何丢失的信息。这些就是需要保存并随后恢复的数据。接下来,验证导航功能:当您离开一个标签页并稍后返回时,该标签页应处于与离开时相同的状态;