5. Trabalho 2 - Controlo de Arduinos com um tablet Android
Vamos agora aprender a controlar uma placa Arduino com um tablet. O exemplo a seguir é o projeto [client-android-skel] do curso (ver parágrafo 2).
5.1. Arquitetura do projeto
Todo o projeto terá a seguinte arquitetura:
![]() |
- O bloco [1], o servidor web/JSON e os Arduinos, serão fornecidos a si;
- terá de construir o bloco [2], o programa para o tablet Android que se comunica com o servidor web/JSON.
5.2. Hardware
Os seguintes componentes estão à sua disposição:
- um Arduino com um shield Ethernet, um LED e um sensor de temperatura;
- um miniHub para partilhar com outro aluno;
- um cabo USB para alimentar o Arduino;
- dois cabos de rede para ligar o Arduino e o PC à mesma rede privada;
- um tablet Android;
5.2.1. O Arduino
![]() |
Veja aqui como ligar os vários componentes entre si:
- desligue o cabo de rede do seu PC;
- Ligue o seu PC ao Arduino utilizando um cabo de rede;
- O Arduino que possui já estará programado. O seu endereço IP será [192.168.2.2]. Para que o seu PC reconheça o Arduino, deve atribuir-lhe um endereço IP na rede [192.168.2]. Os Arduinos foram programados para comunicar com um PC com o endereço IP [192.168.2.1]. Veja como fazê-lo:
Vá a [Painel de Controlo\Rede e Internet\Centro de Rede e Partilha]:
![]() |
- Em [1], clique na ligação [Rede local];
- em [2], clique no botão [Propriedades] da rede local;
![]() | ![]() |
- em [3], clique nas propriedades [IPv4] do adaptador [Conexão de Rede Local];
- em [4], atribua a este adaptador o endereço IP [192.168.2.1] e a máscara de sub-rede [255.255.255.0];
- em [5], clique em [OK] tantas vezes quantas forem necessárias para sair do assistente.
5.2.2. O tablet
- Utilizando o seu adaptador Wi-Fi, ligue o seu computador à rede Wi-Fi que iremos disponibilizar. Faça o mesmo com o seu tablet;
- Verifique o endereço IP Wi-Fi do seu PC digitando [ipconfig] numa janela do Prompt de Comando. Encontrará um endereço como [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
- Verifique o endereço IP Wi-Fi do seu tablet. Se não tiver a certeza de como fazê-lo, pergunte ao seu formador. Encontrará um endereço do tipo [192.168.x.z];
- Desative a firewall do seu PC, caso esteja ativa [Painel de Controlo\Sistema e Segurança\Firewall do Windows];
- Numa janela do Prompt de Comando, verifique se o PC e o tablet conseguem comunicar digitando o comando [ping 192.168.x.z], onde [192.168.x.z] é o endereço IP do seu tablet. O tablet deverá então responder:
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
A configuração de rede do seu sistema já está pronta.
5.2.3. O emulador [Genymotion]
O emulador [Genymotion] (ver Secção 6.9) é uma excelente alternativa a um tablet. É quase tão rápido e não requer uma ligação Wi-Fi. Recomendamos a utilização deste método. Pode utilizar o tablet para o teste final da sua aplicação.
5.3. Programação de Arduinos
Aqui, focamo-nos na escrita de código C para Arduinos:
![]() |
Veja também
- Instalação do IDE de desenvolvimento do Arduino (ver Secção 6.1);
- Utilização de bibliotecas JSON (Anexos, Secção 6.6);
- No IDE do Arduino, teste o exemplo de um servidor TCP (por exemplo, o servidor web) e o de um cliente TCP (por exemplo, o cliente Telnet);
- os apêndices sobre o ambiente de programação do Arduino na secção 6.1.
![]() |
Um Arduino é um conjunto de pinos ligados ao hardware. Estes pinos são entradas ou saídas. Os seus valores são binários ou analógicos. Para controlar o Arduino, existem duas operações básicas:
- escrever um valor binário/analógico num pino identificado pelo seu número;
- ler um valor binário/analógico de um pino identificado pelo seu número;
A estas duas operações básicas, vamos adicionar uma terceira:
- fazer um LED piscar durante um determinado período de tempo e a uma determinada frequência. Esta operação pode ser realizada chamando repetidamente as duas operações básicas anteriores. No entanto, veremos nos testes que as trocas entre a camada [DAO] e um Arduino demoram cerca de um segundo. Por isso, não é possível fazer um LED piscar a cada 100 milissegundos, por exemplo. Assim, iremos implementar esta função de piscar no próprio Arduino.
O Arduino funcionará da seguinte forma:
- A comunicação entre a camada [DAO] e um Arduino ocorre através de uma rede TCP-IP, mediante a troca de linhas de texto no formato JSON (JavaScript Object Notation);
- no arranque, o Arduino liga-se à porta 100 de um servidor de registo localizado na camada [DAO]. Envia uma única linha de texto para o servidor:
Esta é uma cadeia JSON que descreve o Arduino que está a ligar-se:
- id: um identificador para o Arduino;
- desc: uma descrição do que o Arduino pode fazer. Aqui, especificámos simplesmente o modelo do Arduino;
- mac: o endereço MAC do Arduino;
- port: o número da porta na qual o Arduino aguardará comandos da camada [DAO].
Todas estas informações estão no formato de cadeia de caracteres, exceto a porta, que é um número inteiro.
- Assim que o Arduino se registar no servidor de registo, começa a escutar na porta que especificou para o servidor (102 acima). Aguarda comandos JSON no seguinte formato:
Esta é uma cadeia JSON com os seguintes elementos:
- id: um identificador para o comando. Pode ser qualquer coisa;
- ac: uma ação. Existem três:
- pw (gravação de pino) para gravar um valor num pino,
- pr (leitura de pino) para ler o valor de um pino,
- cl (piscar) para fazer um LED piscar;
- pa: os parâmetros da ação. Estes dependem da ação.
- O Arduino devolve sempre uma resposta ao seu cliente. Trata-se de uma cadeia JSON no seguinte formato:
onde
- id: o identificador do comando ao qual se está a responder;
- er (erro): um código de erro, caso tenha ocorrido um erro; caso contrário, 0;
- et (status): um dicionário que está sempre vazio, exceto no comando de leitura pr. O dicionário contém então o valor do pino número x que foi solicitado.
Aqui estão alguns exemplos para esclarecer as especificações anteriores:
Fazer o LED n.º 8 piscar 10 vezes com um intervalo de 100 milissegundos:
Comando | |
Resposta |
Os parâmetros para o comando cl são: a duração (dur) de um flash em milissegundos, o número (nb) de flashes e o número do pino do LED.
Escreva o valor binário 1 no pino 7:
Comando | |
Resposta |
Os parâmetros pa do comando pw são: o modo de escrita mod (b para binário ou a para analógico), o valor val a ser gravado e o número do pino. Para uma escrita binária, val é 0 ou 1. Para uma escrita analógica, val está no intervalo [0,255].
Escreva o valor analógico 120 no pino 2:
Comando | |
Resposta |
Ler o valor analógico do pino 0:
Comando | |
Resposta |
Os parâmetros pa do comando pr são: o modo de leitura (binário ou analógico) e o número do pino. Se não houver erro, o Arduino coloca o valor do pino solicitado na chave "et" da sua resposta. Aqui, pin0 indica que o valor do pino 0 foi solicitado, e 1023 é esse valor. No modo de leitura, um valor analógico estará no intervalo [0, 1024].
Apresentámos os três comandos cl, pw e pr. Poder-se-á questionar por que razão não utilizámos campos mais explícitos nas cadeias JSON — tais como action em vez de ac, pinwrite em vez de pw e parameters em vez de pa. Um Arduino tem uma memória muito limitada. No entanto, as cadeias JSON trocadas com o Arduino contribuem para o consumo de memória. Por isso, optámos por encurtá-las o máximo possível.
Agora, vamos analisar alguns casos de erro:
Comando | |
Resposta |
Foi enviado um comando que não está no formato JSON. O Arduino devolveu o código de erro 100.
Comando | |
Resposta |
Foi enviado um comando pr sem o parâmetro pin. O Arduino devolveu o código de erro 302.
Comando | |
Resposta |
Enviámos um comando pinread desconhecido (é pr). O Arduino devolveu o código de erro 104.
Não vamos continuar com os exemplos. A regra é simples. O Arduino não deve falhar, independentemente do comando que lhe seja enviado. Antes de executar um comando JSON, ele garante que o comando é válido. Assim que ocorre um erro, o Arduino interrompe a execução do comando e devolve a cadeia de erro JSON ao seu cliente. Mais uma vez, como o espaço de memória é limitado, devolvemos um código de erro em vez de uma mensagem completa.
O código do programa em execução no Arduino é fornecido nos exemplos deste documento:
![]() |
Para transferi-lo para o Arduino:
- Ligue-o ao seu PC;
![]() | ![]() |
- em [1], abra o ficheiro [arduino_uno.ino]. O Arduino IDE será iniciado e carregará o ficheiro;
Nota: O código foi originalmente criado e testado com o IDE do Arduino 1.5.x. Desde então, foram lançadas outras versões do IDE. O código não funcionou com o IDE do Arduino 1.6.x. Parece existir um problema de compatibilidade com versões anteriores entre as versões 1.6 e 1.5.
- Em [2-4], especifique o tipo de Arduino utilizado;
![]() | ![]() |
- Em [5-7], especifique a que porta série do PC está ligado;
- em [8], carregue o programa [arduino_uno] no Arduino;
O código do programa está repleto de comentários. Os leitores interessados podem consultá-lo. Destacamos apenas as linhas de código que configuram a comunicação bidirecional cliente/servidor entre o Arduino e o 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()
{
...
}
- Linha 8: o endereço MAC do Arduino. Isso não importa muito aqui, porque o Arduino estará numa rede privada com um PC e um ou mais Arduinos. O endereço MAC precisa apenas de ser único nesta rede privada. Normalmente, a placa de rede do Arduino tem um autocolante que indica o endereço MAC da placa. Se este autocolante estiver em falta e não souber o endereço MAC da placa, pode introduzir o que quiser na linha 8, desde que seja respeitada a regra da exclusividade do endereço MAC na rede privada;
- linha 11: o endereço IP da placa. Mais uma vez, pode introduzir qualquer valor do tipo [192.168.2.x] e variar o x para os diferentes Arduinos na rede privada;
- Linha 13: identificador do Arduino. Deve ser único entre os identificadores dos Arduinos na mesma rede privada;
- linha 15: a porta de serviço do Arduino. Pode introduzir o que quiser;
- linha 17: descrição da função do Arduino. Pode introduzir o que quiser. Tenha cuidado com cadeias de caracteres longas devido à memória limitada do Arduino;
- linha 21: endereço IP do servidor de registo do Arduino no PC. Não deve ser alterado;
- linha 23: porta para este serviço de registo. Não deve ser alterada;
5.4. O servidor Web/JSON
5.4.1. Instalação

O binário Java para o servidor Web/JSON é fornecido:
![]() |
Abra um prompt de comando e digite o seguinte comando:
Se o [java.exe] não estiver no PATH do prompt de comando, terá de digitar o caminho completo para o [java.exe] (normalmente C:\Program Files\java\...).
Uma janela do DOS irá abrir e exibir os registos:
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: 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
- linha 11: um servidor Tomcat incorporado é iniciado;
- linha 15: o servlet Spring MVC [dispatcherServlet] é carregado e executado;
- linha 18: a URL REST [/arduinos/blink/{commandId}/{ArduinoId}/{pin}/{duration}/{count}] é detetada;
- linha 19: a URL REST [/arduinos/commands/{idArduino}] é detetada;
- linha 20: a URL REST [/arduinos/] é detetada;
- linha 21: a URL REST [/arduinos/pinRead/{commandId}/{ArduinoId}/{pin}/{mode}] é detetada;
- linha 22: a URL REST [/arduinos/pinWrite/{commandId}/{ArduinoId}/{pin}/{mode}/{value}] é detetada;
- linha 26: o servidor de registo do Arduino é iniciado;
Ligue o seu Arduino ao PC, caso ainda não o tenha feito. A firewall do PC deve estar desativada. Em seguida, num navegador da Web, introduza a URL [http://localhost:8080/arduinos]:
![]() |
Deve ver aparecer o ID do Arduino ligado. Se nada aparecer, tente reiniciar o Arduino. Este possui um botão de reinicialização para esse efeito.
O servidor web/JSON está agora instalado.
5.4.2. As URLs disponibilizadas pelo serviço web/JSON
Consulte: projeto [Exemplo-15] (ver secção 1.16.1);
O serviço web/JSON foi implementado utilizando o Spring MVC e expõe as seguintes URLs:
@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 {
...
}
}
As respostas enviadas pelo servidor são representações JSON da seguinte classe [Response<T>]:
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
...
}
A URL [/arduinos] devolve uma resposta do tipo [Response<List<Arduino>>], em que [Arduino] é a seguinte classe:
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
...
}
- linha 7: [id] é o identificador do Arduino;
- linha 8: a sua descrição;
- linha 9: o seu endereço MAC;
- linha 10: o seu endereço IP;
- linha 11: a porta na qual ele escuta comandos;
Os URLs:
- [/arduinos/blink/{commandId}/{ArduinoId}/{pin}/{duration}/{count}];
- [/arduinos/pinRead/{commandId}/{ArduinoId}/{pin}/{mode}] ;
- [/arduinos/pinWrite/{commandId}/{ArduinoId}/{pin}/{mode}/{value}] ;
- [/arduinos/commands/{ArduinoID}];
retornar uma resposta do tipo [Response<ArduinoResponse>], em que a classe [ArduinoResponse] representa a resposta padrão do Arduino:
public class ArduinoResponse implements Serializable {
private String json;
private String id;
private String erreur;
private Map<String, Object> etat;
// getters and setters
...
}
- [json]: a cadeia JSON enviada por um Arduino que não foi possível descodificar (em caso de erro); caso contrário, é nulo;
- [id]: o identificador do comando ao qual o Arduino está a responder;
- [error]: um código de erro, 0 se tudo estiver OK, caso contrário, outro valor;
- [status]: um dicionário contendo a resposta específica para o comando. Normalmente está vazio, a menos que o comando tenha solicitado a leitura de um valor do Arduino; nesse caso, esse valor será colocado neste dicionário;
5.4.3. Teste do serviço web / JSON
Familiarize-se com o servidor web / JSON testando os seguintes URLs:
Aqui estão algumas capturas de ecrã do que deverá ver:
Obter a lista de Arduinos ligados:
![]() |
A cadeia JSON recebida do servidor web / JSON é um objeto com os seguintes campos:
- [status]: 0 indica que não houve erro — caso contrário, ocorreu um erro;
- [messages]: uma lista de mensagens explicando o erro, caso tenha ocorrido um erro:
- [body]: a lista de Arduinos, caso não tenha ocorrido nenhum erro. Cada Arduino é então descrito por um objeto com os seguintes campos:
- [id]: o identificador do Arduino. Não podem existir dois Arduinos com o mesmo identificador;
- [description]: uma breve descrição da funcionalidade do Arduino;
- [mac]: o endereço MAC do Arduino;
- [ip]: o endereço IP do Arduino;
- [port]: a porta na qual ele escuta comandos;
Faça com que o LED no pino 8 do Arduino identificado por [cuisine] pisque 20 vezes a cada 100 ms:
![]() |
A cadeia JSON recebida do servidor web / JSON é um objeto com os seguintes campos:
- [status]: 0 indica que não houve erro — caso contrário, ocorreu um erro;
- [messages]: uma lista de mensagens explicando o erro, caso tenha ocorrido um erro:
- [body]: a resposta do Arduino se não houve erro:
- [id]: identificador do comando. Este identificador é o 1 em [/blink/1]. O Arduino inclui este identificador de comando na sua resposta;
- [error]: um número de erro. Um valor diferente de 0 indica um erro;
- [state]: utilizado apenas para ler um pino. O seu valor é o valor do pino;
- [json]: utilizado apenas no caso de um erro JSON entre o cliente e o servidor. O seu valor é a cadeia JSON errada enviada pelo Arduino;
Leitura analógica do pino 0 no Arduino, identificada por [kitchen]:
![]() |
A cadeia JSON recebida do servidor web /json é semelhante à anterior, com exceção do campo [state], que representa o valor do pino 0.
Leitura binária do pino 5 no Arduino, identificada por [kitchen]:
![]() |
A cadeia JSON recebida do servidor web /json é semelhante à anterior.
Gravação binária do valor 1 no pino 8 do Arduino identificado por [kitchen]:
![]() |
A cadeia JSON recebida do servidor web /json é semelhante à anterior.
Testar a URL [http://localhost:8080/arduinos/commands/cuisine] é mais complicado. O método /json do servidor web que lida com esta URL espera um pedido POST, o que não pode ser facilmente simulado utilizando um navegador. Para testar esta URL, pode utilizar um navegador Chrome com a extensão [Advanced REST Client] (ver secção 6.13):
![]() |
- em [1], o URL do método web/JSON a ser testado;
- em [2], o método POST para enviar o pedido;
- em [3-4], o valor enviado é JSON;
- em [5], a cadeia JSON que está a ser enviada. Repare nos parênteses retos que iniciam e terminam a lista. Aqui, a lista contém apenas um comando JSON que faz com que o pino 8 pisque 10 vezes a cada 100 ms;
- em [6], a solicitação é enviada;
![]() |
- em [7], a resposta JSON enviada pelo servidor. O objeto recebeu um objeto com os dois campos habituais [status, messages] e um campo [body] cujo valor é a lista de respostas do Arduino a cada um dos comandos JSON enviados.
Vamos ver o que acontece quando enviamos um comando JSON com sintaxe incorreta para o Arduino:
![]() |
Recebemos então a seguinte resposta:
![]() |
Podemos ver que, na resposta do Arduino, o código de erro é [104], indicando que o comando [xx] não foi reconhecido.
5.5. Teste do cliente Android
![]() |
O executável final do cliente Android é fornecido abaixo:
![]() |
Utilize o rato para arrastar o ficheiro [app-debug.apk] acima para um emulador de tablet [GenyMotion]. Este será então guardado e executado. Inicie também o servidor web/jSON, caso ainda não o tenha feito. Ligue o Arduino ao PC com um LED ligado ao mesmo. O cliente Android permite-lhe gerir Arduinos remotamente. Apresenta os seguintes ecrãs ao utilizador.
O separador [CONFIG] permite-lhe ligar-se ao servidor e recuperar a lista de Arduinos ligados:

- Em [1], introduza o endereço IP [192.168.2.1] atribuído ao seu PC (consulte a secção 5.2).
O separador [PINWRITE] permite-lhe escrever um valor num pino do Arduino:


O separador [PINREAD] permite-lhe ler o valor de um pino do Arduino:

O separador [BLINK] permite-lhe fazer um LED do Arduino piscar:

O separador [COMMAND] permite enviar um comando JSON para um Arduino:

5.6. O cliente Android para o serviço web / JSON
Vamos agora discutir a criação do cliente Android.
5.6.1. Arquitetura do cliente
A arquitetura do cliente Android será a do projeto [Exemplo-15] (ver secção 1.16.2);
![]() |
- a camada [DAO] comunica com o servidor web/JSON;
O cliente Android deve ser capaz de controlar vários Arduinos simultaneamente. Por exemplo, queremos poder fazer com que dois LEDs em dois Arduinos pisquem ao mesmo tempo, e não um após o outro. Por isso, o nosso cliente Android utilizará uma tarefa assíncrona por Arduino, e estas tarefas serão executadas em paralelo.
5.6.2. O projeto do cliente Android Studio
Duplique o projeto [client-android-skel] (consulte a secção 2) para o projeto [client-arduinos-01] (se necessário, reveja como duplicar um projeto Gradle na secção 1.15):
5.6.3. As cinco vistas XML
![]() |
Haverá cinco vistas XML:
- [blink]: para fazer um LED do Arduino piscar. Está associada ao fragmento [BlinkFragment];
- [commands]: para enviar um comando JSON a um Arduino. Está associada ao fragmento [CommandsFragment];
- [config]: para configurar o serviço web/URL JSON e recuperar a lista inicial de Arduinos conectados. Está associada ao fragmento [ConfigFragment];
- [pinread]: para ler o valor binário ou analógico de um pino do Arduino. Está associado ao fragmento [PinReadFragment];
- [pinwrite]: para escrever um valor binário ou analógico num pino do Arduino. Está associado ao fragmento [PinWriteFragment];
Por enquanto, estas cinco vistas XML terão todas o mesmo conteúdo vazio:
<?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>
- A vista encontra-se dentro de um contentor [RelativeLayout] (linhas 7–10), que por sua vez está contido num contentor [ScrollView] (linhas 2–11). Isto garante que podemos percorrer a vista se esta exceder o tamanho do ecrã de um tablet;
Tarefa: Crie as cinco vistas XML.
5.6.4. O menu de fragmentos
Sabemos que os fragmentos num projeto construído com [client-android-skel] devem estar associados a um menu, mesmo que este esteja vazio. Aqui, a aplicação não terá um menu. O menu vazio já se encontra no projeto;
![]() |
5.6.5. Os cinco componentes da aplicação
![]() | ![]() |
Tarefa: Duplique o fragmento [DummyFragment] para os cinco fragmentos da aplicação, conforme mostrado em [2].
O fragmento [ConfigFragment] tem a seguinte estrutura:
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 -------------------------------------------------------
...
Substitua a linha 10 pela seguinte linha:
@EFragment(R.layout.config)
Tarefa: Faça o mesmo para os outros quatro fragmentos, adaptando o atributo [@EFragment] da classe.
Fragmento | View |
ConfigFragment | |
Fragmento de leitura de PIN | |
PinWriteFragment | |
Fragmento de comandos | |
Fragmento de piscar | |
5.6.6. Estados do fragmento
![]() | ![]() |
Cada fragmento terá um estado.
Tarefa: Duplique a classe [DummyFragmentState] cinco vezes para criar os cinco estados apresentados em [2].
5.6.7. Personalização do projeto
![]() |
O pacote [architecture / custom] contém os elementos personalizáveis da arquitetura da aplicação.
5.6.7.1. A interface [IMainActivity]
A interface [IMainActivity] define o que os fragmentos podem solicitar à atividade, bem como as constantes da aplicação. Esta interface será a seguinte:
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;
}
- linhas 25, 28, 31, 40: configuração da camada [DAO]. Esta aplicação consulta um servidor web/JSON;
- linha 37: esta aplicação tem separadores;
- linha 43: esta aplicação tem cinco fragmentos;
- linhas 46–50: os números dos cinco fragmentos;
- linha 34: adjacência de fragmentos. O programador pode definir aqui um valor dentro do intervalo [1, FRAGMENTS_COUNT-1];
5.6.7.2. A classe [CoreState]
A classe [CoreState] é a classe pai dos estados dos fragmentos:
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
...
}
- linhas 12–16: as classes de estado para os cinco fragmentos devem ser declaradas aqui;
5.6.8. A classe [MainActivity]
![]() |
A classe [MainActivity] terá a seguinte forma:
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 -----------------------------------------
}
- linhas 46–50: criação dos cinco separadores da aplicação;
- linha 48: os títulos das guias são fornecidos pelo método nas linhas 63-79;
- os cinco fragmentos são instanciados na linha 60. Devido às anotações AA, as classes dos fragmentos são as apresentadas anteriormente, com um sublinhado como sufixo;
- linhas 63-79: é definido um título para cada fragmento. Estes títulos serão recuperados do ficheiro [res/values/strings.xml]
![]() |
O conteúdo do ficheiro [strings.xml] é o seguinte:
<?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>
Tarefa: Crie os elementos listados acima e compile o projeto. Não deve haver erros.
Execute o projeto. Deverá ver a seguinte visualização no emulador:

Examine os registos que acompanharam a exibição da primeira vista e acompanhe os vários passos que foram executados. Alterne entre os separadores e continue a acompanhar os registos.
5.6.9. A vista XML [config]
A vista XML [config] terá o seguinte aspeto:
![]() | ![]() |
A visualização acima é gerada pelo seguinte código 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>
A vista utiliza strings (android:text nas linhas 15, 25, 37, 50, 61, 73) que estão definidas no ficheiro [res/values/strings]:
![]() |
<?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>
A vista utiliza cores (android:textColor nas linhas 51 e 62) definidas no ficheiro [res/values/colors]:
![]() |
<?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>
A vista utiliza dimensões (android:textSize na linha 16) que estão definidas no ficheiro [res/values/dimens]:
![]() |
<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>
Esta técnica não foi utilizada para todas as dimensões. No entanto, é a abordagem recomendada. Permite-lhe alterar as dimensões num único local.
Tarefa: Crie os elementos acima.
Execute o seu projeto novamente. Deverá ver a seguinte vista:

5.6.10. O fragmento [ConfigFragment]
![]() |
Para lidar com a nova vista [config], o código do fragmento [ConfigFragment] é alterado da seguinte forma:
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);
}
}
- linhas 23–32: os elementos da interface visual;
- linhas 58–60: na primeira visita ao fragmento, a mensagem de erro é ocultada;
- linhas 73–76: cada vez que o fragmento é exibido, o botão [Cancelar] é ocultado (linha 82) e o botão [Atualizar] é exibido (linhas 86–87). De facto, nesta aplicação, um fragmento não pode ser exibido enquanto uma operação assíncrona está em curso e, por isso, o botão [Cancelar] fica visível;
Tarefa: Crie os elementos acima referidos.
Execute esta nova versão. A primeira vista deverá agora ter este aspeto:

5.6.10.1. O botão [Atualizar]
Por enquanto, vamos tratar o clique no botão [Atualizar] da seguinte forma:
@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);
}
- linhas 1-5: o método executado quando o botão [Atualizar] é clicado;
- linha 4: iniciamos a espera;
- linha 18: passamos o número de tarefas assíncronas a serem iniciadas para a classe pai. A imagem de carregamento aparecerá;
- linhas 20-21: esta espera fará com que o botão [Cancel] apareça, o botão [Refresh] desapareça e a imagem de carregamento apareça. Nada mais acontece. No entanto, o utilizador pode clicar no botão [Cancel]. O método nas linhas 7-14 será então executado;
- linha 13: é solicitado à classe pai que cancele todas as tarefas. A classe fará isso e, por sua vez, chamará o método nas linhas 25–29 para sinalizar que todas as tarefas foram concluídas. O parâmetro [runningTasksHaveBeenCanceled] terá o valor true para indicar que as tarefas foram canceladas;
- Linhas 35–36: O botão [Cancelar] desaparecerá, enquanto o botão [Atualizar] reaparecerá.
Tarefa: Efetue estas alterações e, em seguida, execute o projeto. Verifique se o botão [Refresh] inicia a espera e se o botão [Cancel] a interrompe. Observe os registos.
5.6.10.2. Validação de entrada
Na versão anterior, não validávamos o URL introduzido. Para o validar, adicionamos o seguinte código em [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;
}
- linha 2: o URL introduzido;
- linhas 7–9: antes de fazer qualquer coisa, verificamos a validade da entrada;
- linha 19: recuperamos a URL introduzida e adicionamos-lhe o prefixo [http://];
- linha 22: tentamos construir um objeto URI (Uniform Resource Identifier) com ela. Se a URL introduzida estiver sintaticamente incorreta, será lançada uma exceção;
- linhas 23–27: é lançada uma exceção se o URI for válido, mas [host==null] e [port==-1]. Este é um cenário possível;
- linha 30: ocorreu uma exceção. A mensagem de erro é exibida;
- linha 32: devolvemos [false] para indicar que a página é inválida;
- linha 35: não ocorreram erros. Devolvemos [true] para indicar que a página é válida;
Tarefa: Implemente a funcionalidade acima.
Teste esta nova versão e verifique se os URLs inválidos são devidamente sinalizados.
5.6.10.3. Exibição da lista de Arduinos
![]() |
As diferentes vistas terão de apresentar a lista de Arduinos ligados. Para tal, iremos definir diferentes classes e uma vista XML:
- Um Arduino será representado pela classe [Arduino] [1];
- a classe [CheckedArduino] [1] herda da classe [Arduino], à qual adicionámos um valor booleano para indicar se o Arduino foi selecionado numa lista;
A classe [Arduino] é a que já é utilizada pelo servidor e apresentada na secção 5.4.2. É a seguinte:
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
...
}
- linha 7: [id] é o identificador do Arduino;
- linha 8: a sua descrição;
- linha 9: o seu endereço MAC;
- linha 10: o seu endereço IP;
- linha 11: a porta na qual ele escuta comandos;
Esta classe corresponde à cadeia JSON recebida do servidor ao solicitar a lista de Arduinos conectados:
![]() |
A classe [CheckedArduino] herda da classe [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;
}
}
- Linha 3: A classe [CheckedArduino] herda da classe [Arduino];
- linha 6: adicionamos uma variável booleana que nos dirá se um Arduino foi selecionado da lista de Arduinos apresentada;
Em [ConfigFragment], simularemos a recuperação da lista de Arduinos conectados.
![]() |
@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));
}
- linha 2: o ListView que exibe os Arduinos ligados ao servidor;
- linha 5: o método que recupera a lista de Arduinos conectados;
- linha 11: informamos à classe pai que vamos iniciar uma tarefa assíncrona;
- linha 12: limpamos a lista de Arduinos atualmente exibida;
- linha 15: solicitamos a lista de Arduinos conectados como uma tarefa em segundo plano;
- linhas 23–28: o método que limpa a lista de Arduinos atualmente exibida;
O método [getArduinosInBackground] é o seguinte:
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);
}
- linhas 3–6: criamos uma lista de 20 Arduinos;
- linhas 8-9: construímos a resposta do tipo [Response<List<Arduino>>] (secção 5.4.2) que irá encapsular a lista de Arduinos criada;
- linha 11: cancelar a espera;
- linha 13: reinicializar os botões para o seu estado inicial;
- linha 15: consumir a resposta;
O método [consumeArduinosResponse] é o seguinte:
// 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);
}
- linhas 4-11: verificamos o código de erro na resposta enviada pelo servidor:
- linha 4: se o código de erro não for zero;
- linha 6: exibe as mensagens armazenadas pelo servidor no campo [messages] da resposta;
- linha 8: regressar à interface do utilizador;
- linhas 11-16: se não houver erros, exiba a lista de Arduinos recebidos, após convertê-la para o tipo List<CheckedArduino>;
O método [showArduinos] é o seguinte:
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));
}
Tarefa: Efetue as alterações acima e execute o seu projeto.
Deve ver a seguinte vista quando clicar no botão [Atualizar]:

A entrada em [1] não é utilizada. Por isso, pode introduzir qualquer coisa, desde que siga o formato esperado.
5.6.10.4. Um modelo para apresentar um Arduino
Atualmente, os Arduinos ligados são apresentados na vista [Config] da seguinte forma:

Agora queremos exibi-los da seguinte forma:
![]()
- em [1], uma caixa de seleção que permitirá selecionar um Arduino. Esta caixa de seleção ficará oculta quando pretender exibir uma lista de Arduinos não selecionáveis;
- em [2], o ID do Arduino;
- em [3], a sua descrição;
O que se segue baseia-se nos conceitos desenvolvidos nos projetos [exemplo-19] e [exemplo-19B] na Secção 1.20. Reveja-os, se necessário.
Primeiro, criamos a vista que irá exibir um item da lista de Arduinos:
![]() |
O código para a vista [listarduinos_item] acima é o seguinte:
<?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>
- linhas 9–15: a caixa de seleção;
- linhas 17-23: o texto [Id: ];
- linhas 25-33: o ID do Arduino será inserido aqui;
- linhas 35-43: o texto [Descrição: ];
- linhas 45-53: a descrição do Arduino será inserida aqui;
Esta vista utiliza texto (linhas 23, 32, 43) definido em [res/values/strings.xml]:
<string name="dummy">XXXXX</string>
<!-- listarduinos_item -->
<string name="txt_arduino_id">Id : </string>
<string name="txt_arduino_description">Description : </string>
A vista também utiliza uma cor (linhas 33, 53) definida em [res / values / colors.xml]:
<?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>
O gestor de visualização para um item na lista do Arduino
![]() |
A classe [ListArduinosAdapter] é a classe chamada pela [ListView] para apresentar cada item da lista do Arduino. O seu código é o seguinte:
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) {
...
}
}
- linha 18: o construtor da classe recebe quatro parâmetros: a atividade atualmente em execução, o ID da vista a ser exibida para cada item na fonte de dados, a fonte de dados que preenche a lista e um valor booleano indicando se a caixa de seleção associada a cada Arduino deve ser exibida ou não;
- Linhas 8–15: Estas quatro informações são armazenadas localmente;
Linha 29: O método [getView] é responsável por gerar a vista #[position] na [ListView] e por tratar os seus eventos. O seu código é o seguinte:
@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;
}
- linha 2: o primeiro parâmetro é a posição na [ListView] da linha a ser criada. É também a posição na lista de Arduinos armazenada localmente;
- linha 4: recuperamos uma referência ao Arduino que será associado à linha construída;
- linha 6: a linha atual é construída a partir da vista [listarduinos_item.xml];
- linhas 8–9: são recuperadas as referências aos dois [TextView]s;
- linhas 11-12: são atribuídos valores aos dois [TextView]s;
- linha 14: é recuperada uma referência à caixa de seleção;
- linha 15: torna-se visível ou não, dependendo do valor [selectable] inicialmente passado ao construtor;
- linha 16: se a caixa de seleção estiver presente;
- linha 18: o valor [isChecked] do Arduino atual é atribuído a ela;
- linhas 20–26: tratamos do clique na caixa de seleção;
- linha 23: o valor da caixa de seleção é armazenado no Arduino atual;
Gerir a lista de Arduinos
A exibição da lista de Arduinos é atualmente tratada por dois métodos da classe [ConfigFragment]:
- [clearArduinos]: que exibe uma lista vazia;
- [showArduinos]: que exibe a lista devolvida pelo servidor;
Estes dois métodos funcionam da seguinte forma:
// 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);
}
Tarefa: Efetue estas alterações e teste a nova aplicação.

5.6.10.5. A sessão
A sessão é onde armazenamos as informações partilhadas entre os fragmentos e a atividade. Todos os fragmentos precisam de apresentar a lista de Arduinos ligados. Assim, uma versão inicial da sessão teria o seguinte aspeto:
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
...
}
Tarefa: Crie a classe [Session] apresentada acima.
A criação desta sessão requer que modifiquemos o código existente da seguinte forma:
// 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();
}
- linha 18: a lista de Arduinos criada pelas linhas anteriores é colocada na sessão;
5.6.10.6. Gestão do estado do fragmento
Quando o dispositivo é rodado, os componentes visuais da vista são renderizados (por predefinição) no estado em que se encontravam quando a vista foi desenhada:
- a [ListView] contém os itens que o designer colocou lá;
- a mensagem de erro encontra-se no estado visível ou não visível em que o designer a colocou;
Os estados dos componentes visuais no momento do design podem ou não ser adequados ao restaurar um fragmento. Qual é o caso aqui?
- o [ListView] deve exibir a lista de Arduinos conectados. O valor do [ListView] no momento do design não pode, portanto, ser utilizado;
- O [TextView] para a mensagem de erro deve ser restaurado para o estado visível ou oculto que tinha no momento do salvamento. O seu valor no momento do design pode não ser adequado para estes dois casos;
Devemos, portanto, guardar o estado destes dois componentes ao guardar o estado do fragmento:
- a lista de Arduinos conectados;
- a visibilidade (mostrada/oculta) da mensagem de erro ao introduzir o URL do serviço web/JSON;
Uma vez que a lista de Arduinos está presente na sessão, será guardada automaticamente. A visibilidade da mensagem de erro será armazenada na seguinte classe [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
...
}
Tarefa: Crie a classe [ConfigFragmentState] apresentada acima.
Para restaurar corretamente os estados dos fragmentos, os seus métodos [getNumView] e [saveFragment] devem ser modificados. Por exemplo, o método para o fragmento [BlinkFragment] está atualmente definido da seguinte forma:
@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;
}
Se nada for feito, o estado renderizado na linha 6 será guardado no elemento 0 (linha 13) da matriz CoreState[] coreStates da classe [AbstractSession] (linha 5 abaixo):
public class AbstractSession implements ISession {
...
// view status
private CoreState[] coreStates = new CoreState[0];
...
No entanto, deve ser guardado no elemento correspondente ao ID do fragmento [BlinkFragment] na matriz de fragmentos definida na classe [MainActivity] (linha 9 abaixo):
@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_()};
}
Os IDs dos fragmentos foram definidos na interface [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;
}
Em última análise, o estado do fragmento [BlinkFragment] será gerido corretamente se escrevermos:
@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;
}
- Linha 14: Retorna o ID do fragmento [BlinkFragment] na matriz de fragmentos gerida pela atividade;
Além disso, a classe [CoreState], que é a classe pai dos estados dos fragmentos, apresenta-se atualmente da seguinte forma (ver secção 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
....
}
- Linhas 12–16: A classe [DummyFragmentState] não está listada entre as classes filhas da classe [CoreState]. No entanto, o método [saveFragment] da classe [BlinkFragment] devolve atualmente um tipo [DummyFragmentState]. Se mantido tal como está, a serialização/desserialização da sessão falhará e a sessão não será restaurada, levando a uma falha da aplicação;
O método [saveFragment] do fragmento [BlinkFragment] deve ser reescrito da seguinte forma:
@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]
}
Tarefa: Em cada fragmento, modifique o método [getNumView] para que retorne o número do fragmento e o método [saveFragment] para que retorne uma instância da classe de estado do fragmento (conforme mostrado acima).
5.6.10.7. Gestão do ciclo de vida do fragmento
Aqui, focamo-nos no ciclo de vida do fragmento [ConfigFragment], mais concretamente nos quatro métodos:
- [saveFragment]: deve guardar o estado do fragmento para que possa ser restaurado posteriormente;
- [initFragment]: que deve inicializar determinados campos do fragmento, se necessário. Este método é chamado quando a aplicação é iniciada e sempre que o dispositivo é rodado. Mais precisamente, é chamado quando o fragmento se torna visível após um dos dois eventos anteriores;
- [initView]: que deve inicializar determinados componentes da vista, se necessário. Este método é chamado sempre que [initFragment] for chamado e quando a vista tiver de ser redesenhada porque o fragmento, em algum momento, saiu da vizinhança do fragmento exibido. Tal como anteriormente, é chamado quando o fragmento se torna visível após um destes eventos;
- [updateOnRestore]: que é executado após os dois métodos anteriores quando o dispositivo foi rodado, mas também quando ocorreu uma navegação. A sua função é restaurar o estado anterior do fragmento;
Estes métodos serão os seguintes:
// 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();
}
- linha 2: o adaptador ListView para os Arduinos. É uma variável global porque é utilizada em diferentes métodos;
- linhas 7–12: o método [saveFragment] guarda a visibilidade do TextView txtMsgErreurUrlServiceRestVisible (linha 10) num tipo [ConfigFragmentState];
- linhas 14–19: o método [initFragment] inicializa o adaptador da linha 2 com a lista de Arduinos atualmente na sessão (linha 17). Note-se que a função de [initFragment] é inicializar os campos do fragmento. Aqui, esta inicialização deve ser realizada em todos os casos, quer seja a primeira visita (previousState == null) ou não;
- linha 17: vemos que o adaptador está vinculado à fonte de dados [session.getCheckedArduinos]. Esta não deve ter o valor null. Por este motivo, o campo [session.checkedArduinos] é inicializado com uma lista vazia na sessão:
// la liste des Arduinos
private List<CheckedArduino> checkedArduinos = new ArrayList<>();
- linhas 21–35: o método [initView] é responsável por inicializar determinados componentes da interface visual, particularmente aqueles cujos valores não são preservados quando o dispositivo é rodado;
- linha 24: o Arduino ListView é vinculado ao adaptador da linha 2;
- linhas 28–32: a primeira visita é distinguida das outras visitas;
- linha 29: na primeira visita, deve ser exibido um [ListView] vazio. Isto acontece porque, na primeira visita, o adaptador [ListView] estava associado a uma lista vazia (linha 17);
- linha 31: a mensagem de erro é ocultada;
- linhas 32–36: o caso em que esta não é a primeira visita;
- o [ListView] já se encontra no estado correto a partir da linha 24. Não há mais nada a fazer;
- linhas 34–35: a mensagem de erro é restaurada para o estado em que se encontrava quando o fragmento foi guardado pela última vez;
- linhas 31–36: o método [updateOnRestore] deve restaurar o fragmento ao seu estado inicial. Chegamos ao método [updateOnRestore] de duas maneiras:
- ou porque o dispositivo foi rodado. Neste caso, todas as inicializações necessárias já foram realizadas em [initView];
- ou porque estamos a navegar de um separador para o separador [Config]. Se o fragmento [Config] tiver saído da vizinhança dos fragmentos exibidos desde que o deixámos, o método [initView] já foi executado e o fragmento já se encontra no estado desejado. Se o fragmento [Config] não tiver saído da lista de fragmentos exibidos desde que foi abandonado, os seus componentes visuais não alteraram de estado e não há nada a fazer;
Vemos que o método [updateOnRestore] não tem nada para fazer. Às vezes é assim, outras vezes não. A diferença vem do método [updateOnSubmit]: se este método fizer algo que torne desnecessárias certas inicializações feitas em [initView], então essas inicializações devem ser feitas no método [updateOnRestore]. Tomemos o exemplo de um botão de opção com três valores: V1, V2 e V3. Talvez, no caso de navegação associada a uma ação [SUBMIT], o botão de opção selecionado deva ser sempre aquele com o valor V1. Neste caso, restaurar o valor do botão de opção no método [initView] é desnecessário, porque no caso de um [SUBMIT], este valor será substituído pelo fornecido pelo método [updateOnSubmit]. É, portanto, preferível mover esta restauração para o método [updateOnRestore] para evitar realizar uma operação desnecessária.
- linhas 48–52: o método [notifyEndOfUpdates] é executado após todos os anteriores;
- Linha 51: Os botões são definidos para o seu estado inicial: o botão [Refresh] é exibido e o botão [Cancel] é ocultado:
Tarefa: Adicione o código acima ao [ConfigFragment] e, em seguida, execute a aplicação. Repare que, quando roda o dispositivo, o separador [Config] mantém o seu estado (mensagem de erro, lista de Arduinos). Verifique se o mesmo comportamento ocorre quando simplesmente navega do separador [Config] para o separador [Commands] --> separador [Config]. Neste último caso, se tiver definido a adjacência do fragmento como 1 em [IMainActivity], a vista [ConfigFragment] é destruída ao mudar para o separador [Commands] e recriada ao regressar ao separador [Config]. Durante os testes, examine os registos.
5.6.10.8. Melhoria do código
O código do fragmento [ConfigFragment] pode ser melhorado. Por exemplo, escrevemos:
// 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);
}
- Podemos ver que nas linhas 9 e 16, estamos a utilizar uma variável local que está desligada do campo na linha 2, apesar de estarmos a tentar manipular a mesma entidade;
Atualizamos o código da seguinte forma:
// 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);
...
}
- Quando o método na linha 5 é executado, o ciclo de vida do fragmento já foi concluído. Portanto:
- o adaptador na linha 2 foi associado à sua fonte de dados (linha 41);
- o [ListView] dos Arduinos conectados foi vinculado a este adaptador (linha 48);
Quando quisermos alterar a exibição do [ListView], precisamos de fazer duas coisas:
- alterar o conteúdo da fonte de dados [session.checkedArduinos];
- notificar o adaptador desta alteração utilizando a instrução [adapterListArduinos.notifyDataSetChanged()];
É importante alterar o conteúdo da fonte de dados, e não a própria fonte de dados. Se alterarmos a própria fonte de dados, a operação [adapterListArduinos.notifyDataSetChanged()] continuará a exibir a fonte de dados antiga. Teríamos então de associar o adaptador à nova fonte de dados.
O código é o seguinte:
- linha 27: recuperamos a fonte de dados;
- linha 28: limpamos a fonte de dados. Por este motivo, removemos o método [clearArduinos];
- linhas 29–31: adicionamos novos itens a esta lista agora vazia;
- linha 33: dizemos ao adaptador para atualizar. Isto irá atualizar a exibição do [ListView] associado;
Tarefa: Efetue estas alterações e verifique se a sua aplicação continua a funcionar.
5.6.11. Comunicação entre vistas
Para verificar a comunicação entre as vistas, faremos com que todas as outras vistas exibam a lista de Arduinos obtida pela vista [Config]. Comecemos pela vista [blink.xml]. Enquanto anteriormente não exibia nada, agora irá exibir a lista de Arduinos ligados:

![]() |
O código XML para a vista [blink.xml] será o seguinte:
<?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>
Este código foi retirado diretamente da visualização [config.xml]. Simplesmente modificámos a margem superior na linha 19.
Tarefa: Duplique este código nas vistas [commands.xml, pinread.xml, pinwrite.xml].
O código do fragmento [BlinkFragment] associado à vista [blink.xml] também está a mudar:
![]() |
// 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);
}
...
- linhas 2-3: o componente [ListView] para os Arduinos ligados;
- linha 6: o adaptador para este [ListView];
- linhas 12-23: o código para os métodos [initFragment] e [initView] é o mesmo que já foi utilizado para o fragmento [ConfigFragment];
- linha 15: quando o fragmento precisa de ser reiniciado, reiniciamos o adaptador da linha 2, associando-o à lista de Arduinos armazenada na sessão. O último parâmetro [true] do construtor [ListArduinosAdapter] significa que queremos ver uma caixa de seleção ao lado de cada Arduino;
- linha 22: quando a vista do fragmento precisa de ser reiniciada, associamos o [ListView] dos Arduinos ligados ao adaptador da linha 6;
Tarefa: Duplique este código nos outros fragmentos [CommandsFragment, PinReadFragment, PinWriteFragment]. Execute a aplicação e verifique se cada separador apresenta agora a lista de Arduinos ligados. Verifique também se, ao marcar Arduinos num separador e navegar para outro separador, estes permanecem marcados neste último.
Nota: A razão pela qual os Arduinos permanecem marcados é a seguinte. A classe [ListArduinosAdapter] foi apresentada na Secção 5.6.10.4. O código relacionado com a caixa de seleção é o seguinte:
// 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);
}
});
}
- Linhas 11–15: Se uma caixa de seleção estiver marcada na guia X, a propriedade [checked] do Arduino na linha 2 é definida como true (linha 14);
- ao mudar para o separador Y, é apresentada a [ListView] dos Arduinos nesse separador. Na linha 9, vemos que se o Arduino na linha 2 tiver a sua propriedade [checked] definida como true, então a caixa de seleção [ck] na linha 5 será marcada;
5.6.12. A camada [DAO]
![]() |
Nota: Para esta secção, reveja a implementação da camada [DAO] no projeto [example-16B] (ver secção 2.8.3).
Até agora, gerámos manualmente a lista de Arduinos ligados. Vamos agora solicitá-la ao servidor web / jSON. Para tal, vamos construir a camada [DAO]:
![]() |
5.6.12.1. A interface IDao
A interface [IDao] da camada [DAO] será a seguinte:
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();
}
- linhas 11-26: estas linhas já estão presentes na interface [IDao] do projeto modelo [client-android-skel];
- linha 30: o método [getArduinos] devolve a lista de Arduinos ligados como um observável do tipo Observable<[Response<List<Arduino>>>] ;
Note que [Response<T>] é o tipo de todas as respostas enviadas pelo servidor na forma de uma cadeia 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. A interface [WebClient]
![]() |
A interface [WebClient] é uma interface para a qual a biblioteca AA fornece uma implementação. Esta interface será a seguinte:
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();
}
- linhas 15-19: estas linhas estão incluídas por predefinição na interface [WebClient] do projeto modelo [client-android-skel];
- linha 23: a URL do servidor utilizada para recuperar a lista de Arduinos através de um pedido GET. Note-se que esta URL é relativa à URL raiz [RestClientRootUrl] na linha 16;
- linha 24: o servidor devolve uma cadeia JSON do tipo [Response<List<Arduino>>]. Esta cadeia JSON é automaticamente deserializada para o tipo [Response<List<Arduino>>] utilizando o conversor JSON [MappingJackson2HttpMessageConverter] da linha 15;
5.6.12.3. A classe [Dao]
A classe [Dao] implementa a interface [IDao] da seguinte forma:
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();
}
});
}
}
- linhas 19–87: estas linhas fazem parte da classe [Dao] no projeto [client-android-skel];
- linhas 91–100: implementação do método [getArduinos];
- linha 94: o método [getResponse] da classe pai é chamado. O único parâmetro deste método é uma instância da interface [IRequest<T>];
- linhas 95–99: o único método da interface [IRequest<T>] é o método [T getResponse()];
- linha 94: o tipo T de [IRequest<T>] deve ser o tipo T do resultado Observable<T> do método na linha 92, portanto, aqui, um tipo [Response<List<Arduino>>];
- linha 97: o método [IRequest.getResponse()] delega o trabalho ao método [webClient.getArduinos()] que introduzimos. [webClient], definido na linha 24, é instanciado pela biblioteca AA e é uma instância da interface [WebClient] que introduzimos;
5.6.13. O [MainActivity]
![]() |
Já apresentámos a atividade [MainActivity] na Secção 5.6.8. Ela estende a classe [AbstractActivity] e, como tal, implementa a interface [IMainActivity], que por sua vez estende a interface [IDao]. Sempre que um método é adicionado à interface [IDao], este deve ser implementado na classe [MainActivity]. O método [IDao.getArduinos] adicionado à interface [IDao] será implementado da seguinte forma na [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();
}
}
- linhas 15–18: o método [getArduinos] é implementado delegando o trabalho à classe [Dao] que acabámos de apresentar e à qual temos uma referência na linha 8;
5.6.14. O fragmento [ConfigFragment] revisitado
Na classe [ConfigFragment], o código executado quando o botão [Refresh] é clicado é atualmente o seguinte:
@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) {
...
}
Precisamos de reescrever as linhas 10–16, que codificavam de forma rígida uma resposta do tipo [Response<List<Arduino>>]. Agora, precisamos de solicitar esta lista à camada [DAO] através da atividade. O código fica assim:
@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);
}
});
}
- linha 8: a URL raiz do serviço web / JSON introduzida pelo utilizador é passada para a camada [DAO] através da atividade. Esta será a URL raiz da interface [WebClient] (ver secção 5.6.12.2);
- linha 10: a classe pai é notificada de que uma tarefa assíncrona está prestes a ser iniciada;
- linhas 12–19: lançamento da tarefa assíncrona que irá devolver a lista de Arduinos ligados ao servidor;
- linha 12: chamar o método [executeInBackground] da classe pai. Este método espera dois parâmetros:
- linha 12: o processo a observar. Este processo é fornecido aqui pelo método [mainActivity.getArduinos()];
- linhas 12–19: uma instância da interface [Action1<T>], onde o tipo T é o tipo fornecido pelo processo, aqui um tipo [Response<List<Arduino>>];
- linhas 14–18: o método chamado quando a tarefa assíncrona retorna o seu resultado do tipo [Response<List<Arduino>>];
- linha 17: a resposta recebida é passada para o método [consumeArduinosResponse] definido anteriormente;
Tarefa: Inicie o servidor conforme descrito na Secção 5.4. Ligue um ou mais Arduinos ao PC no qual o servidor está a ser executado. Em seguida, inicie o cliente Android e verifique se consegue recuperar com sucesso a lista de Arduinos ligados. Observe os registos.

- Introduza o URL indicado em [1]. Este é um dos endereços IP do seu servidor;
- Clique no botão [2];
- Deve ver a lista de Arduinos ligados em [3];
Verifique se esta lista também aparece nos outros separadores.
5.7. Próximos passos
Seguindo o mesmo procedimento utilizado para a vista [Config], implemente e, em seguida, teste as outras quatro vistas da aplicação, uma a uma: [Blink], [PinRead], [PinWrite] e [Commands].
As vistas a criar foram apresentadas na Secção 5.5.
Para cada vista, deve:
- desenhar a vista XML (ver Secção 5.6.9);
- construir o fragmento associado (ver Secção 5.6.10);
- adicionar um método à interface [WebClient] (ver Secção 5.6.12.2);
- adicionar um método à interface [IDao] (ver Secção 5.6.12.2);
- adicionar um método à classe [Dao] (ver secção 5.6.12.3);
- adicionar um método à atividade [MainActivity] (ver secção 5.6.13);
- escrever os manipuladores de eventos do fragmento (ver secção 5.6.14);
- testar e observar os registos;
Nota 1: O exemplo a seguir é o projeto [Example-16B] do curso (ver secção 2.8.3).
Nota 2: Os URLs a consultar e o tipo das respetivas respostas foram apresentados na secção 5.4.2.
Nota 3:
A classe [CommandsFragment] envia uma lista contendo um único comando a ser executado por um ou mais Arduinos. Este comando será encapsulado na seguinte classe [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
...
}
Na interface [WebClient], o método para executar esta lista de comandos será o seguinte:
// envoi de commandes JSON
@Post("/arduinos/commands/{idArduino}")
Response<List<ArduinoResponse>> sendCommands(@Body List<ArduinoCommand> commands, @Path String idArduino);
- linha 2: o URL é solicitado com uma solicitação HTTP POST;
- linha 3: o valor enviado deve ter a anotação [@Body];
Nota 4: Recomenda-se abordar esta tarefa da seguinte forma:
- só avance para a próxima vista depois de a vista atual ter sido criada e testada;
- gerir o estado das vistas apenas após obter uma aplicação funcional em condições normais. Em seguida, para cada vista, percorra o dispositivo para diferentes estados da vista e anote qualquer informação perdida. Estes são os dados que precisam de ser guardados e posteriormente restaurados. A seguir, verifique a navegação: quando sair de um separador e regressar a ele mais tarde, este deve estar no mesmo estado em que o deixou;


















































