5. TP 2 - Controlar Arduinos com um tablet Android
Vamos agora aprender a controlar uma placa Arduino com um tablet. O exemplo a seguir é o do projeto [client-android-skel] do curso (ver parágrafo 2).
5.1. Arquitetura do projeto
O projeto na sua totalidade terá a seguinte arquitetura:
![]() |
- o bloco [1], servidor web / jSON e os Arduinos serão fornecidos;
- terão de construir o bloco [2] e programar o tablet Android para comunicar com o servidor web / jSON.
5.2. O material
Têm à vossa disposição os seguintes elementos:
- um Arduino com uma extensão 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
![]() |
Eis como proceder para ligar os diferentes elementos entre si:
- retire o cabo de rede do seu PC;
- ligue o seu PC ao Arduino através de um cabo de rede;
- o Arduino que terá à sua disposição já estará programado. O seu endereço IP será [192.168.2.2]. Para que o seu PC detecte o Arduino, é necessário atribuir-lhe o 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]. Eis como proceder:
Aceda ao [Panneau de configuration\Réseau et Internet\Centre Réseau et partage]:
![]() |
- em [1], clique na ligação [réseau local];
- em [2], clique no botão [Propriétés] da rede local;
![]() | ![]() |
- em [3], clique nas propriedades [IPv4] do mapa [réseau local];
- em [4], atribua a este cartão 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 a sua chave Wi-Fi, ligue o seu computador à rede Wi-Fi que lhe for indicada. Faça o mesmo com o seu tablet;
- Verifique o endereço Wi-Fi IP do seu PC, digitando [ipconfig] numa janela DOS. Irá encontrar um endereço do tipo [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 Wi-Fi IP do seu tablet. Pergunte ao seu orientador como fazê-lo, caso não saiba. Irá encontrar um endereço do tipo [192.168.x.z];
- desative o firewall do seu PC, caso esteja ativo [Panneau de configuration\Système et sécurité\Pare-feu Windows];
- numa janela do DOS, verifique se o PC e o tablet conseguem comunicar, digitando o comando [ping 192.168.x.z], em que [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 está agora pronta.
5.2.3. O emulador [Genymotion]
O emulador [Genymotion] (ver parágrafo 6.9) substitui vantajosamente o tablet. É praticamente tão rápido e não requer rede Wi-Fi. Recomenda-se a utilização deste método. Poderá utilizar o tablet para a verificação final da sua aplicação.
5.3. Programação dos Arduinos
Aqui, vamos centrar-nos na escrita do código C dos Arduinos:
![]() |
A ler
- instalação do ambiente de desenvolvimento Arduino (ver parágrafo 6.1);
- utilização das bibliotecas jSON (Anexos, parágrafo 6.6);
- no Arduino, testar o exemplo de um servidor IDE (por exemplo, o servidor web) e o de um cliente TCP (por exemplo, o cliente Telnet);
- os anexos sobre o ambiente de programação dos Arduinos no parágrafo 6.1.
![]() |
Um Arduino é um conjunto de pinos ligados a hardware. Estes pinos são entradas ou saídas. O seu valor é binário ou analógico. Para controlar o Arduino, existem duas operações básicas:
- escrever um valor binário/analógico num pino designado pelo seu número;
- ler um valor binário/analógico num pino designado pelo seu número;
A estas duas operações básicas, acrescentaremos uma terceira:
- fazer um LED piscar durante um determinado período de tempo e com uma determinada frequência. Esta operação pode ser realizada chamando repetidamente as duas operações básicas anteriores. Mas veremos nos testes que as trocas de dados entre a camada [DAO] e um Arduino ocorrem na ordem de segundos. Não é, portanto, possível fazer um LED piscar a cada 100 milissegundos, por exemplo. Por isso, vamos implementar esta função de piscar diretamente no próprio Arduino.
O funcionamento do Arduino será o seguinte:
- as comunicações entre a camada [DAO] e um Arduino realizam-se através de uma rede TCP-IP, por meio de trocas de linhas de texto no formato jSON (JavaScript Object Notation);
- ao arrancar, o Arduino liga-se à porta 100 de um servidor de registo presente na camada [DAO]. Envia ao servidor uma única linha de texto:
Trata-se de uma cadeia jSON que identifica o Arduino que se está a ligar:
- id: um identificador do Arduino;
- desc: uma descrição do que o Arduino é capaz de fazer. Aqui, indicámos simplesmente o tipo do Arduino;
- mac: endereço MAC do Arduino;
- port: o número da porta na qual o Arduino irá aguardar os comandos da camada [DAO].
Todas estas informações são do tipo cadeias de caracteres, exceto a porta, que é um número inteiro.
- Assim que o Arduino se registar no servidor de registo, fica à escuta na porta que indicou ao servidor (102, acima). Aguarda comandos jSON com o seguinte formato:
Trata-se de uma cadeia jSON com os seguintes elementos:
- id: um identificador do comando. Pode ser qualquer valor;
- ac: uma ação. Existem três:
- pw (pin write) para escrever um valor num pino,
- pr (pin read) para ler o valor de um pino,
- cl (piscar) para fazer um LED piscar;
- pa: os parâmetros da ação. Dependem da ação.
- O Arduino envia sistematicamente uma resposta ao seu cliente. Esta resposta é uma cadeia de caracteres jSON com o seguinte formato:
onde
- id: o identificador do comando ao qual se responde;
- er (erro): um código de erro, caso tenha ocorrido um erro; caso contrário, 0;
- e (estado): um dicionário sempre vazio, exceto no caso do comando de leitura pr. Nesse caso, o dicionário contém o valor do pino n.º x solicitado.
Eis alguns exemplos destinados a esclarecer as especificações anteriores:
Fazer piscar o LED n.º 8 10 vezes com um intervalo de 100 milissegundos:
Comando | |
Resposta |
Os parâmetros pa do comando cl são: a duração dur, em milissegundos, de um piscar, o número nb de piscares e o n.º do pino do LED.
Escrever o valor binário 1 no pino n.º 7:
Comando | |
Resposta |
Os parâmetros pa do comando pw são: o modo mod b (binário) ou a (analógico) da gravação, o valor val a gravar e o n.º do pino. Para uma gravação binária, val é 0 ou 1. Para uma gravação analógica, val está no intervalo [0,255].
Gravar o valor analógico 120 no pino n.º 2:
Comando | |
Resposta |
Ler o valor analógico do pino 0:
Comando | |
Resposta |
Os parâmetros «pa» do comando «pr» são: o modo «mod b» (binário) ou «a» (analógico) da leitura, o número do pino. Se não houver erros, o Arduino insere no dicionário «et» da sua resposta o valor do pino solicitado. Aqui, «pin0» indica que foi solicitado o valor do pino n.º 0 e «1023» é esse valor. Na leitura, um valor analógico estará no intervalo [0, 1024].
Apresentámos os três comandos cl, pw e pr. Podemos questionar-nos por que razão não utilizámos campos mais explícitos nas cadeias jSON, como «action» em vez de «ac», «pinwrite» em vez de «pw», «parametres» em vez de «pa», etc. Um Arduino tem uma memória muito reduzida. No entanto, as cadeias de caracteres jSON trocadas com o Arduino contribuem para a ocupação de memória. Por isso, optou-se por encurtá-las ao máximo.
Vejamos agora 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 indicar o parâmetro pin. O Arduino devolveu o código de erro 302.
Comando | |
Resposta |
Enviámos um comando pinread desconhecido (é o 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 bloquear, independentemente do comando que lhe seja enviado. Antes de executar um comando jSON, ele verifica se este está correto. Assim que surge um erro, o Arduino interrompe a execução do comando e devolve ao seu cliente a cadeia de erro jSON. Mais uma vez, devido às limitações de espaço de memória, é devolvido um código de erro em vez de uma mensagem completa.
O código do programa executado no Arduino é fornecido nos exemplos deste documento:
![]() |
Para o transferir para o Arduino:
- ligue-o ao seu PC;
![]() | ![]() |
- no [1], abra o ficheiro [arduino_uno.ino]. O Arduino IDE irá iniciar-se e carregar o ficheiro;
Nota: o código foi originalmente criado e testado com o IDE ARDUINO 1.5.x. Desde então, foram lançadas outras versões do IDE. O código não funcionou com o IDE ARDUINO 1.6.x. Parece haver um problema de compatibilidade com versões anteriores entre as versões 1.6 e 1.5.
- No [2-4], indique o tipo de Arduino utilizado;
![]() | ![]() |
- no [5-7], indique em que porta série do PC se encontra;
- no [8], carregue o programa [arduino_uno] no Arduino;
O código do programa está muito bem comentado. O leitor interessado poderá consultá-lo. Assinalamos apenas as linhas de código que permitem configurar a comunicação bidirecional cliente/servidor entre o Arduino e o PC:
#include <SPI.h>
#include <Ethernet.h>
#include <ajSON.h>
// ---------------------------------- CONFIGURATION DE O ARDUINO UNO
// endereço MAC do Arduino UNO
byte macArduino[] = {
0x90, 0xA2, 0xDA, 0x0D, 0xEE, 0xC7 };
char * strMacArduino="90:A2:DA:0D:EE:C7";
// a morada IP do Arduino
IPAddress ipArduino(192,168,2,2);
// o seu identificador
char * idArduino="cuisine";
// porta do servidor Arduino
int portArduino=102;
// descrição do Arduino
char * descriptionArduino="contrôle domotique";
// o servidor Arduino funcionará na porta 102
EthernetServer server(portArduino);
// IP do servidor de registo
IPAddress ipServeurEnregistrement(192,168,2,1);
// porta do servidor de registo
int portServeurEnregistrement=100;
// o cliente Arduino do servidor de registo
EthernetClient clientArduino;
// o comando do cliente
char commande[100];
// a resposta do Arduino
char message[100];
// inicialização
void setup() {
// O monitor de série permitirá acompanhar as trocas de dados
Serial.begin(9600);
// início da ligação Ethernet
Ethernet.begin(macArduino,ipArduino);
// memória disponível
Serial.print(F("Memoire disponible : "));
Serial.println(freeRam());
}
// loop infinito
void loop()
{
...
}
- linha 8: o endereço MAC do Arduino. Não tem grande importância neste contexto, uma vez que o Arduino estará numa rede privada onde existe um PC e um ou mais Arduinos. Basta que o endereço MAC seja único nessa rede privada. Normalmente, a placa de rede do Arduino tem um autocolante onde está indicado o endereço MAC da placa. Se esse autocolante não estiver presente e se não souber o endereço MAC da placa, pode colocar o que quiser na linha 8, desde que a regra de exclusividade do endereço MAC na rede privada seja respeitada;
- linha 11: o endereço IP da placa. Mais uma vez, pode-se colocar o que se quiser do tipo [192.168.2.x] e variar o valor de x para os diferentes Arduinos da rede privada;
- linha 13: identificador do Arduino. Deve ser único entre os identificadores dos Arduinos de uma mesma rede privada;
- linha 15: a porta de serviço do Arduino. Pode-se inserir o que se quiser;
- linha 17: a descrição da função do Arduino. Pode-se definir o que se 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 deste serviço de registo. Não deve ser alterada;
5.4. O servidor web / jSON
5.4.1. Instalação

O ficheiro binário Java do servidor web / jSON é-lhe fornecido:
![]() |
Abra uma janela de comandos e introduza o seguinte comando:
Se o [java.exe] não estiver no PATH da janela de comandos, será necessário introduzir o caminho completo do [java.exe] (normalmente C:\Program Files\java\...).
Será aberta uma janela DOS que 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 [/**] para o manipulador do tipo [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/**] para o manipulador do tipo [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: é iniciado um servidor Tomcat incorporado;
- linha 15: a servlet [dispatcherServlet] do Spring MVC é carregada e executada;
- linha 18: o Rest URL [/arduinos/blink/{idCommande}/{idArduino}/{pin}/{duree}/{nombre}] é detetado;
- linha 19: é detetado o Rest URL [/arduinos/commands/{idArduino}];
- linha 20: é detetado o URL Rest [/arduinos/];
- linha 21: é detetado o URL Rest [/arduinos/pinRead/{idCommande}/{idArduino}/{pin}/{mode}];
- linha 22: o URL Rest [/arduinos/pinWrite/{idCommande}/{idArduino}/{pin}/{mode}/{valeur}] é detetado;
- linha 26: o servidor de registo dos Arduinos é iniciado;
Ligue o seu Arduino ao PC, caso ainda não o tenha feito. O firewall do PC deve estar desativado. Em seguida, num navegador, aceda ao URL [http://localhost:8080/arduinos]:
![]() |
Deve aparecer o identificador do Arduino ligado. Se não aparecer nada, lembre-se de reiniciar o Arduino. Este possui um botão para esse efeito.
O servidor web / jSON está agora instalado.
5.4.2. Os URL expostos pelo serviço web / jSON
A ler: projeto [Exemple-15] (ver parágrafo 1.16.1);
O serviço web / jSON foi implementado com o Spring MVC e expõe as seguintes URL:
@Controller
public class WebController {
// camada de negócios
@Autowired
private IMetier métier;
// lista de Arduinos
@RequestMapping(value = "/arduinos", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
@ResponseBody
public String getArduinos() throws JsonProcessingException {
...
}
// luz intermitente
@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 {
...
}
// envio de comandos 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 {
...
}
// leitura do pino
@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 {
....
}
// gravação no pino
@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> {
// ----------------- propriedades
// estado da operação
private int status;
// eventuais mensagens de estado
private List<String> messages;
// corpo da resposta
private T body;
// construtores
public Response() {
}
public Response(int status, List<String> messages, T body) {
this.status = status;
this.messages = messages;
this.body = body;
}
// getters e setters
...
}
O URL [/arduinos] envia 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 {
// dados
private String id;
private String description;
private String mac;
private String ip;
private int port;
// getters e 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 aguarda comandos;
Os URL:
- [/arduinos/blink/{idCommande}/{idArduino}/{pin}/{duree}/{nombre}];
- [/arduinos/pinRead/{idCommande}/{idArduino}/{pin}/{mode}];
- [/arduinos/pinWrite/{idCommande}/{idArduino}/{pin}/{mode}/{valeur}];
- [/arduinos/commands/{idArduino}];
enviam uma resposta do tipo [Response<ArduinoResponse>], em que a classe [ArduinoResponse] representa a resposta padrão de um Arduino:
public class ArduinoResponse implements Serializable {
private String json;
private String id;
private String erreur;
private Map<String, Object> etat;
// getters e setters
...
}
- [json]: a cadeia jSON enviada por um Arduino e que não pôde ser descodificada (caso de erro), null caso contrário;
- [id]: o identificador do comando ao qual o Arduino responde;
- [erreur]: um código de erro, 0 se for OK, outro valor caso contrário;
- [etat]: um dicionário que contém a resposta específica ao comando. Na maioria das vezes, 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. Testes do serviço web / jSON
Familiarize-se com o servidor web / jSON testando os seguintes URL:
Eis algumas capturas de ecrã do resultado que deve obter:
Obter a lista dos Arduinos ligados:
![]() |
A cadeia jSON recebida do servidor web / jSON é um objeto com os seguintes campos:
- [status]: se for 0, indica que não houve erro; caso contrário, houve um erro;
- [messages]: uma lista de mensagens que explicam o erro, caso tenha ocorrido algum:
- [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]: identificador do Arduino. Dois Arduinos não podem ter o mesmo identificador;
- [description]: breve descrição da funcionalidade do Arduino;
- [mac]: endereço MAC do Arduino;
- [ip]: endereço IP do Arduino;
- [port]: porta na qual aguarda comandos;
Fazer piscar o LED do pino n.º 8 do Arduino identificado por [cuisine], 20 vezes a cada 100 ms:
![]() |
A cadeia jSON recebida do servidor web / jSON é um objeto com os seguintes campos:
- [status]: se for 0, indica que não houve erro; caso contrário, houve um erro;
- [messages]: uma lista de mensagens que explicam o erro, caso tenha ocorrido algum erro:
- [body]: a resposta do Arduino caso não tenha ocorrido qualquer erro:
- [id]: identificador do comando. Este identificador é o 1 em [/blink/1]. O Arduino inclui este identificador de comando na sua resposta;
- [erreur]: um número de erro. Um valor diferente de 0 indica um erro;
- [etat]: é utilizado apenas para a leitura de um pino. Nesse caso, tem como valor o valor do pino;
- [json]: é utilizado apenas em caso de erro jSON entre o cliente e o servidor. Tem então como valor a cadeia de caracteres errada jSON enviada pelo Arduino;
Leitura analógica do pino n.º 0 do Arduino identificado por [cuisine]:
![]() |
A cadeia jSON recebida do servidor web / jSON é análoga à anterior, com a única diferença no campo [etat], que representa o valor do pino n.º 0.
Leitura binária do pino n.º 5 do Arduino identificado por [cuisine]:
![]() |
A cadeia jSON recebida do servidor web / jSON é análoga à anterior.
Gravação binária do valor 1 no pino n.º 8 do Arduino identificado por [cuisine]:
![]() |
A cadeia jSON recebida do servidor web / jSON é análoga à anterior.
O teste do URL [http://localhost:8080/arduinos/commands/cuisine] é mais complexo. O método do servidor web / jSON, que processa este URL, aguarda um pedido POST que não é possível simular simplesmente com um navegador. Para testar este URL, pode-se utilizar um navegador Chrome com a extensão [Advanced REST Client] (ver parágrafo 6.13):
![]() |
- em [1], o URL do método web / jSON a testar;
- em [2], o método POST para enviar o pedido;
- em [3-4], o valor enviado é o de jSON;
- em [5], a cadeia jSON foi enviada. É importante reparar nos parênteses retos que iniciam e terminam a lista. Aqui, na lista, existe apenas um comando jSON que faz piscar o pino n.º 8, 10 vezes a cada 100 ms;
- em [6], envia-se o pedido;
![]() |
- 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 das respostas do Arduino a cada um dos comandos jSON enviados.
Vamos ver o que acontece quando se envia um comando jSON com sintaxe incorreta para o Arduino:
![]() |
Recebemos então a seguinte resposta:
![]() |
Vemos que, na resposta do Arduino, o número de erro é [104], indicando assim que o comando [xx] não foi reconhecido.
5.5. Testes no cliente Android
![]() |
Apresenta-se o ficheiro executável do cliente Android já concluído:
![]() |
Com o rato, arraste o ficheiro executável [app-debug.apk] acima para um emulador de tablet [GenyMotion]. Este será então guardado e, em seguida, executado. Inicie também o servidor web / jSON, caso ainda não o tenha feito. Ligue o Arduino ao PC com um LED acoplado. O cliente Android permite gerir os Arduinos à distância. Apresenta ao utilizador os seguintes ecrãs.
O separador [CONFIG] permite ligar-se ao servidor e obter a lista dos Arduinos ligados:

- em [1], introduza o endereço IP [192.168.2.1] atribuído ao seu PC (ver parágrafo 5.2).
O separador [PINWRITE] permite escrever um valor num pino de um Arduino:


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

O separador [BLINK] permite fazer piscar um LED de um Arduino:

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

5.6. O cliente Android do serviço web / jSON
Passamos agora à programação do cliente Android.
5.6.1. A arquitetura do cliente
A arquitetura do cliente Android será a do projeto [Exemple-15] (ver parágrafo 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 piscar dois LEDs colocados em dois Arduinos, ao mesmo tempo e não um após o outro. Assim, o nosso cliente Android utilizará uma tarefa assíncrona por Arduino e essas tarefas serão executadas em paralelo.
5.6.2. O projeto do cliente no Android Studio
Duplique o projeto [client-android-skel] (ver parágrafo 2) no projeto [client-arduinos-01] (se necessário, consulte novamente como duplicar um projeto Gradle no parágrafo 1.15):

5.6.3. As cinco vistas XML
![]() |
Haverá cinco vistas XML:
- [blink]: para fazer piscar um LED de um Arduino. Está associada ao fragmento [BlinkFragment];
- [commands]: para enviar um comando jSON a um Arduino. Está associada ao fragmento [CommandsFragment];
- [config]: para configurar o URL do serviço web / jSON e obter a lista inicial de Arduinos ligados. Está associado ao fragmento [ConfigFragment];
- [pinread]: para ler o valor binário ou analógico de um pino de um Arduino. Está associado ao fragmento [PinReadFragment];
- [pinwrite]: para escrever um valor binário ou analógico num pino de um Arduino. Está associada 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 num contentor [RelativeLayout] (linhas 7-10), que por sua vez está incluído num contentor [ScrollView] (linhas 2-11). Isto garante que possamos «deslizar» a vista caso esta exceda o tamanho do ecrã de um tablet;
Tarefa: crie as cinco vistas XML.
5.6.4. O menu dos fragmentos
Sabemos que os fragmentos de um projeto construído com [client-android-skel] têm de estar associados a um menu, mesmo que este esteja vazio. Neste caso, a aplicação não terá menu. O menu vazio já se encontra no projeto;
![]() |
5.6.5. Os cinco fragmentos da aplicação
![]() | ![]() |
Tarefa: duplique o fragmento [DummyFragment] nos cinco fragmentos da aplicação, tal como 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 {
// campos herdados da classe pai -------------------------------------------------------
...
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 | Vista |
ConfigFragment | |
PinReadFragment | |
PinWriteFragment | |
CommandsFragment | |
BlinkFragment | |
5.6.6. Os estados dos fragmentos
![]() | ![]() |
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á, neste caso, a seguinte:
package client.android.architecture.custom;
import client.android.architecture.core.ISession;
import client.android.dao.service.IDao;
public interface IMainActivity extends IDao {
// acesso à sessão
ISession getSession();
// mudança de vista
void navigateToView(int position, ISession.Action action);
// gestão da espera
void beginWaiting();
void cancelWaiting();
// constantes da aplicação -------------------------------------
// modo de depuração
boolean IS_DEBUG_ENABLED = true;
// tempo máximo de espera pela resposta do servidor
int TIMEOUT = 1000;
// tempo de espera antes da execução do pedido do cliente
int DELAY = 000;
// autenticação básica
boolean IS_BASIC_AUTHENTIFICATION_NEEDED = false;
// adjacência dos fragmentos
int OFF_SCREEN_PAGE_LIMIT = 1;
// barra de separadores
boolean ARE_TABS_NEEDED = true;
// imagem de espera
boolean IS_WAITING_ICON_NEEDED = true;
// número de fragmentos
int FRAGMENTS_COUNT = 5;
// número de visualizações
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 possui separadores;
- linha 43: esta aplicação tem cinco fragmentos;
- linhas 46-50: os números dos cinco fragmentos;
- linha 34: adjacência dos fragmentos. O programador pode introduzir aqui um valor no 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 {
// fragmento visitado ou não
protected boolean hasBeenVisited = false;
// estado do eventual menu do fragmento
protected MenuItemState[] menuOptionsState;
// getters e setters
...
}
- linhas 12-16: é necessário declarar aqui as classes dos estados dos cinco fragmentos;
5.6.8. A classe [MainActivity]
![]() |
A classe [MainActivity] será a seguinte:
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 {
// camada [DAO]
@Bean(Dao.class)
protected IDao dao;
// sessão
private Session session;
// métodos da classe pai -----------------------
@Override
protected void onCreateActivity() {
// registo
if (IS_DEBUG_ENABLED) {
Log.d(className, "onCreateActivity");
}
// sessão
this.session = (Session) super.session;
// criação das cinco separadores
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) {
// exibe-se o fragmento na posição n.º
navigateToView(position, ISession.Action.NAVIGATION);
}
@Override
protected int getFirstView() {
return IMainActivity.VUE_CONFIG;
}
// implementação IDao -----------------------------------------
}
- linhas 46-50: criação dos cinco separadores da aplicação;
- linha 48: os títulos dos separadores são fornecidos pelo método das 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: define-se um título para cada um dos fragmentos. Estes títulos serão procurados no ficheiro [res / values / strings.xml]
![]() |
O conteúdo do ficheiro [strings.xml] é o seguinte:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- nome da aplicação -->
<string name="app_name">[arduinos-client-01]</string>
<!-- Fragmentos e separadores -->
<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 anteriores e compile o projeto. Não deve haver erros.
Execute o projeto. Deverá obter a seguinte visualização no emulador:

Analise os registos que acompanharam a exibição da primeira vista e acompanhe as diferentes etapas executadas. Navegue de um separador para outro e continue a acompanhar os registos.
5.6.9. A vista XML [config]
A vista XML [config] será a seguinte:
![]() | ![]() |
A visualização acima é obtida com o 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 cadeias de caracteres (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>
<!-- Fragmentos e separadores -->
<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>
<!-- Configuração -->
<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>
<!-- aplicação -->
<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>
<!-- Margens de ecrã predefinidas, de acordo com as diretrizes de design do Android. -->
<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>
<!-- aplicação -->
<dimen name="titre">30dp</dimen>
</resources>
Esta técnica não foi utilizada para todas as dimensões. No entanto, é a técnica recomendada. Permite alterar as dimensões num único local.
Tarefa: crie os elementos acima referidos.
Execute novamente o seu projeto. Deverá obter a seguinte visualização:

5.6.10. O fragmento [ConfigFragment]
![]() |
Para gerir 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 {
// elementos da interface visual
@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() {
}
// gestão do ciclo de vida do fragmento -------------------------------------
@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) {
// Primeira visita?
if(previousState==null){
txtMsgErreurUrlServiceRest.setVisibility(View.INVISIBLE);
}
}
@Override
protected void updateOnSubmit(CoreState previousState) {
}
@Override
protected void updateOnRestore(CoreState previousState) {
}
@Override
protected void notifyEndOfUpdates() {
// botões
initButtons();
}
@Override
protected void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled) {
}
// métodos privados --------------------------------------------
private void initButtons() {
// o botão [Exécuter] substitui o botão [Annuler]
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: sempre que o fragmento for apresentado, o botão [Annuler] será ocultado (linha 82) e o botão [Rafraîchir] será apresentado (linhas 86-87). Com efeito, nesta aplicação, um fragmento não pode ser apresentado enquanto estiver em curso uma operação assíncrona e, por isso, o botão [Annuler] fica visível;
Tarefa: crie os elementos acima referidos.
Execute esta nova versão. A primeira vista deve agora ser a seguinte:

5.6.10.1. O botão [Rafraîchir]
Por enquanto, vamos tratar o clique no botão [Rafraîchir] da seguinte forma:
@Click(R.id.btn_Rafraichir)
protected void doRafraichir() {
// vamos iniciar uma tarefa - preparamos a espera
beginWaiting(1);
}
@Click(R.id.btn_Annuler)
protected void doAnnuler() {
if (isDebugEnabled) {
Log.d(className, "Annulation demandée");
}
// cancelamos as tarefas assíncronas
cancelRunningTasks();
}
protected void beginWaiting(int numberOfRunningTasks) {
// prepara-se a espera das tarefas
beginRunningTasks(numberOfRunningTasks);
// o botão [Annuler] substitui o botão [Rafraîchir]
btnRafraichir.setVisibility(View.INVISIBLE);
btnAnnuler.setVisibility(View.VISIBLE);
}
// gestão do ciclo de vida do fragmento -------------------------------------
...
@Override
protected void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled) {
// botões no seu estado inicial
initButtons();
}
// métodos privados --------------------------------------------
private void initButtons() {
// o botão [Exécuter] substitui o botão [Annuler]
btnAnnuler.setVisibility(View.INVISIBLE);
btnRafraichir.setVisibility(View.VISIBLE);
}
- linhas 1-5: o método executado ao clicar no botão [Rafraîchir];
- linha 4: inicia-se a espera;
- linha 18: passamos para a classe pai o número de tarefas assíncronas que vamos iniciar. A imagem de espera irá aparecer;
- linhas 20-21: esta espera resultará no aparecimento do botão [Annuler], no desaparecimento do botão [Rafraîchir] e no aparecimento da imagem de espera. Não acontece mais nada. No entanto, o utilizador pode clicar no botão [Annuler]. O método das linhas 7-14 será então executado;
- linha 13: solicita-se à classe pai que cancele todas as tarefas. A classe irá fazê-lo e, em resposta, chamará o método das linhas 25-29 para indicar que todas as tarefas estão concluídas. O parâmetro [runningTasksHaveBeenCanceled] assumirá o valor true para indicar que as tarefas foram canceladas;
- linhas 35-36: o botão [Annuler] desaparecerá, enquanto o botão [Rafraîchir] reaparecerá.
Tarefa: Efetue estas alterações e, em seguida, execute o projeto. Verifique se o botão [Rafraîchir] inicia a espera e se o botão [Annuler] a interrompe. Observe os registos.
5.6.10.2. Verificação dos dados introduzidos
Na versão anterior, não verificávamos a validade do valor introduzido em URL. Para o verificar, adicionamos o seguinte código em [ConfigFragment]:
// os valores introduzidos
private String urlServiceRest;
@Click(R.id.btn_Rafraichir)
protected void doRafraichir() {
// verificam-se os dados introduzidos
if (!pageValid()) {
return;
}
// vai-se iniciar uma tarefa — prepara-se a espera
beginWaiting(1);
}
// verificação dos dados introduzidos
private boolean pageValid() {
// inicialmente, nenhuma mensagem de erro
txtMsgErreurUrlServiceRest.setVisibility(View.INVISIBLE);
// recuperam-se o IP e a porta do servidor
urlServiceRest = String.format("http://%s", edtUrlServiceRest.getText().toString().trim());
// verifica-se a sua validade
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) {
// exibição de mensagem de erro
txtMsgErreurUrlServiceRest.setVisibility(View.VISIBLE);
// regresso ao UI
return false;
}
// Está tudo bem
return true;
}
- linha 2: a entrada URL;
- linhas 7-9: antes de fazer qualquer coisa, verificamos a validade dos dados introduzidos;
- linha 19: recuperamos o URL introduzido e adicionamos-lhe o prefixo [http://];
- linha 22: tenta-se construir um objeto URI (Uniform Resource Identifier) com ele. Se o URL introduzido estiver sintaticamente incorreto, ocorrerá uma exceção;
- linhas 23-27: é gerada uma exceção se o URI estiver correto, mas existirem, no entanto, os valores [host==null] e [port==-1]. Este é um caso possível;
- linha 30: ocorreu uma exceção. É apresentada a mensagem de erro;
- linha 32: devolve-se [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: crie os elementos acima referidos.
Teste esta nova versão e verifique se os URL inválidos são devidamente sinalizados.
5.6.10.3. Exibição da lista de Arduinos
![]() |
As diferentes vistas vão precisar de apresentar a lista dos Arduinos ligados. Para tal, vamos definir várias classes e uma vista XML:
- um Arduino será representado pela classe [Arduino] [1];
- a classe [CheckedArduino] [1] herda da classe [Arduino], à qual foi adicionado um valor booleano para indicar se o Arduino foi ou não selecionado numa lista;
A classe [Arduino] é a que já é utilizada pelo servidor e apresentada no parágrafo 5.4.2. É a seguinte:
package android.arduinos.entities;
import java.io.Serializable;
public class Arduino implements Serializable {
// dados
private String id;
private String description;
private String mac;
private String ip;
private int port;
// getters e 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 aguarda comandos;
Esta classe corresponde à cadeia jSON recebida do servidor quando lhe é solicitada a lista dos Arduinos ligados:
![]() |
A classe [CheckedArduino] herda da classe [Arduino]:
package android.arduinos.entities;
public class CheckedArduino extends Arduino {
private static final long serialVersionUID = 1L;
// é possível selecionar um Arduino
private boolean isChecked;
// construtor
public CheckedArduino(Arduino arduino, boolean isChecked) {
// pai
super(arduino.getId(), arduino.getDescription(), arduino.getMac(), arduino.getIp(), arduino.getPort());
// local
this.isChecked = isChecked;
}
// getters e 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: adiciona-se-lhe um valor booleano que nos servirá para saber se, na lista de Arduinos apresentada, foi ou não selecionado um Arduino;
Na classe [ConfigFragment], vamos simular a obtenção da lista de Arduinos ligados.
![]() |
@ViewById(R.id.ListViewArduinos)
protected ListView listArduinos;
..
@Click(R.id.btn_Rafraichir)
protected void doRafraichir() {
// verifica-se os dados introduzidos
if (!pageValid()) {
return;
}
// vamos iniciar uma tarefa - preparamos a espera
beginWaiting(1);
// limpa-se a lista de Arduinos
clearArduinos();
// solicita-se a lista de Arduinos em segundo plano
getArduinosInBackground();
}
private void getArduinosInBackground() {
...
}
// zerar a lista de Arduinos
private void clearArduinos() {
// criamos uma lista vazia
List<String> strings = new ArrayList<>();
// exibe-se a lista
listArduinos.setAdapter(new ArrayAdapter<String>(activity, android.R.layout.simple_list_item_1, android.R.id.text1, strings));
}
- linha 2: o ListView que apresenta os Arduinos ligados ao servidor;
- linha 5: o método que solicita a lista dos Arduinos ligados;
- linha 11: indica-se à classe pai que se vai iniciar uma tarefa assíncrona;
- linha 12: apaga-se a lista de Arduinos atualmente apresentada;
- linha 15: solicita-se, em segundo plano, a lista dos Arduinos ligados;
- linhas 23-28: o método que limpa a lista de Arduinos atualmente apresentada;
O método [getArduinosInBackground] é o seguinte:
private void getArduinosInBackground() {
// cria-se uma lista fictícia de Arduinos
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));
}
// simula-se uma resposta do servidor
Response<List<Arduino>> response = new Response<>();
response.setBody(arduinos);
// cancela-se a espera
cancelWaitingTasks();
// alteramos os botões
initButtons();
// processa-se a resposta
consumeArduinosResponse(response);
}
- linhas 3-6: cria-se uma lista de 20 Arduinos;
- linhas 8-9: constrói-se a resposta do tipo [Response<List<Arduino>>] (parágrafo 5.4.2) que irá encapsular a lista de Arduinos criada;
- linha 11: cancela-se a espera;
- linha 13: repõe-se os botões no seu estado inicial;
- linha 15: processa-se a resposta;
O método [consumeArduinosResponse] é o seguinte:
// exibição da resposta
private void consumeArduinosResponse(Response<List<Arduino>> response) {
// erro?
if (response.getStatus() != 0) {
// exibição
showAlert(response.getMessages());
// regresso à interface do utilizador
return;
}
// criamos uma lista de [CheckedArduino]
List<CheckedArduino> checkedArduinos = new ArrayList<>();
for (Arduino arduino : response.getBody()) {
checkedArduinos.add(new CheckedArduino(arduino, false));
}
// exibem-se
showArduinos(checkedArduinos);
}
- linhas 4-11: verifica-se o código de erro da resposta enviada pelo servidor:
- linha 4: se o código de erro for diferente de zero;
- linha 6: exibe-se as mensagens armazenadas pelo servidor no campo [messages] da resposta;
- linha 8: regressa-se à interface do utilizador;
- linhas 11-16: se não tiverem ocorrido erros, exibe-se a lista de Arduinos recebida, após a ter convertido num tipo List<CheckedArduino>;
O método [showArduinos] é o seguinte:
private void showArduinos(List<CheckedArduino> checkedArduinos) {
// cria-se uma lista de strings a partir da lista de Arduinos
List<String> strings = new ArrayList<>();
for (CheckedArduino checkedArduino : checkedArduinos) {
strings.add(checkedArduino.toString());
}
// exibe-se
listArduinos.setAdapter(new ArrayAdapter<>(activity, android.R.layout.simple_list_item_1, android.R.id.text1, strings));
}
Tarefa: efetue as alterações anteriores e execute o seu projeto.
Deve obter a seguinte visualização ao clicar no botão [Rafraîchir]:

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

Agora, pretendemos 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 se pretender apresentar uma lista de Arduinos não selecionáveis;
- em [2], o identificador do Arduino;
- em [3], a sua descrição;
O que se segue retoma conceitos desenvolvidos nos projetos [exemple-19] e [exemple-19B] do parágrafo 1.20. Reveja-os, se necessário.
Começamos por criar a vista que irá apresentar um elemento da lista de Arduinos:
![]() |
O código da 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 [Description : ];
- linhas 45-53: a descrição do Arduino será inserida aqui;
Esta vista utiliza textos (linhas 23, 32, 43) definidos 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 utiliza também 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 de um elemento da lista de Arduinos
![]() |
A classe [ListArduinosAdapter] é a classe chamada pela [ListView] para apresentar cada um dos elementos da lista de Arduinos. O seu código é o seguinte:
package istia.st.android.vues;
import istia.st.android.R;
...
public class ListArduinosAdapter extends ArrayAdapter<CheckedArduino> {
// a tabela dos Arduinos
private List<CheckedArduino> arduinos;
// o contexto de execução
private Context context;
// o ID do layout de visualização de uma linha da lista de Arduinos
private int layoutResourceId;
// a linha contém ou não uma caixa de seleção
private Boolean selectable;
// construtor
public ListArduinosAdapter(Context context, int layoutResourceId, List<CheckedArduino> arduinos, Boolean selectable) {
// pai
super(context, layoutResourceId, arduinos);
// as informações são guardadas
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 aceita quatro parâmetros: a atividade em execução, o identificador da vista a apresentar para cada elemento da fonte de dados, a fonte de dados que alimenta a lista e um valor booleano que indica se a caixa de seleção associada a cada Arduino deve ser apresentada 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 n.º [position] no [ListView] e por gerir os seus eventos. O seu código é o seguinte:
@Override
public View getView(int position, View convertView, ViewGroup parent) {
// o Arduino atual
final CheckedArduino arduino = arduinos.get(position);
// cria-se a linha atual
View row = ((Activity) context).getLayoutInflater().inflate(layoutResourceId, parent, false);
// recuperam-se as referências nos [TextView]
TextView txtArduinoId = (TextView) row.findViewById(R.id.txt_arduino_id);
TextView txtArduinoDesc = (TextView) row.findViewById(R.id.txt_arduino_description);
// preenche-se a linha
txtArduinoId.setText(arduino.getId());
txtArduinoDesc.setText(arduino.getDescription());
// o CheckBox nem sempre está visível
CheckBox ck = (CheckBox) row.findViewById(R.id.checkBoxArduino);
ck.setVisibility(selectable ? View.VISIBLE : View.INVISIBLE);
if (selectable) {
// atribui-se-lhe o seu valor
ck.setChecked(arduino.isChecked());
// gestiona-se o clique
ck.setOnCheckedChangeListener(new OnCheckedChangeListener() {
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
arduino.setChecked(isChecked);
}
});
}
// a linha é apresentada
return row;
}
- linha 2: o primeiro parâmetro é a posição no [ListView] da linha a criar. É também a posição na lista de Arduinos armazenada localmente;
- linha 4: obtém-se uma referência ao Arduino que será associado à linha criada;
- linha 6: a linha atual é criada a partir da vista [listarduinos_item.xml];
- linhas 8-9: são obtidas as referências aos dois [TextView];
- linhas 11-12: os dois [TextView] recebem o seu valor;
- linha 14: recupera-se uma referência à caixa de seleção;
- linha 15: torna-se visível ou não, consoante o valor [selectable] inicialmente passado ao construtor;
- linha 16: se a caixa de seleção estiver presente;
- linha 18: atribui-se-lhe o valor [isChecked] do Arduino atual;
- linhas 20-26: trata-se do clique na caixa de seleção;
- linha 23: o valor da caixa de seleção é guardado no Arduino atual;
Gestão da lista de Arduinos
A exibição da lista de Arduinos é, de momento, gerida por dois métodos da classe [ConfigFragment]:
- [clearArduinos]: que apresenta uma lista vazia;
- [showArduinos]: que apresenta a lista devolvida pelo servidor;
Estes dois métodos evoluem da seguinte forma:
// a lista de Arduinos é zerada
private void clearArduinos() {
// exibe-se uma lista vazia
ListArduinosAdapter adapter = new ListArduinosAdapter(getActivity(), R.layout.listarduinos_item, new ArrayList<CheckedArduino>(), false);
listArduinos.setAdapter(adapter);
}
// exibição da lista de Arduinos
private void showArduinos(List<CheckedArduino> checkedArduinos) {
// exibe os 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 é o local onde colocamos as informações partilhadas pelos fragmentos e pela atividade. Todos os fragmentos precisam de apresentar a lista dos Arduinos ligados. Assim, uma primeira versão da sessão será a seguinte:
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 {
// dados a partilhar entre os próprios fragmentos e entre fragmentos e a atividade
// os elementos que não podem ser serializados em jSON devem ter a anotação @JsonIgnore
// não se esqueça dos getters e setters necessários para a serialização/deserialização em jSON
// a lista de Arduinos
private List<CheckedArduino> checkedArduinos = new ArrayList<>();
// getters e setters
...
}
Tarefa: crie a classe [Session] acima referida.
A criação desta sessão leva-nos a alterar o código já escrito da seguinte forma:
// exibição da resposta
private void consumeArduinosResponse(Response<List<Arduino>> response) {
// erro?
if (response.getStatus() != 0) {
// exibição
showAlert(response.getMessages());
// anulação
doAnnuler();
// regresso à interface do utilizador
return;
}
// cria-se uma lista de [CheckedArduino]
List<CheckedArduino> checkedArduinos = new ArrayList<>();
for (Arduino arduino : response.getBody()) {
checkedArduinos.add(new CheckedArduino(arduino, false));
}
// coloca-se a lista na sessão
session.setCheckedArduinos(checkedArduinos);
// exibem-se
showArduinos(checkedArduinos);
// cancela-se a espera
cancelWaitingTasks();
}
- linha 18: a lista de Arduinos criada pelas linhas anteriores é inserida na sessão;
5.6.10.6. Gestão do estado do fragmento
Quando o dispositivo é rodado, os componentes visuais da vista são apresentados (por predefinição) no estado em que se encontravam aquando da conceção da vista:
- o [ListView] contém os elementos que o designer nele colocou;
- a mensagem de erro encontra-se no estado visível ou não visível em que o criador a colocou;
Os estados dos componentes visuais no momento da conceção podem ou não ser adequados aquando da restauração de um fragmento. O que acontece neste caso?
- o [ListView] deve apresentar a lista dos Arduinos ligados. O valor do [ListView] no momento da conceção não pode, portanto, ser utilizado;
- o [TextView] da mensagem de erro deve ser restaurado no estado (visível ou não) em que se encontrava no momento do salvamento. O seu valor na fase de conceção pode não ser adequado para estes dois casos;
Por isso, temos de guardar o estado destes dois componentes ao guardar o estado do fragmento:
- a lista dos Arduinos ligados;
- a visibilidade (exibida/oculta) da mensagem de erro ao introduzir o URL do serviço web / jSON;
Como a lista de Arduinos está presente na sessão, será guardada automaticamente. A visibilidade da mensagem de erro será memorizada na seguinte classe [ConfigFragmentState]:
![]() |
package client.android.fragments.state;
import client.android.architecture.custom.CoreState;
public class ConfigFragmentState extends CoreState {
// visibilidade da mensagem de erro
private boolean txtMsgErreurUrlServiceRestVisible;
// getters e setters
...
}
Tarefa: crie a classe [ConfigFragmentState] anterior.
Para reproduzir corretamente os estados dos fragmentos, é necessário que os seus métodos [getNumView] e [saveFragment] sejam alterados. Por exemplo, o do fragmento [BlinkFragment] é atualmente o seguinte:
@Override
public CoreState saveFragment() {
// é necessário guardar o fragmento
DummyFragmentState state=new DummyFragmentState();
// ...
return state;
// senão houver nada para guardar, execute [return new CoreState();] e elimine a classe [DummyFragmentState]
}
@Override
protected int getNumView() {
// é necessário devolver o n.º do fragmento na tabela de fragmentos geridos pela atividade (ver MainActivity)
return 0;
}
Se não for tomada nenhuma medida, o estado gerado na linha 6 será guardado no elemento 0 (linha 13) da tabela CoreState[] coreStates da classe [AbstractSession] (linha 5 abaixo):
public class AbstractSession implements ISession {
...
// estado das visualizações
private CoreState[] coreStates = new CoreState[0];
...
No entanto, deve ser guardado no elemento correspondente ao n.º do fragmento [BlinkFragment] na tabela 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 números dos fragmentos foram definidos na interface [IMainActivity]:
public interface IMainActivity extends IDao {
...
// números das vistas
int VUE_CONFIG = 0;
int VUE_BLINK = 1;
int VUE_PINREAD = 2;
int VUE_PINWRITE = 3;
int VUE_COMMANDS = 4;
}
Por fim, o estado do fragmento [BlinkFragment] será gerido corretamente se escrevermos:
@Override
public CoreState saveFragment() {
// é necessário guardar o fragmento
DummyFragmentState state=new DummyFragmentState();
// ...
return state;
// senão houver nada para guardar, execute [return new CoreState();] e elimine a classe [DummyFragmentState]
}
@Override
protected int getNumView() {
// é necessário devolver o n.º do fragmento na tabela de fragmentos geridos pela atividade (ver MainActivity)
return IMainActivity.VUE_BLINK;
}
- linha 14: devolve-se o número do fragmento [BlinkFragment] na tabela de fragmentos geridos pela atividade;
Além disso, a classe [CoreState], que é a classe-pai dos estados dos fragmentos, é, neste momento, a seguinte (ver parágrafo 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 {
// fragmento visitado ou não
protected boolean hasBeenVisited = false;
// estado do eventual menu do fragmento
protected MenuItemState[] menuOptionsState;
// getters e setters
....
}
- linhas 12-16: a classe [DummyFragmentState] não consta da lista de classes filhas da classe [CoreState]. No entanto, o método [saveFragment] da classe [BlinkFragment] devolve atualmente um tipo [ DummyFragmentState]. Se deixarmos as coisas como estão, a serialização/deserialização da sessão irá falhar e a sessão não será restaurada, o que levará a uma falha da aplicação;
O método [saveFragment] do fragmento [BlinkFragment] deve ser reescrito da seguinte forma:
@Override
public CoreState saveFragment() {
// é necessário guardar o fragmento
BlinkFragmentState state=new BlinkFragmentState();
// ...
return state;
// senão houver nada para guardar, execute [return new CoreState();] e elimine a classe [DummyFragmentState]
}
Tarefa: em cada um dos fragmentos, altere o método [getNumView] para que este devolva o n.º do fragmento e o método [saveFragment] para que este devolva uma instância da classe de estado do fragmento (como acima).
5.6.10.7. Gestão do ciclo de vida do fragmento
Estamos aqui a analisar o ciclo de vida do fragmento [ConfigFragment], nomeadamente os quatro métodos:
- [saveFragment]: deve guardar o estado do fragmento para que este possa ser recuperado posteriormente;
- [initFragment]: que deve inicializar determinados campos do fragmento, se necessário. Este método é chamado no arranque da aplicação e sempre que ocorre uma rotação do dispositivo. 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 o [initFragment] for chamado e quando a vista tiver de ser regenerada porque o fragmento, num determinado momento, saiu da adjacência 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 ocorre uma rotação do dispositivo, mas também quando há navegação. A sua função é restabelecer o estado anterior do fragmento;
Estes métodos serão os seguintes:
// adaptador da lista de Arduinos
private ListArduinosAdapter adapterListArduinos;
...
// gestão do ciclo de vida do fragmento -------------------------------------
@Override
public CoreState saveFragment() {
ConfigFragmentState state = new ConfigFragmentState();
state.setTxtMsgErreurUrlServiceRestVisible(txtMsgErreurUrlServiceRest.getVisibility() == View.VISIBLE);
return state;
}
@Override
protected void initFragment(CoreState previousState) {
// adaptador listArduinos
adapterListArduinos = new ListArduinosAdapter(activity, R.layout.listarduinos_item, session.getCheckedArduinos(), false);
}
@Override
protected void initView(CoreState previousState) {
// ligação entre a listview e o adaptador
listArduinos.setAdapter(adapterListArduinos);
// Primeira visita?
if (previousState == null) {
// ListView vazio - criado por [initFragment]
// mensagem de erro oculta
txtMsgErreurUrlServiceRest.setVisibility(View.INVISIBLE);
} else {
// a mensagem de erro volta a ficar visível
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() {
// botões
initButtons();
}
- linha 2: o adaptador do ListView dos Arduinos. É uma variável global porque é utilizada em diferentes métodos;
- linhas 7-12: o método [saveFragment] guarda, num tipo [ConfigFragmentState], a visibilidade do TextView txtMsgErreurUrlServiceRestVisible (linha 10);
- linhas 14-19: o método [initFragment] inicializa o adaptador da linha 2 com a lista de Arduinos presentes na sessão (linha 17). Recorde-se que a função do [initFragment] é inicializar os campos do fragmento. Aqui, esta inicialização deve ser efetuada em todos os casos, quer se trate da primeira visita (previousState==null) ou não;
- linha 17: verifica-se que o adaptador está ligado à 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:
// lista de Arduinos
private List<CheckedArduino> checkedArduinos = new ArrayList<>();
- linhas 21-35: o método [initView] tem como função inicializar determinados componentes da interface visual, nomeadamente aqueles cujo valor não é mantido durante a rotação do dispositivo;
- linha 24: o ListView dos Arduinos está associado ao adaptador da linha 2;
- linhas 28-32: distingue-se a primeira visita das restantes;
- linha 29: na primeira visita, deve ser apresentado um [ListView] vazio. É o que acontece, uma vez que, na primeira visita, o adaptador do [ListView] foi associado a uma lista vazia (linha 17);
- linha 31: a mensagem de erro está oculta;
- linhas 32-36: o caso em que não se trata da primeira visita;
- o [ListView] já se encontra no estado correto desde a linha 24. Não há mais nada a fazer;
- linhas 34-35: restaura-se a mensagem de erro ao estado em que se encontrava na última gravação do fragmento;
- linhas 31-36: o método [updateOnRestore] deve repor o fragmento no seu estado inicial. Chega-se ao método [updateOnRestore] de duas formas:
- ou porque houve uma rotação do dispositivo. Neste caso, todas as inicializações necessárias foram realizadas no método [initView];
- ou porque se navega de um separador para o separador [Config]. Se o fragmento [Config] saiu da vizinhança dos fragmentos exibidos desde que foi abandonado, o método [initView] foi então executado e o fragmento já se encontra no estado desejado. Se o fragmento [Config] não tiver saído da vizinhança dos fragmentos exibidos desde que foi abandonado, os seus componentes visuais não alteraram de estado e não há nada a fazer;
Vê-se que o método [updateOnRestore] não tem nada a fazer. Por vezes é esse o caso, outras vezes não. A diferença reside no método [updateOnSubmit]: se este método realizar alguma ação que torne desnecessárias certas inicializações efetuadas em [initView], então essas inicializações deveriam ser efetuadas no método [updateOnRestore]. Tomemos o exemplo de um botão de opção com três valores: V1, V2, V3. Talvez, no caso de uma 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, pois, no caso de um [SUBMIT], esse valor será substituído pelo valor fornecido pelo método [updateOnSubmit]. É, portanto, preferível deslocar essa restauração para o método [updateOnRestore], para evitar realizar, por vezes, 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 colocados no seu estado inicial: o botão [Rafraîchir] é exibido, o botão [Annuler] é ocultado:
Tarefa: adicione o código anterior ao [ConfigFragment] e, em seguida, execute a aplicação. Verifique que, ao rodar o dispositivo, o separador [Config] mantém o seu estado (mensagem de erro, lista de Arduinos). Verifique se o mesmo acontece quando efetuar uma simples navegação da guia [config] --> guia [Commands] --> guia [Config]. Neste último caso, se tiver mantido no [IMainActivity] uma adjacência de fragmentos igual a 1, então a visualização do fragmento [ConfigFragment] é destruída ao passar para o separador [Commands] e, em seguida, recriada ao regressar ao separador [Config]. Durante os testes, analise os registos.
5.6.10.8. Melhoria do código
O código do fragmento [ConfigFragment] pode ser melhorado. Por exemplo, escrevemos:
// adaptador da lista de Arduinos
private ListArduinosAdapter adapterListArduinos;
...
// exibição da lista de Arduinos
private void showArduinos(List<CheckedArduino> checkedArduinos) {
// exibição dos Arduinos
ListArduinosAdapter adapter = new ListArduinosAdapter(getActivity(), R.layout.listarduinos_item, checkedArduinos, false);
listArduinos.setAdapter(adapter);
}
// limpar a lista de Arduinos
private void clearArduinos() {
// exibe uma lista vazia
ListArduinosAdapter adapter = new ListArduinosAdapter(getActivity(), R.layout.listarduinos_item, new ArrayList<CheckedArduino>(), false);
listArduinos.setAdapter(adapter);
}
- vemos que, nas linhas 9 e 16, é utilizada uma variável local desligada do campo da linha 2, quando na verdade é a mesma entidade que pretendemos manipular;
Modificamos o código da seguinte forma:
// adaptador da lista de Arduinos
private ListArduinosAdapter adapterListArduinos;
@Click(R.id.btn_Rafraichir)
protected void doRafraichir() {
...
}
private void getArduinosInBackground() {
...
// a lista é consumida
consumeArduinosResponse(response);
}
// exibição da resposta
private void consumeArduinosResponse(Response<List<Arduino>> response) {
// erro?
if (response.getStatus() != 0) {
// exibição
showAlert(response.getMessages());
// cancelar
doAnnuler();
// regresso à interface do utilizador
return;
}
// cria-se uma lista de [CheckedArduino]
List<CheckedArduino> checkedArduinos = session.getCheckedArduinos();
checkedArduinos.clear();
for (Arduino arduino : response.getBody()) {
checkedArduinos.add(new CheckedArduino(arduino, false));
}
// exibem-se
adapterListArduinos.notifyDataSetChanged();
// cancela-se a espera
cancelWaitingTasks();
}
@Override
protected void initFragment(CoreState previousState) {
// adaptador listArduinos
adapterListArduinos = new ListArduinosAdapter(activity, R.layout.listarduinos_item, session.getCheckedArduinos(), false);
}
@Override
protected void initView(CoreState previousState) {
// ligação entre a lista de visualização e o adaptador
listArduinos.setAdapter(adapterListArduinos);
...
}
- quando o método da linha 5 é executado, o ciclo de vida do fragmento já foi executado. Portanto:
- o adaptador da linha 2 foi associado à sua fonte de dados (linha 41);
- o [ListView] dos Arduinos ligados foi ligado a este adaptador (linha 48);
Quando queremos alterar a exibição do [ListView], é necessário fazer duas coisas:
- alterar o conteúdo da fonte de dados [session.checkedArduinos];
- comunicar essa alteração ao adaptador através da instrução [adapterListArduinos.notifyDataSetChanged()];
Trata-se, de facto, de 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 apresentar a fonte de dados anterior. Seria então necessário associar o adaptador à nova fonte de dados.
O código é o seguinte:
- linha 27: recuperamos a fonte de dados;
- linha 28: esvaziamo-la. Por este motivo, eliminámos o método [clearArduinos];
- linhas 29-31: nesta lista agora vazia, adicionamos novos elementos;
- linha 33: diz-se ao adaptador para atualizar. Isto irá atualizar a visualizaçã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 vistas, vamos fazer com que todas as outras vistas apresentem a lista de Arduinos obtida pela vista [Config]. Comecemos pela vista [blink.xml]. Enquanto antes não apresentava nada, passará agora a apresentar a lista de Arduinos ligados:

![]() |
O código XML da 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 vista [config.xml]. Apenas se alterou a margem superior da 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 sofre alterações:
![]() |
// componentes visuais
@ViewById(R.id.ListViewArduinos)
protected ListView listArduinos;
// adaptador da lista de Arduinos
private ListArduinosAdapter adapterListArduinos;
...
// métodos impostos pela classe pai -------------------------------------------------------
...
@Override
protected void initFragment(CoreState previousState) {
// adaptador listArduinos
adapterListArduinos = new ListArduinosAdapter(activity, R.layout.listarduinos_item, session.getCheckedArduinos(), true);
}
@Override
protected void initView(CoreState previousState) {
// ligação entre a listview e o adaptador
listArduinos.setAdapter(adapterListArduinos);
}
...
- linhas 2-3: o componente [ListView] dos Arduinos ligados;
- linha 6: o adaptador deste [ListView];
- linhas 12-23: o código dos métodos [initFragment] e [initView] é o mesmo já utilizado para o fragmento [ConfigFragment];
- linha 15: quando o fragmento tem de ser reinicializado, reinicializa-se o adaptador da linha 2, associando-o à lista de Arduinos memorizada na sessão. O último parâmetro [true] do construtor [ListArduinosAdapter] significa que se pretende que apareça uma caixa de seleção ao lado de cada Arduino;
- linha 22: quando a visualização do fragmento tiver de ser reiniciada, associa-se 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 agora que cada separador apresenta a lista dos Arduinos ligados. Verifique também que, se marcar os Arduinos num separador e navegar para outro separador, os encontrará marcados neste último.
Nota: A explicação para a manutenção dos Arduinos marcados é a seguinte. A classe [ListArduinosAdapter] foi apresentada no parágrafo 5.6.10.4. O código relacionado com a caixa de seleção é o seguinte:
// o Arduino atual
final CheckedArduino arduino = arduinos.get(position);
...
// o CheckBox nem sempre está visível
CheckBox ck = (CheckBox) row.findViewById(R.id.checkBoxArduino);
ck.setVisibility(selectable ? View.VISIBLE : View.INVISIBLE);
if (selectable) {
// atribui-se-lhe o seu valor
ck.setChecked(arduino.isChecked());
// gestiona-se o clique
ck.setOnCheckedChangeListener(new OnCheckedChangeListener() {
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
arduino.setChecked(isChecked);
}
});
}
- linhas 11-15: se, no separador X, se marcar uma caixa de seleção, a propriedade [checked] do Arduino da linha 2 é alterada para true (linha 14);
- ao passar para o separador Y, é exibido o valor [ListView] dos Arduinos desse separador. Na linha 9, verifica-se que, se a propriedade [checked] do Arduino da linha 2 for alterada para true, então a caixa [ck] da linha 5 será marcada;
5.6.12. A camada [DAO]
![]() |
Nota: para esta parte, reveja a implementação da camada [DAO] no projeto [exemple-16B] (ver parágrafo 2.8.3).
Até ao momento, gerámos manualmente a lista dos 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 {
// URL do serviço web
void setUrlServiceWebJson(String url);
// utilizador
void setUser(String user, String mdp);
// tempo limite do cliente
void setTimeout(int timeout);
// autenticação básica
void setBasicAuthentification(boolean isBasicAuthentificationNeeded);
// modo de depuração
void setDebugMode(boolean isDebugEnabled);
// tempo de espera do cliente, em milissegundos, antes da solicitação
void setDelay(int delay);
// específico ----------------------------------------
// lista de 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] permite obter a lista dos Arduinos ligados sob a forma de um observável do tipo Observable<[Response<List<Arduino>>>];
Recorde-se 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> {
// ----------------- propriedades
// estado da operação
private int status;
// eventuais mensagens de erro
private List<String> messages;
// corpo da resposta
private T body;
// construtores
public Response() {
}
public Response(int status, List<String> messages, T body) {
this.status = status;
this.messages = messages;
this.body = body;
}
// getters e setters
...
}
5.6.12.2. A interface [WebClient]
![]() |
A interface [WebClient] é uma interface cuja implementação é fornecida pela biblioteca AA. 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);
// específico --------------------------------------
// lista de Arduinos
@Get("/arduinos")
Response<List<Arduino>> getArduinos();
}
- linhas 15-19: estas linhas estão presentes por defeito na interface [WebClient] do projeto modelo [client-android-skel];
- linha 23: o URL do servidor que permite obter a lista de Arduinos através de uma operação GET. Recorde-se que este URL é medido em relação ao URL raiz [RestClientRootUrl] da linha 16;
- linha 24: o servidor devolve a cadeia jSON de um tipo [Response<List<Arduino>>]. Esta cadeia jSON é automaticamente deserializada para o tipo [Response<List<Arduino>>] graças ao 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 {
// cliente do serviço web
@RestService
protected WebClient webClient;
// segurança
@Bean
protected MyAuthInterceptor authInterceptor;
// o RestTemplate
private RestTemplate restTemplate;
// fábrica do RestTemplate
private SimpleClientHttpRequestFactory factory;
@AfterInject
public void afterInject() {
// registo
Log.d(className, "afterInject");
// constrói-se o restTemplate
factory = new SimpleClientHttpRequestFactory();
restTemplate = new RestTemplate(factory);
// configura-se o conversor jSON
restTemplate.getMessageConverters().add(new MappingJackson2HttpMessageConverter());
// define-se o restTemplate do cliente web
webClient.setRestTemplate(restTemplate);
}
@Override
public void setUrlServiceWebJson(String url) {
// define-se o URL do serviço web
webClient.setRootUrl(url);
}
@Override
public void setUser(String user, String mdp) {
// regista-se o utilizador no 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));
}
// configuração de fábrica
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));
}
// interceptor de autenticação?
if (isBasicAuthentificationNeeded) {
// adiciona-se o interceptor de autenticação
List<ClientHttpRequestInterceptor> interceptors = new ArrayList<ClientHttpRequestInterceptor>();
interceptors.add(authInterceptor);
restTemplate.setInterceptors(interceptors);
}
}
// métodos privados -------------------------------------------------
private void log(String message) {
if (isDebugEnabled) {
Log.d(className, message);
}
}
// implementação específica IDao -----------------------------------------------
@Override
public Observable<Response<List<Arduino>>> getArduinos() {
// execução no cliente web
return getResponse(new IRequest<Response<List<Arduino>>>() {
@Override
public Response<List<Arduino>> getResponse() {
return webClient.getArduinos();
}
});
}
}
- linhas 19-87: estas linhas são de base na classe [Dao] do projeto [client-android-skel];
- linhas 91-100: implementação do método [getArduinos];
- linha 94: é chamado o método [getResponse] da classe pai. 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 da linha 92, ou seja, neste caso, um tipo [Response<List<Arduino>>];
- linha 97: o método [IRequest.getResponse()] delega a tarefa ao método [webClient.getArduinos()] que já apresentámos. O [webClient], definido na linha 24, é instanciado pela biblioteca AA e é uma instância da interface [WebClient] que já apresentámos;
5.6.13. A atividade [MainActivity]
![]() |
Já apresentámos a atividade [MainActivity] no parágrafo 5.6.8. Esta atividade estende a classe [AbstractActivity] e, como tal, implementa a interface [IMainActivity], que, por sua vez, estende a interface [IDao]. Sempre que se adiciona um método à interface [IDao], é necessário implementá-lo na classe [MainActivity]. O método [IDao.getArduinos] adicionado à interface [IDao] será implementado da seguinte forma na classe [MainActivity]:
...
@EActivity
@OptionsMenu(R.menu.menu_main)
public class MainActivity extends AbstractActivity {
// camada [DAO]
@Bean(Dao.class)
protected IDao dao;
// sessão
private Session session;
...
// implementação IDao -----------------------------------------
@Override
public Observable<Response<List<Arduino>>> getArduinos() {
return dao.getArduinos();
}
}
- linhas 15-18: o método [getArduinos] é implementado delegando a tarefa à classe [Dao], que acabámos de apresentar e à qual há uma referência na linha 8;
5.6.14. O fragmento [ConfigFragment] revisto
Na classe [ConfigFragment], o código executado ao clicar no botão [Rafraîchir] é, por enquanto, o seguinte:
@Click(R.id.btn_Rafraichir)
protected void doRafraichir() {
...
// solicita-se a lista de Arduinos em segundo plano
getArduinosInBackground();
}
private void getArduinosInBackground() {
// cria-se uma lista fictícia de Arduinos
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));
}
// simula-se uma resposta do servidor
Response<List<Arduino>> response = new Response<>();
response.setBody(arduinos);
// esta é processada
consumeArduinosResponse(response);
}
// exibição da resposta
private void consumeArduinosResponse(Response<List<Arduino>> response) {
...
}
Temos de reescrever as linhas 10 a 16, que geravam de forma estática uma resposta do tipo [Response<List<Arduino>>]. Agora, temos de solicitar essa lista à camada [DAO] através da atividade. O código passa a ser o seguinte:
@Click(R.id.btn_Rafraichir)
protected void doRafraichir() {
// verifica-se os dados introduzidos
if (!pageValid()) {
return;
}
// armazenamos a entrada
mainActivity.setUrlServiceWebJson(urlServiceRest);
// prepara-se a espera
beginWaiting(1);
// executa-se a tarefa assíncrona
executeInBackground(mainActivity.getArduinos(), new Action1<Response<List<Arduino>>>() {
@Override
public void call(Response<List<Arduino>> response) {
// processa-se a resposta
consumeArduinosResponse(response);
}
});
}
- linha 8: o URL, raiz do serviço web / jSON introduzido pelo utilizador, é passado para a camada [DAO] através da atividade. Esta será a raiz URL da interface [WebClient] (ver parágrafo 5.6.12.2);
- linha 10: avisa-se a classe pai de que se vai iniciar uma tarefa assíncrona;
- linhas 12-19: lançamento da tarefa assíncrona que irá devolver a lista dos Arduinos ligados ao servidor;
- linha 12: chamada do método [executeInBackground] da classe pai. Este método espera dois parâmetros:
- linha 12: o processo a observar. Este processo é aqui fornecido pelo método [mainActivity.getArduinos()];
- linhas 12-19: uma instância da interface [Action1<T>], em que o tipo T é o tipo fornecido pelo processo, neste caso um tipo [Response<List<Arduino>>];
- linhas 14-18: o método chamado quando a tarefa assíncrona devolve o seu resultado do tipo [Response<List<Arduino>>];
- linha 17: a resposta recebida é passada para o método [consumeArduinosResponse] já definido;
Trabalho: Inicie o servidor conforme indicado no parágrafo 5.4. Ligue um ou mais Arduinos ao PC no qual o servidor foi iniciado. Em seguida, inicie o cliente Android e verifique se consegue obter a lista dos Arduinos ligados. Observe os registos.

- digite o endereço URL indicado em [1]. Este é um dos endereços IP do seu servidor;
- clique no botão [2];
- deverá obter a lista dos Arduinos ligados em [3];
Verifique se esta lista também aparece nos outros separadores.
5.7. Tarefa a realizar
Seguindo o procedimento que acabou de ser descrito para a vista [Config], crie e teste sucessivamente as outras quatro vistas da aplicação: [Blink], [PinRead], [PinWrite] e [Commands].
As vistas a criar foram apresentadas no parágrafo 5.5.
Para cada vista, é necessário:
- desenhar a vista XML (ver parágrafo 5.6.9);
- construir o fragmento associado (ver parágrafo 5.6.10);
- adicionar um método à interface [WebClient] (ver parágrafo 5.6.12.2);
- adicionar um método à interface [IDao] (ver parágrafo 5.6.12.2);
- adicionar um método à classe [Dao] (ver parágrafo 5.6.12.3);
- adicionar um método à atividade [MainActivity] (ver parágrafo 5.6.13);
- escrever os gestores de eventos do fragmento (ver parágrafo 5.6.14);
- testar e observar os registos;
Nota 1: o exemplo a seguir é o projeto [Exemple-16B] do curso (ver parágrafo 2.8.3).
Nota 2: os URL a interrogar e o tipo das respetivas respostas foram apresentados no parágrafo 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 {
// dados
private String id;
private String ac;
private Map<String, Object> pa;
// construtores
public ArduinoCommand() {
}
public ArduinoCommand(String id, String ac, Map<String, Object> pa) {
this.id = id;
this.ac = ac;
this.pa = pa;
}
// getters e setters
...
}
Na interface [WebClient], o método para executar esta lista de um comando será o seguinte:
// envio de comandos JSON
@Post("/arduinos/commands/{idArduino}")
Response<List<ArduinoResponse>> sendCommands(@Body List<ArduinoCommand> commands, @Path String idArduino);
- linha 2: o URL é solicitado com uma ordem HTTP POST;
- linha 3: o valor lançado deve ter a anotação [@Body];
Nota 4: recomenda-se realizar este trabalho da seguinte forma:
- só passar para a vista seguinte quando a vista atual tiver sido criada e testada;
- só gerir o estado das vistas depois de se ter obtido uma aplicação funcional em condições normais. Em seguida, para cada vista, execute o dispositivo para diferentes estados da vista e anote as informações perdidas. São essas que devem ser guardadas e, posteriormente, restauradas. Verifique, em seguida, a navegação: quando se sai de um separador e se regressa a ele posteriormente, deve encontrá-lo no estado em que o deixou;


















































