5. Aufgabe 2 – Steuerung von Arduinos mit einem Android-Tablet
Wir werden nun lernen, wie man ein Arduino-Board mit einem Tablet steuert. Das folgende Beispiel ist das Projekt [client-android-skel] aus dem Kurs (siehe Absatz 2).
5.1. Projektarchitektur
Das gesamte Projekt wird die folgende Architektur aufweisen:
![]() |
- Block [1], der Webserver/JSON und die Arduinos, werden Ihnen zur Verfügung gestellt;
- Sie müssen Block [2], das Android-Tablet-Programm zur Kommunikation mit dem Webserver/JSON, selbst erstellen.
5.2. Hardware
Folgende Komponenten stehen Ihnen zur Verfügung:
- ein Arduino mit einem Ethernet-Shield, einer LED und einem Temperatursensor;
- ein MiniHub zur gemeinsamen Nutzung mit einem anderen Teilnehmer;
- ein USB-Kabel zur Stromversorgung des Arduino;
- zwei Netzwerkkabel, um den Arduino und den PC mit demselben privaten Netzwerk zu verbinden;
- ein Android-Tablet;
5.2.1. Der Arduino
![]() |
So verbinden Sie die verschiedenen Komponenten miteinander:
- Trennen Sie das Netzwerkkabel von Ihrem PC;
- Verbinden Sie Ihren PC und den Arduino mit einem Netzwerkkabel;
- Ihr Arduino ist bereits vorprogrammiert. Seine IP-Adresse lautet [192.168.2.2]. Damit Ihr PC den Arduino erkennt, müssen Sie ihm eine IP-Adresse im Netzwerk [192.168.2] zuweisen. Die Arduinos sind so programmiert, dass sie mit einem PC mit der IP-Adresse [192.168.2.1] kommunizieren. So geht’s:
Gehen Sie zu [Systemsteuerung\Netzwerk und Internet\Netzwerk- und Freigabecenter]:
![]() |
- Klicken Sie unter [1] auf den Link [Lokales Netzwerk];
- klicken Sie unter [2] auf die Schaltfläche [Eigenschaften] für das lokale Netzwerk;
![]() | ![]() |
- Klicken Sie unter [3] auf die [IPv4]-Eigenschaften des Adapters [LAN-Verbindung];
- Weisen Sie diesem Adapter in [4] die IP-Adresse [192.168.2.1] und die Subnetzmaske [255.255.255.0] zu;
- Klicken Sie in [5] so oft wie nötig auf [OK], um den Assistenten zu beenden.
5.2.2. Das Tablet
- Verbinden Sie Ihren Computer über Ihren WLAN-Adapter mit dem von uns bereitgestellten WLAN-Netzwerk. Verfahren Sie ebenso mit Ihrem Tablet;
- Überprüfen Sie die WLAN-IP-Adresse Ihres PCs, indem Sie [ipconfig] in ein Eingabeaufforderungsfenster eingeben. Sie finden eine Adresse wie [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
- Überprüfen Sie die WLAN-IP-Adresse Ihres Tablets. Fragen Sie Ihren Kursleiter, wie das geht, wenn Sie sich nicht sicher sind. Sie finden eine Adresse wie [192.168.x.z];
- Deaktivieren Sie die Firewall Ihres PCs, falls diese aktiv ist [Systemsteuerung\System und Sicherheit\Windows-Firewall];
- Überprüfen Sie in einem Eingabeaufforderungsfenster, ob der PC und das Tablet miteinander kommunizieren können, indem Sie den Befehl [ping 192.168.x.z] eingeben, wobei [192.168.x.z] die IP-Adresse Ihres Tablets ist. Das Tablet sollte dann antworten:
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
Die Netzwerkkonfiguration Ihres Systems ist nun fertig.
5.2.3. Der [Genymotion]-Emulator
Der [Genymotion]-Emulator (siehe Abschnitt 6.9) ist eine hervorragende Alternative zu einem Tablet. Er ist fast genauso schnell und benötigt keine WLAN-Verbindung. Wir empfehlen diese Methode. Für die abschließende Testphase Ihrer App können Sie das Tablet verwenden.
5.3. Arduinos programmieren
Hier konzentrieren wir uns auf das Schreiben von C-Code für Arduinos:
![]() |
Siehe auch
- Installation der Arduino-Entwicklungs-IDE (siehe Abschnitt 6.1);
- Verwendung von JSON-Bibliotheken (Anhänge, Abschnitt 6.6);
- Testen Sie in der Arduino-IDE das Beispiel eines TCP-Servers (z. B. den Webserver) und das eines TCP-Clients (z. B. den Telnet-Client);
- die Anhänge zur Arduino-Programmierumgebung in Abschnitt 6.1.
![]() |
Ein Arduino besteht aus einer Reihe von Pins, die mit der Hardware verbunden sind. Diese Pins sind Ein- oder Ausgänge. Ihre Werte sind binär oder analog. Zur Steuerung des Arduino gibt es zwei grundlegende Operationen:
- Schreiben eines binären/analogen Werts an einen durch seine Nummer identifizierten Pin;
- Lesen eines binären/analogen Werts von einem durch seine Nummer identifizierten Pin;
Zu diesen beiden grundlegenden Operationen fügen wir eine dritte hinzu:
- eine LED für eine bestimmte Dauer und mit einer bestimmten Frequenz blinken lassen. Diese Operation kann durch wiederholtes Aufrufen der beiden vorherigen Grundoperationen durchgeführt werden. Wir werden jedoch in den Tests sehen, dass der Datenaustausch zwischen der [DAO]-Schicht und einem Arduino etwa eine Sekunde dauert. Es ist daher nicht möglich, eine LED beispielsweise alle 100 Millisekunden blinken zu lassen. Daher werden wir diese Blinkfunktion auf dem Arduino selbst implementieren.
Der Arduino funktioniert wie folgt:
- Die Kommunikation zwischen der [DAO]-Schicht und einem Arduino erfolgt über ein TCP-IP-Netzwerk durch den Austausch von Textzeilen im JSON-Format (JavaScript Object Notation);
- Beim Start verbindet sich der Arduino mit Port 100 eines Registrierungsservers, der sich in der [DAO]-Schicht befindet. Er sendet eine einzelne Textzeile an den Server:
Dies ist eine JSON-Zeichenkette, die den sich verbindenden Arduino beschreibt:
- id: eine Kennung für den Arduino;
- desc: eine Beschreibung dessen, was der Arduino kann. Hier haben wir lediglich das Arduino-Modell angegeben;
- mac: die MAC-Adresse des Arduinos;
- Port: Die Portnummer, auf der der Arduino auf Befehle von der [DAO]-Schicht wartet.
Alle diese Informationen liegen im String-Format vor, mit Ausnahme des Ports, der eine Ganzzahl ist.
- Sobald sich der Arduino beim Registrierungsserver angemeldet hat, beginnt er, auf dem Port zu lauschen, den er dem Server angegeben hat (oben 102). Er wartet auf JSON-Befehle im folgenden Format:
Dies ist eine JSON-Zeichenkette mit den folgenden Elementen:
- id: eine Kennung für den Befehl. Kann beliebig sein;
- ac: eine Aktion. Es gibt drei:
- pw (Pin schreiben), um einen Wert an einen Pin zu schreiben,
- pr (Pin lesen), um den Wert von einem Pin zu lesen,
- cl (blink) zum Blinken einer LED;
- pa: die Parameter der Aktion. Diese hängen von der Aktion ab.
- Das Arduino sendet immer eine Antwort an seinen Client zurück. Dabei handelt es sich um eine JSON-Zeichenkette im folgenden Format:
wobei
- id: die Kennung des Befehls, auf den geantwortet wird;
- er (Fehler): ein Fehlercode, falls ein Fehler aufgetreten ist, andernfalls 0;
- et (status): ein Wörterbuch, das immer leer ist, außer beim Lese-Befehl pr. Das Wörterbuch enthält dann den Wert der angeforderten Pin-Nummer x.
Hier sind einige Beispiele zur Verdeutlichung der vorstehenden Spezifikationen:
Lassen Sie LED Nr. 8 10 Mal mit einer Periode von 100 Millisekunden blinken:
Befehl | |
Antwort |
Die Parameter für den Befehl „cl“ sind: die Dauer (dur) eines Blinksignals in Millisekunden, die Anzahl (nb) der Blinksignale und die Pin-Nummer der LED.
Schreibe den Binärwert 1 an Pin 7:
Befehl | |
Antwort |
Die pa-Parameter des Befehls pw sind: der Schreibmodus mod (b für binär oder a für analog), der zu schreibende Wert val und die Pin-Nummer. Bei einem binären Schreibvorgang ist val 0 oder 1. Bei einem analogen Schreibvorgang liegt val im Bereich [0,255].
Schreibe den analogen Wert 120 auf Pin 2:
Befehl | |
Antwort |
Lies den Analogwert von Pin 0 aus:
Befehl | |
Antwort |
Die pa-Parameter des pr-Befehls sind: der Lesemodus (binär oder analog) und die Pin-Nummer. Wenn kein Fehler vorliegt, setzt das Arduino den Wert des angeforderten Pins in den „et“-Schlüssel seiner Antwort. Hier gibt pin0 an, dass der Wert von Pin 0 angefordert wurde, und 1023 ist dieser Wert. Im Lesemodus liegt ein analoger Wert im Bereich [0, 1024].
Wir haben die drei Befehle cl, pw und pr vorgestellt. Man könnte sich fragen, warum wir in den JSON-Zeichenfolgen keine expliziteren Felder verwendet haben – wie beispielsweise action statt ac, pinwrite statt pw und parameters statt pa. Ein Arduino verfügt über sehr begrenzten Speicher. Die mit dem Arduino ausgetauschten JSON-Zeichenfolgen tragen jedoch zur Speicherauslastung bei. Wir haben uns daher entschieden, sie so weit wie möglich zu verkürzen.
Betrachten wir nun einige Fehlerfälle:
Befehl | |
Antwort |
Es wurde ein Befehl gesendet, der nicht im JSON-Format vorliegt. Der Arduino hat den Fehlercode 100 zurückgegeben.
Befehl | |
Antwort |
Ein PR-Befehl wurde ohne den Parameter „pin“ gesendet. Der Arduino hat den Fehlercode 302 zurückgegeben.
Befehl | |
Antwort |
Wir haben einen unbekannten pinread-Befehl gesendet (es ist pr). Der Arduino hat den Fehlercode 104 zurückgegeben.
Wir werden die Beispiele nicht fortsetzen. Die Regel ist einfach: Der Arduino darf nicht abstürzen, unabhängig davon, welcher Befehl an ihn gesendet wird. Vor der Ausführung eines JSON-Befehls stellt er sicher, dass der Befehl gültig ist. Sobald ein Fehler auftritt, bricht der Arduino die Ausführung des Befehls ab und gibt die JSON-Fehlerzeichenfolge an seinen Client zurück. Da der Speicherplatz begrenzt ist, geben wir wiederum einen Fehlercode statt einer vollständigen Meldung zurück.
Der Code für das auf dem Arduino laufende Programm ist in den Beispielen in diesem Dokument enthalten:
![]() |
So überträgst du es auf den Arduino:
- Schließen Sie es an Ihren PC an;
![]() | ![]() |
- Öffnen Sie in [1] die Datei [arduino_uno.ino]. Die Arduino-IDE wird gestartet und lädt die Datei;
Hinweis: Der Code wurde ursprünglich mit der Arduino IDE 1.5.x erstellt und getestet. Seitdem sind weitere Versionen der IDE erschienen. Der Code funktionierte nicht mit der Arduino IDE 1.6.x. Es scheint ein Problem mit der Abwärtskompatibilität zwischen den Versionen 1.6 und 1.5 zu geben.
- Geben Sie in [2-4] den verwendeten Arduino-Typ an;
![]() | ![]() |
- Geben Sie in [5-7] an, an welchen seriellen Anschluss des PCs er angeschlossen ist;
- Laden Sie in [8] das Programm [arduino_uno] auf den Arduino hoch;
Der Programmcode ist ausführlich kommentiert. Interessierte Leser können ihn einsehen. Wir heben lediglich die Codezeilen hervor, die die bidirektionale Client-Server-Kommunikation zwischen dem Arduino und dem PC konfigurieren:
#include <SPI.h>
#include <Ethernet.h>
#include <ajSON.h>
// ---------------------------------- CONFIGURATION DE L'ARDUINO UNO
// adresse MAC de l'Arduino UNO
byte macArduino[] = {
0x90, 0xA2, 0xDA, 0x0D, 0xEE, 0xC7 };
char * strMacArduino="90:A2:DA:0D:EE:C7";
// l'adresse IP de l'Arduino
IPAddress ipArduino(192,168,2,2);
// son identifiant
char * idArduino="cuisine";
// port du serveur Arduino
int portArduino=102;
// description de l'Arduino
char * descriptionArduino="contrôle domotique";
// le serveur Arduino travaillera sur le port 102
EthernetServer server(portArduino);
// IP du serveur d'enregistrement
IPAddress ipServeurEnregistrement(192,168,2,1);
// port du serveur d'enregistrement
int portServeurEnregistrement=100;
// le client Arduino du serveur d'enregistrement
EthernetClient clientArduino;
// la commande du client
char commande[100];
// la réponse de l'Arduino
char message[100];
// initialisation
void setup() {
// Le moniteur série permettra de suivre les échanges
Serial.begin(9600);
// démarrage de la connection Ethernet
Ethernet.begin(macArduino,ipArduino);
// mémoire disponible
Serial.print(F("Memoire disponible : "));
Serial.println(freeRam());
}
// boucle infinie
void loop()
{
...
}
- Zeile 8: Die MAC-Adresse des Arduino. Das spielt hier keine große Rolle, da sich der Arduino in einem privaten Netzwerk mit einem PC und einem oder mehreren Arduinos befindet. Die MAC-Adresse muss lediglich innerhalb dieses privaten Netzwerks eindeutig sein. Normalerweise befindet sich auf der Netzwerkplatine des Arduinos ein Aufkleber mit der MAC-Adresse der Platine. Falls dieser Aufkleber fehlt und Sie die MAC-Adresse der Platine nicht kennen, können Sie in Zeile 8 einen beliebigen Wert eingeben, solange die Regel der Eindeutigkeit der MAC-Adresse im privaten Netzwerk eingehalten wird;
- Zeile 11: Die IP-Adresse der Karte. Auch hier können Sie einen beliebigen Wert im Format [192.168.2.x] eingeben und das x für die verschiedenen Arduinos im privaten Netzwerk variieren;
- Zeile 13: Arduino-Kennung. Muss unter den Kennungen der Arduinos im selben privaten Netzwerk eindeutig sein;
- Zeile 15: Der Service-Port des Arduinos. Sie können hier einen beliebigen Wert eingeben;
- Zeile 17: Beschreibung der Arduino-Funktion. Sie können einen beliebigen Wert eingeben. Seien Sie bei langen Zeichenfolgen vorsichtig, da der Speicher des Arduinos begrenzt ist;
- Zeile 21: IP-Adresse des Protokollierungsservers des Arduino auf dem PC. Darf nicht geändert werden;
- Zeile 23: Port für diesen Protokollierungsdienst. Darf nicht geändert werden;
5.4. Der Web-/JSON-Server
5.4.1. Installation

Die Java-Binärdatei für den Web/JSON-Server wird bereitgestellt:
![]() |
Öffnen Sie eine Eingabeaufforderung und geben Sie den folgenden Befehl ein:
Wenn sich [java.exe] nicht im PATH der Eingabeaufforderung befindet, müssen Sie den vollständigen Pfad zu [java.exe] eingeben (normalerweise C:\Program Files\java\...).
Es öffnet sich ein DOS-Fenster, in dem Protokolle angezeigt werden:
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v0.5.0.M6)
2014-01-06 11:11:35.550 INFO 8408 --- [ main] arduino.rest.metier.Application : Starting Application on Gportpers3 with PID 8408 (C:\Users\SergeTahÚ\Desktop\part2\server.jar started by ST)
2014-01-06 11:11:35.587 INFO 8408 --- [ main] ationConfigEmbeddedWebApplicationContext : Refreshing org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext@6a4ba620: startup date [Mon Jan 06 11:11:35 CET 2014]; root of context hierarchy
2014-01-06 11:11:36.765 INFO 8408 --- [ main] o.apache.catalina.core.StandardService : Starting service Tomcat
2014-01-06 11:11:36.766 INFO 8408 --- [ main] org.apache.catalina.core.StandardEngine : Starting Servlet Engine: Apache Tomcat/7.0.42
2014-01-06 11:11:36.876 INFO 8408 --- [ost-startStop-1] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring embedded WebApplicationContext
2014-01-06 11:11:36.877 INFO 8408 --- [ost-startStop-1] o.s.web.context.ContextLoader : Root WebApplicationContext: initialization completed in 1293 ms
2014-01-06 11:11:37.084 INFO 8408 --- [ost-startStop-1] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring FrameworkServlet 'dispatcherServlet'
2014-01-06 11:11:37.084 INFO 8408 --- [ost-startStop-1] o.s.web.servlet.DispatcherServlet : FrameworkServlet 'dispatcherServlet': initialization started
2014-01-06 11:11:37.184 INFO 8408 --- [ost-startStop-1] o.s.w.s.handler.SimpleUrlHandlerMapping : Mapped URL path [/**/favicon.ico] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2014-01-06 11:11:37.386 INFO 8408 --- [ost-startStop-1] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/arduinos/blink/{idCommande}/{idArduino}/{pin}/{duree}/{nombre}],methods=[GET],params=[],headers=[],consumes=[],produces=[],custom=[]}" onto public java.lang.String arduino.rest.metier.RestMetier.faireClignoterLed(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,javax.servlet.http.HttpServletResponse)
2014-01-06 11:11:37.388 INFO 8408 --- [ost-startStop-1] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/arduinos/commands/{idArduino}],methods=[POST],params=[],headers=[],consumes=[],produces=[],custom=[]}" onto public java.lang.String arduino.rest.metier.RestMetier.sendCommandesJson(java.lang.String,java.lang.String,javax.servlet.http.HttpServletResponse)
2014-01-06 11:11:37.388 INFO 8408 --- [ost-startStop-1] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/arduinos/],methods=[GET],params=[],headers=[],consumes=[],produces=[],custom=[]}" onto public java.lang.String arduino.rest.metier.RestMetier.getArduinos(javax.servlet.http.HttpServletResponse)
2014-01-06 11:11:37.389 INFO 8408 --- [ost-startStop-1] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/arduinos/pinRead/{idCommande}/{idArduino}/{pin}/{mode}],methods=[GET],params=[],headers=[],consumes=[],produces=[],custom=[]}" onto public java.lang.String arduino.rest.metier.RestMetier.pinRead(java.lang.String,java.lang.String,java.lang.String,java.lang.String,javax.servlet.http.HttpServletResponse)
2014-01-06 11:11:37.390 INFO 8408 --- [ost-startStop-1] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/arduinos/pinWrite/{idCommande}/{idArduino}/{pin}/{mode}/{valeur}],methods=[GET],params=[],headers=[],consumes=[],produces=[],custom=[]}" onto public java.lang.String arduino.rest.metier.RestMetier.pinWrite(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,javax.servlet.http.HttpServletResponse)
2014-01-06 11:11:37.463 INFO 8408 --- [ost-startStop-1] o.s.w.s.handler.SimpleUrlHandlerMapping : Mapped URL path [/**] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2014-01-06 11:11:37.464 INFO 8408 --- [ost-startStop-1] o.s.w.s.handler.SimpleUrlHandlerMapping : Mapped URL path [/webjars/**] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2014-01-06 11:11:37.881 INFO 8408 --- [ost-startStop-1] o.s.web.servlet.DispatcherServlet : FrameworkServlet 'dispatcherServlet': initialization completed in 796 ms
Serveur d'enregistrement lancÚ sur 192.168.2.1:100
2014-01-06 11:11:38.101 INFO 8408 --- [ Thread-4] arduino.dao.Recorder : Recorder : [11:11:38:101] : [Serveur d'enregistrement : attente d'un client]
2014-01-06 11:11:38.142 INFO 8408 --- [ main] arduino.rest.metier.Application : Started Application in 3.257 seconds
- Zeile 11: Ein eingebetteter Tomcat-Server wird gestartet;
- Zeile 15: Das Spring-MVC-Servlet [dispatcherServlet] wird geladen und ausgeführt;
- Zeile 18: Die REST-URL [/arduinos/blink/{commandId}/{ArduinoId}/{pin}/{duration}/{count}] wird erkannt;
- Zeile 19: Die REST-URL [/arduinos/commands/{idArduino}] wird erkannt;
- Zeile 20: Die REST-URL [/arduinos/] wird erkannt;
- Zeile 21: Die REST-URL [/arduinos/pinRead/{commandId}/{ArduinoId}/{pin}/{mode}] wird erkannt;
- Zeile 22: Die REST-URL [/arduinos/pinWrite/{commandId}/{ArduinoId}/{pin}/{mode}/{value}] wird erkannt;
- Zeile 26: Der Arduino-Logging-Server wird gestartet;
Schließen Sie Ihr Arduino an den PC an, falls Sie dies noch nicht getan haben. Die Firewall des PCs muss deaktiviert sein. Geben Sie dann in einem Webbrowser die URL [http://localhost:8080/arduinos] ein:
![]() |
Die ID des angeschlossenen Arduino sollte nun angezeigt werden. Wenn nichts angezeigt wird, versuchen Sie, den Arduino zurückzusetzen. Zu diesem Zweck verfügt er über einen Reset-Knopf.
Der Web-/JSON-Server ist nun installiert.
5.4.2. Die vom Web-/JSON-Dienst bereitgestellten URLs
Siehe: Projekt [Beispiel-15] (siehe Abschnitt 1.16.1);
Der Web-/JSON-Dienst wurde mit Spring MVC implementiert und stellt die folgenden URLs bereit:
@Controller
public class WebController {
// business layer
@Autowired
private IMetier métier;
// list of arduinos
@RequestMapping(value = "/arduinos", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
@ResponseBody
public String getArduinos() throws JsonProcessingException {
...
}
// flashing
@RequestMapping(value = "/arduinos/blink/{idCommande}/{idArduino}/{pin}/{duree}/{nombre}", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
@ResponseBody
public String faireClignoterLed(@PathVariable("idCommande") String idCommande, @PathVariable("idArduino") String idArduino, @PathVariable("pin") int pin, @PathVariable("duree") int duree, @PathVariable("nombre") int nombre) throws JsonProcessingException {
...
}
// order dispatch JSON
@RequestMapping(value = "/arduinos/commands/{idArduino}", method = RequestMethod.POST, produces = MediaType.APPLICATION_JSON_VALUE, consumes = MediaType.APPLICATION_JSON_VALUE)
@ResponseBody
public String sendCommandesJson(@PathVariable("idArduino") String idArduino, HttpServletRequest request) throws IOException {
...
}
// pin reading
@RequestMapping(value = "/arduinos/pinRead/{idCommande}/{idArduino}/{pin}/{mode}", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
@ResponseBody
public String pinRead(@PathVariable("idCommande") String idCommande, @PathVariable("idArduino") String idArduino, @PathVariable("pin") int pin, @PathVariable("mode") String mode) throws JsonProcessingException {
....
}
// writing pin
@RequestMapping(value = "/arduinos/pinWrite/{idCommande}/{idArduino}/{pin}/{mode}/{valeur}", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
@ResponseBody
public String pinWrite(@PathVariable("idCommande") String idCommande, @PathVariable("idArduino") String idArduino, @PathVariable("pin") int pin, @PathVariable("mode") String mode, @PathVariable("valeur") int valeur) throws JsonProcessingException {
...
}
}
Vom Server gesendete Antworten sind JSON-Darstellungen der folgenden Klasse [Response<T>]:
package client.android.dao.service;
import java.util.List;
public class Response<T> {
// ----------------- properties
// operation status
private int status;
// any status messages
private List<String> messages;
// the body of the reply
private T body;
// manufacturers
public Response() {
}
public Response(int status, List<String> messages, T body) {
this.status = status;
this.messages = messages;
this.body = body;
}
// getters and setters
...
}
Die URL [/arduinos] gibt eine Antwort vom Typ [Response<List<Arduino>>] zurück, wobei [Arduino] die folgende Klasse ist:
package android.arduinos.entities;
import java.io.Serializable;
public class Arduino implements Serializable {
// data
private String id;
private String description;
private String mac;
private String ip;
private int port;
// getters and setters
...
}
- Zeile 7: [id] ist die Kennung des Arduino;
- Zeile 8: seine Beschreibung;
- Zeile 9: seine MAC-Adresse;
- Zeile 10: seine IP-Adresse;
- Zeile 11: der Port, auf dem er auf Befehle wartet;
Die URLs:
- [/arduinos/blink/{commandId}/{ArduinoId}/{pin}/{duration}/{count}];
- [/arduinos/pinRead/{commandId}/{ArduinoId}/{pin}/{mode}] ;
- [/arduinos/pinWrite/{commandId}/{ArduinoId}/{pin}/{mode}/{value}] ;
- [/arduinos/commands/{ArduinoID}];
gibt eine Antwort vom Typ [Response<ArduinoResponse>] zurück, wobei die Klasse [ArduinoResponse] die Standard-Arduino-Antwort darstellt:
public class ArduinoResponse implements Serializable {
private String json;
private String id;
private String erreur;
private Map<String, Object> etat;
// getters and setters
...
}
- [json]: die von einem Arduino gesendete JSON-Zeichenkette, die nicht dekodiert werden konnte (im Falle eines Fehlers), andernfalls null;
- [id]: die Kennung des Befehls, auf den der Arduino antwortet;
- [error]: ein Fehlercode, 0 bei OK, andernfalls ein anderer Wert;
- [status]: Ein Wörterbuch, das die für den Befehl spezifische Antwort enthält. Es ist in der Regel leer, es sei denn, der Befehl hat das Auslesen eines Werts vom Arduino angefordert; in diesem Fall wird dieser Wert in dieses Wörterbuch aufgenommen;
5.4.3. Webservice-/JSON-Tests
Machen Sie sich mit dem Webserver / JSON vertraut, indem Sie die folgenden URLs testen:
Hier sind einige Screenshots davon, was Sie sehen sollten:
Rufe die Liste der verbundenen Arduinos ab:
![]() |
Die vom Webserver / JSON empfangene JSON-Zeichenkette ist ein Objekt mit den folgenden Feldern:
- [status]: 0 bedeutet, dass kein Fehler vorliegt – andernfalls ist ein Fehler aufgetreten;
- [messages]: eine Liste von Meldungen, die den Fehler erklären, falls ein Fehler aufgetreten ist:
- [body]: die Liste der Arduinos, falls kein Fehler aufgetreten ist. Jeder Arduino wird dann durch ein Objekt mit den folgenden Feldern beschrieben:
- [id]: die Kennung des Arduinos. Keine zwei Arduinos dürfen dieselbe Kennung haben;
- [description]: eine kurze Beschreibung der Funktionalität des Arduinos;
- [mac]: die MAC-Adresse des Arduinos;
- [ip]: die IP-Adresse des Arduinos;
- [port]: der Port, auf dem er auf Befehle wartet;
Lassen Sie die LED an Pin 8 des Arduinos, der mit [cuisine] gekennzeichnet ist, alle 100 ms 20 Mal blinken:
![]() |
Die vom Webserver / JSON empfangene JSON-Zeichenkette ist ein Objekt mit den folgenden Feldern:
- [status]: 0 bedeutet, dass kein Fehler vorliegt – andernfalls ist ein Fehler aufgetreten;
- [messages]: eine Liste von Meldungen, die den Fehler erklären, falls ein Fehler aufgetreten ist:
- [body]: die Antwort des Arduino, falls kein Fehler aufgetreten ist:
- [id]: Befehlskennung. Diese Kennung ist die 1 in [/blink/1]. Der Arduino fügt diese Befehlskennung in seine Antwort ein;
- [error]: eine Fehlernummer. Ein Wert ungleich 0 weist auf einen Fehler hin;
- [state]: wird nur zum Auslesen eines Pins verwendet. Sein Wert ist der Wert des Pins;
- [json]: Wird nur im Falle eines JSON-Fehlers zwischen Client und Server verwendet. Sein Wert ist die fehlerhafte JSON-Zeichenkette, die vom Arduino gesendet wurde;
Analoger Messwert von Pin 0 am Arduino, gekennzeichnet durch [kitchen]:
![]() |
Die vom Webserver /json empfangene JSON-Zeichenkette ähnelt der vorherigen, mit Ausnahme des Feldes [state], das den Wert von Pin 0 angibt.
Binäre Abtastung von Pin 5 am Arduino, gekennzeichnet durch [kitchen]:
![]() |
Die vom /json-Webserver empfangene JSON-Zeichenkette ähnelt der vorherigen.
Binäres Schreiben des Werts 1 an Pin 8 des Arduinos mit der Kennung [kitchen]:
![]() |
Die vom /json-Webserver empfangene JSON-Zeichenkette ähnelt der vorherigen.
Das Testen der URL [http://localhost:8080/arduinos/commands/cuisine] ist komplizierter. Die /json-Methode des Webservers, die diese URL verarbeitet, erwartet eine POST-Anfrage, die sich mit einem Browser nicht ohne Weiteres simulieren lässt. Um diese URL zu testen, können Sie einen Chrome-Browser mit der Erweiterung [Advanced REST Client] verwenden (siehe Abschnitt 6.13):
![]() |
- in [1] die URL der zu testenden Web-/JSON-Methode;
- in [2] die POST-Methode, mit der die Anfrage gesendet werden soll;
- in [3-4] der gesendete Wert im JSON-Format;
- in [5] die JSON-Zeichenkette, die gesendet wird. Beachten Sie die eckigen Klammern, die die Liste einleiten und abschließen. Hier enthält die Liste nur einen JSON-Befehl, der Pin 8 alle 100 ms 10 Mal blinken lässt;
- in [6] wird die Anfrage gesendet;
![]() |
- in [7], die vom Server gesendete JSON-Antwort. Das Objekt erhielt ein Objekt mit den beiden üblichen Feldern [status, messages] und einem Feld [body], dessen Wert die Liste der Arduino-Antworten auf jeden der gesendeten JSON-Befehle ist.
Schauen wir uns an, was passiert, wenn wir einen JSON-Befehl mit falscher Syntax an den Arduino senden:
![]() |
Wir erhalten dann die folgende Antwort:
![]() |
Wir sehen, dass in der Antwort des Arduino der Fehlercode [104] steht, was darauf hinweist, dass der Befehl [xx] nicht erkannt wurde.
5.5. Testen des Android-Clients
![]() |
Die fertige ausführbare Datei des Android-Clients finden Sie unten:
![]() |
Ziehen Sie die oben angezeigte Datei [app-debug.apk] mit der Maus auf den Tablet-Emulator [GenyMotion]. Sie wird dann gespeichert und ausgeführt. Starten Sie außerdem den Web-/JSON-Server, falls Sie dies noch nicht getan haben. Schließen Sie das Arduino mit einer daran angeschlossenen LED an den PC an. Mit dem Android-Client können Sie Arduinos aus der Ferne verwalten. Er zeigt dem Benutzer die folgenden Bildschirme an.
Über die Registerkarte [CONFIG] können Sie eine Verbindung zum Server herstellen und die Liste der verbundenen Arduinos abrufen:

- Geben Sie unter [1] die Ihrem PC zugewiesene IP-Adresse [192.168.2.1] ein (siehe Abschnitt 5.2).
Über die Registerkarte [PINWRITE] können Sie einen Wert an einen Arduino-Pin schreiben:


Über die Registerkarte [PINREAD] können Sie den Wert von einem Arduino-Pin auslesen:

Über die Registerkarte [BLINK] können Sie eine Arduino-LED blinken lassen:

Über die Registerkarte [COMMAND] können Sie einen JSON-Befehl an einen Arduino senden:

5.6. Der Android-Client für den Webdienst / JSON
Wir werden nun das Schreiben des Android-Clients besprechen.
5.6.1. Client-Architektur
Die Architektur des Android-Clients entspricht der des Projekts [Beispiel-15] (siehe Abschnitt 1.16.2);
![]() |
- die [DAO]-Schicht kommuniziert mit dem Web-/JSON-Server;
Der Android-Client muss in der Lage sein, mehrere Arduinos gleichzeitig zu steuern. Wir möchten beispielsweise zwei LEDs auf zwei Arduinos gleichzeitig blinken lassen, nicht nacheinander. Daher verwendet unser Android-Client eine asynchrone Aufgabe pro Arduino, und diese Aufgaben werden parallel ausgeführt.
5.6.2. Das Android Studio-Client-Projekt
Duplizieren Sie das Projekt [client-android-skel] (siehe Abschnitt 2) in das Projekt [client-arduinos-01] (lesen Sie gegebenenfalls in Abschnitt 1.15 nach, wie man ein Gradle-Projekt dupliziert):
5.6.3. Die fünf XML-Ansichten
![]() |
Es wird fünf XML-Ansichten geben:
- [blink]: um eine Arduino-LED blinken zu lassen. Sie ist mit dem Fragment [BlinkFragment] verknüpft;
- [commands]: zum Senden eines JSON-Befehls an einen Arduino. Es ist mit dem Fragment [CommandsFragment] verknüpft;
- [config]: zum Konfigurieren der Webservice-/JSON-URL und zum Abrufen der anfänglichen Liste der verbundenen Arduinos. Es ist mit dem Fragment [ConfigFragment] verknüpft;
- [pinread]: zum Auslesen des binären oder analogen Werts eines Arduino-Pins. Es ist mit dem Fragment [PinReadFragment] verknüpft;
- [pinwrite]: zum Schreiben eines binären oder analogen Werts an einen Arduino-Pin. Es ist mit dem Fragment [PinWriteFragment] verknüpft;
Derzeit haben diese fünf XML-Ansichten alle denselben leeren Inhalt:
<?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>
- Die Ansicht befindet sich innerhalb eines [RelativeLayout]-Containers (Zeilen 7–10), der wiederum in einem [ScrollView]-Container (Zeilen 2–11) enthalten ist. Dadurch wird sichergestellt, dass wir die Ansicht scrollen können, wenn sie die Größe eines Tablet-Bildschirms überschreitet;
Aufgabe: Erstellen Sie die fünf XML-Ansichten.
5.6.4. Das Fragment-Menü
Wir wissen, dass Fragmente in einem mit [client-android-skel] erstellten Projekt einem Menü zugeordnet sein müssen, auch wenn dieses leer ist. In diesem Fall wird die App kein Menü haben. Das leere Menü ist bereits im Projekt vorhanden;
![]() |
5.6.5. Die fünf Fragmente der App
![]() | ![]() |
Aufgabe: Duplizieren Sie das Fragment [DummyFragment] in die fünf Fragmente der Anwendung, wie in [2] gezeigt.
Das [ConfigFragment]-Fragment hat das folgende Grundgerüst:
package client.android.fragments.behavior;
import client.android.R;
import client.android.architecture.core.AbstractFragment;
import client.android.architecture.custom.CoreState;
import client.android.fragments.state.DummyFragmentState;
import org.androidannotations.annotations.EFragment;
import org.androidannotations.annotations.OptionsMenu;
@EFragment
@OptionsMenu(R.menu.menu_vide)
public class ConfigFragment extends AbstractFragment {
// fields inherited from parent class -------------------------------------------------------
...
Ersetzen Sie Zeile 10 durch die folgende Zeile:
@EFragment(R.layout.config)
Aufgabe: Verfahren Sie bei den anderen vier Fragmenten genauso, indem Sie das Attribut [@EFragment] der Klasse anpassen.
Fragment | View |
ConfigFragment | |
PinReadFragment | |
PinWriteFragment | |
BefehleFragment | |
Blink-Fragment | |
5.6.6. Fragmentzustände
![]() | ![]() |
Jedes Fragment hat einen Status.
Aufgabe: Duplizieren Sie die Klasse [DummyFragmentState] fünfmal, um die fünf in [2] gezeigten Zustände zu erstellen.
5.6.7. Projektanpassung
![]() |
Das Paket [architecture / custom] enthält die anpassbaren Elemente der Anwendungsarchitektur.
5.6.7.1. Die Schnittstelle [IMainActivity]
Die Schnittstelle [IMainActivity] definiert, welche Fragmente von der Aktivität angefordert werden können, sowie die Anwendungskonstanten. Diese Schnittstelle sieht wie folgt aus:
package client.android.architecture.custom;
import client.android.architecture.core.ISession;
import client.android.dao.service.IDao;
public interface IMainActivity extends IDao {
// session access
ISession getSession();
// change of view
void navigateToView(int position, ISession.Action action);
// wait management
void beginWaiting();
void cancelWaiting();
// constant application -------------------------------------
// debug mode
boolean IS_DEBUG_ENABLED = true;
// maximum time to wait for server response
int TIMEOUT = 1000;
// waiting time before executing customer request
int DELAY = 000;
// basic authentication
boolean IS_BASIC_AUTHENTIFICATION_NEEDED = false;
// fragment adjacency
int OFF_SCREEN_PAGE_LIMIT = 1;
// tab bar
boolean ARE_TABS_NEEDED = true;
// waiting image
boolean IS_WAITING_ICON_NEEDED = true;
// number of fragments
int FRAGMENTS_COUNT = 5;
// view n°s
int VUE_CONFIG = 0;
int VUE_BLINK = 1;
int VUE_PINREAD = 2;
int VUE_PINWRITE = 3;
int VUE_COMMANDS = 4;
}
- Zeilen 25, 28, 31, 40: Konfiguration der [DAO]-Schicht. Diese Anwendung fragt einen Web-/JSON-Server ab;
- Zeile 37: Diese Anwendung verfügt über Registerkarten;
- Zeile 43: Diese Anwendung verfügt über fünf Fragmente;
- Zeilen 46–50: die Nummern der fünf Fragmente;
- Zeile 34: Fragment-Nachbarschaft. Der Entwickler kann hier einen Wert im Bereich [1, FRAGMENTS_COUNT-1] festlegen;
5.6.7.2. Die Klasse [CoreState]
Die Klasse [CoreState] ist die übergeordnete Klasse der Fragmentzustände:
package client.android.architecture.custom;
import client.android.architecture.core.MenuItemState;
import client.android.fragments.state.*;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
@JsonIgnoreProperties(ignoreUnknown = true)
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY)
@JsonSubTypes({
@JsonSubTypes.Type(value = ConfigFragmentState.class),
@JsonSubTypes.Type(value = BlinkFragmentState.class),
@JsonSubTypes.Type(value = PinReadFragmentState.class),
@JsonSubTypes.Type(value = PinWriteFragmentState.class),
@JsonSubTypes.Type(value = CommandsFragmentState.class)}
)
public class CoreState {
// fragment visited or not
protected boolean hasBeenVisited = false;
// status of any fragment menu
protected MenuItemState[] menuOptionsState;
// getters and setters
...
}
- Zeilen 12–16: Die Zustandsklassen für die fünf Fragmente müssen hier deklariert werden;
5.6.8. Die Klasse [MainActivity]
![]() |
Die Klasse [MainActivity] sieht wie folgt aus:
package client.android.activity;
import android.support.design.widget.TabLayout;
import android.util.Log;
import client.android.R;
import client.android.architecture.core.AbstractActivity;
import client.android.architecture.core.AbstractFragment;
import client.android.architecture.core.ISession;
import client.android.architecture.custom.IMainActivity;
import client.android.architecture.custom.Session;
import client.android.dao.entities.Arduino;
import client.android.dao.entities.ArduinoCommand;
import client.android.dao.entities.ArduinoResponse;
import client.android.dao.service.Dao;
import client.android.dao.service.IDao;
import client.android.dao.service.Response;
import client.android.fragments.behavior.*;
import org.androidannotations.annotations.Bean;
import org.androidannotations.annotations.EActivity;
import org.androidannotations.annotations.OptionsMenu;
import rx.Observable;
import java.util.List;
import java.util.Locale;
@EActivity
@OptionsMenu(R.menu.menu_main)
public class MainActivity extends AbstractActivity {
// layer [DAO]
@Bean(Dao.class)
protected IDao dao;
// session
private Session session;
// methods parent class -----------------------
@Override
protected void onCreateActivity() {
// log
if (IS_DEBUG_ENABLED) {
Log.d(className, "onCreateActivity");
}
// session
this.session = (Session) super.session;
// creation of the five tabs
for (int i = 0; i < 5; i++) {
TabLayout.Tab newTab = tabLayout.newTab();
newTab.setText(getFragmentTitle(i));
tabLayout.addTab(newTab);
}
}
@Override
protected IDao getDao() {
return dao;
}
@Override
protected AbstractFragment[] getFragments() {
return new AbstractFragment[]{new ConfigFragment_(), new BlinkFragment_(), new PinReadFragment_(), new PinWriteFragment_(), new CommandsFragment_()};
}
@Override
protected CharSequence getFragmentTitle(int position) {
Locale l = Locale.getDefault();
switch (position) {
case 0:
return getString(R.string.config_titre).toUpperCase(l);
case 1:
return getString(R.string.blink_titre).toUpperCase(l);
case 2:
return getString(R.string.pinread_titre).toUpperCase(l);
case 3:
return getString(R.string.pinwrite_titre).toUpperCase(l);
case 4:
return getString(R.string.commands_titre).toUpperCase(l);
}
return null;
}
@Override
protected void navigateOnTabSelected(int position) {
// fragment n° position is displayed
navigateToView(position, ISession.Action.NAVIGATION);
}
@Override
protected int getFirstView() {
return IMainActivity.VUE_CONFIG;
}
// implémentation IDao -----------------------------------------
}
- Zeilen 46–50: Erstellung der fünf Registerkarten der Anwendung;
- Zeile 48: Die Registerkartentitel werden von der Methode in den Zeilen 63–79 bereitgestellt;
- die fünf Fragmente werden in Zeile 60 instanziiert. Aufgrund der AA-Annotationen sind die Fragmentklassen die zuvor vorgestellten, mit einem Unterstrich als Suffix;
- Zeilen 63–79: Für jedes Fragment wird ein Titel definiert. Diese Titel werden aus der Datei [res/values/strings.xml] abgerufen
![]() |
Der Inhalt von [strings.xml] lautet wie folgt:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- application name -->
<string name="app_name">[arduinos-client-01]</string>
<!-- Fragments and tabs -->
<string name="config_titre">[Config]</string>
<string name="blink_titre">[Blink]</string>
<string name="pinread_titre">[PinRead]</string>
<string name="pinwrite_titre">[PinWrite]</string>
<string name="commands_titre">[Commands]</string>
</resources>
Aufgabe: Erstellen Sie die oben aufgeführten Elemente und kompilieren Sie das Projekt. Es sollten keine Fehler auftreten.
Führen Sie das Projekt aus. Auf dem Emulator sollte folgende Ansicht angezeigt werden:

Untersuchen Sie die Protokolle, die bei der Anzeige der ersten Ansicht ausgegeben wurden, und verfolgen Sie die verschiedenen Schritte, die ausgeführt wurden. Wechseln Sie zwischen den Registerkarten und verfolgen Sie die Protokolle weiter.
5.6.9. Die [config]-XML-Ansicht
Die [config]-XML-Ansicht sieht wie folgt aus:
![]() | ![]() |
Die obige Ansicht wird durch den folgenden XML-Code generiert:
<?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>
Die Ansicht verwendet Zeichenfolgen (android:text in den Zeilen 15, 25, 37, 50, 61, 73), die in der Datei [res/values/strings] definiert sind:
![]() |
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">android-domotique</string>
<!-- Fragments and tabs -->
<string name="config_titre">[Config]</string>
<string name="blink_titre">[Blink]</string>
<string name="pinread_titre">[PinRead]</string>
<string name="pinwrite_titre">[PinWrite]</string>
<string name="commands_titre">[Commands]</string>
<!-- Config -->
<string name="txt_TitreConfig">Se connecter au serveur</string>
<string name="txt_UrlServiceRest">Url du service web / jSON</string>
<string name="txt_MsgErreurUrlServiceRest">L\'Url du service doit être entrée sous la forme Ip1.Ip2.Ip3.IP4:Port/contexte</string>
<string name="hint_UrlServiceRest">ex (192.168.1.120:8080/rest)</string>
<string name="btn_annuler">Annuler</string>
<string name="btn_rafraichir">Rafraîchir</string>
<string name="titre_list_arduinos">Liste des Arduinos connectés</string>
</resources>
Die Ansicht verwendet Farben (android:textColor in den Zeilen 51 und 62), die in der Datei [res/values/colors] definiert sind:
![]() |
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="colorPrimary">#3F51B5</color>
<color name="colorPrimaryDark">#303F9F</color>
<color name="colorAccent">#FF4081</color>
<color name="floral_white">#FFFAF0</color>
<!-- app -->
<color name="red">#FF0000</color>
<color name="blue">#0000FF</color>
<color name="wheat">#FFEFD5</color>
</resources>
Die Ansicht verwendet Abmessungen (android:textSize in Zeile 16), die in der Datei [res/values/dimens] definiert sind:
![]() |
<resources>
<!-- Default screen margins, per the Android Design guidelines. -->
<dimen name="activity_horizontal_margin">16dp</dimen>
<dimen name="activity_vertical_margin">16dp</dimen>
<dimen name="fab_margin">16dp</dimen>
<dimen name="appbar_padding_top">8dp</dimen>
<!-- appli -->
<dimen name="titre">30dp</dimen>
</resources>
Diese Technik wurde nicht für alle Dimensionen verwendet. Sie ist jedoch der empfohlene Ansatz. Sie ermöglicht es Ihnen, Dimensionen an einer einzigen Stelle zu ändern.
Aufgabe: Erstellen Sie die oben genannten Elemente.
Führen Sie Ihr Projekt erneut aus. Sie sollten die folgende Ansicht sehen:

5.6.10. Das Fragment [ConfigFragment]
![]() |
Um die neue [config]-Ansicht zu verarbeiten, ändert sich der Code für das [ConfigFragment]-Fragment wie folgt:
package client.android.fragments.behavior;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ListView;
import android.widget.TextView;
import client.android.R;
import client.android.architecture.core.AbstractFragment;
import client.android.architecture.custom.CoreState;
import client.android.architecture.custom.IMainActivity;
import client.android.fragments.state.ConfigFragmentState;
import org.androidannotations.annotations.Click;
import org.androidannotations.annotations.EFragment;
import org.androidannotations.annotations.OptionsMenu;
import org.androidannotations.annotations.ViewById;
@EFragment(R.layout.config)
@OptionsMenu(R.menu.menu_vide)
public class ConfigFragment extends AbstractFragment {
// visual interface elements
@ViewById(R.id.btn_Rafraichir)
protected Button btnRafraichir;
@ViewById(R.id.btn_Annuler)
protected Button btnAnnuler;
@ViewById(R.id.edt_UrlServiceRest)
protected EditText edtUrlServiceRest;
@ViewById(R.id.txt_MsgErreurIpPort)
protected TextView txtMsgErreurUrlServiceRest;
@ViewById(R.id.ListViewArduinos)
protected ListView listArduinos;
@Click(R.id.btn_Rafraichir)
protected void doRafraichir() {
}
// fragment lifecycle management -------------------------------------
@Override
public CoreState saveFragment() {
return new ConfigFragmentState();
}
@Override
protected int getNumView() {
return IMainActivity.VUE_CONFIG;
}
@Override
protected void initFragment(CoreState previousState) {
}
@Override
protected void initView(CoreState previousState) {
// 1st visit?
if(previousState==null){
txtMsgErreurUrlServiceRest.setVisibility(View.INVISIBLE);
}
}
@Override
protected void updateOnSubmit(CoreState previousState) {
}
@Override
protected void updateOnRestore(CoreState previousState) {
}
@Override
protected void notifyEndOfUpdates() {
// buttons
initButtons();
}
@Override
protected void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled) {
}
// méthodes privées --------------------------------------------
private void initButtons() {
// the [Execute] button replaces the [Cancel] button
btnAnnuler.setVisibility(View.INVISIBLE);
btnRafraichir.setVisibility(View.VISIBLE);
}
}
- Zeilen 23–32: die Elemente der Benutzeroberfläche;
- Zeilen 58–60: Beim ersten Aufruf des Fragments wird die Fehlermeldung ausgeblendet;
- Zeilen 73–76: Bei jeder Anzeige des Fragments wird die Schaltfläche [Abbrechen] ausgeblendet (Zeile 82) und die Schaltfläche [Aktualisieren] angezeigt (Zeilen 86–87). Tatsächlich kann in dieser Anwendung ein Fragment nicht angezeigt werden, während ein asynchroner Vorgang läuft, weshalb die Schaltfläche [Abbrechen] sichtbar ist;
Aufgabe: Erstellen Sie die oben genannten Elemente.
Führen Sie diese neue Version aus. Die erste Ansicht sollte nun wie folgt aussehen:

5.6.10.1. Die Schaltfläche [Aktualisieren]
Vorerst werden wir den Klick auf die Schaltfläche [Aktualisieren] wie folgt behandeln:
@Click(R.id.btn_Rafraichir)
protected void doRafraichir() {
// we're going to launch a task - we're preparing the wait
beginWaiting(1);
}
@Click(R.id.btn_Annuler)
protected void doAnnuler() {
if (isDebugEnabled) {
Log.d(className, "Annulation demandée");
}
// asynchronous tasks are cancelled
cancelRunningTasks();
}
protected void beginWaiting(int numberOfRunningTasks) {
// prepare to wait for tasks
beginRunningTasks(numberOfRunningTasks);
// the [Cancel] button replaces the [Refresh] button
btnRafraichir.setVisibility(View.INVISIBLE);
btnAnnuler.setVisibility(View.VISIBLE);
}
// fragment lifecycle management -------------------------------------
...
@Override
protected void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled) {
// buttons in their original state
initButtons();
}
// méthodes privées --------------------------------------------
private void initButtons() {
// the [Execute] button replaces the [Cancel] button
btnAnnuler.setVisibility(View.INVISIBLE);
btnRafraichir.setVisibility(View.VISIBLE);
}
- Zeilen 1–5: Die Methode, die ausgeführt wird, wenn auf die Schaltfläche [Aktualisieren] geklickt wird;
- Zeile 4: Wir starten die Wartezeit;
- Zeile 18: Wir übergeben die Anzahl der zu startenden asynchronen Aufgaben an die übergeordnete Klasse. Das Ladesymbol wird angezeigt;
- Zeilen 20–21: Diese Wartezeit führt dazu, dass die Schaltfläche [Abbrechen] erscheint, die Schaltfläche [Aktualisieren] verschwindet und das Ladebild angezeigt wird. Sonst geschieht nichts. Der Benutzer kann jedoch auf die Schaltfläche [Abbrechen] klicken. Die Methode in den Zeilen 7–14 wird dann ausgeführt;
- Zeile 13: Die übergeordnete Klasse wird aufgefordert, alle Aufgaben abzubrechen. Die Klasse führt dies aus und ruft daraufhin die Methode in den Zeilen 25–29 auf, um zu signalisieren, dass alle Aufgaben abgeschlossen sind. Der Parameter [runningTasksHaveBeenCanceled] erhält den Wert „true“, um anzuzeigen, dass die Aufgaben abgebrochen wurden;
- Zeilen 35–36: Die Schaltfläche [Abbrechen] verschwindet, während die Schaltfläche [Aktualisieren] wieder erscheint.
Aufgabe: Nehmen Sie diese Änderungen vor und führen Sie dann das Projekt aus. Vergewissern Sie sich, dass die Schaltfläche [Refresh] die Wartezeit startet und die Schaltfläche [Cancel] sie beendet. Beobachten Sie die Protokolle.
5.6.10.2. Eingabevalidierung
In der vorherigen Version haben wir die eingegebene URL nicht validiert. Um sie zu validieren, fügen wir den folgenden Code in [ConfigFragment] ein:
// entered values
private String urlServiceRest;
@Click(R.id.btn_Rafraichir)
protected void doRafraichir() {
// check entries
if (!pageValid()) {
return;
}
// we're going to launch a task - we're preparing the wait
beginWaiting(1);
}
// input verification
private boolean pageValid() {
// initially no error msg
txtMsgErreurUrlServiceRest.setVisibility(View.INVISIBLE);
// retrieve server IP and port
urlServiceRest = String.format("http://%s", edtUrlServiceRest.getText().toString().trim());
// we check its validity
try {
URI uri = new URI(urlServiceRest);
String host = uri.getHost();
int port = uri.getPort();
if (host == null || port == -1) {
throw new Exception();
}
} catch (Exception ex) {
// error msg display
txtMsgErreurUrlServiceRest.setVisibility(View.VISIBLE);
// back to UI
return false;
}
// it's good
return true;
}
- Zeile 2: die eingegebene URL;
- Zeilen 7–9: Bevor wir etwas tun, prüfen wir die Gültigkeit der Eingabe;
- Zeile 19: Die eingegebene URL wird abgerufen und mit dem Präfix [http://] versehen;
- Zeile 22: Wir versuchen, damit ein URI-Objekt (Uniform Resource Identifier) zu erstellen. Ist die eingegebene URL syntaktisch falsch, wird eine Ausnahme ausgelöst;
- Zeilen 23–27: Es wird eine Ausnahme ausgelöst, wenn die URI gültig ist, aber [host==null] und [port==-1]. Dies ist ein mögliches Szenario;
- Zeile 30: Es ist eine Ausnahme aufgetreten. Die Fehlermeldung wird angezeigt;
- Zeile 32: Wir geben [false] zurück, um anzuzeigen, dass die Seite ungültig ist;
- Zeile 35: Es sind keine Fehler aufgetreten. Wir geben [true] zurück, um anzuzeigen, dass die Seite gültig ist;
Aufgabe: Implementieren Sie die oben beschriebene Funktionalität.
Testen Sie diese neue Version und überprüfen Sie, ob ungültige URLs korrekt markiert werden.
5.6.10.3. Anzeige der Liste der Arduinos
![]() |
Die verschiedenen Ansichten müssen die Liste der verbundenen Arduinos anzeigen. Dazu definieren wir verschiedene Klassen und eine XML-Ansicht:
- Ein Arduino wird durch die Klasse [Arduino] [1] dargestellt;
- die Klasse [CheckedArduino] [1] erbt von der Klasse [Arduino], der wir einen booleschen Wert hinzugefügt haben, um anzugeben, ob der Arduino in einer Liste ausgewählt wurde;
Die Klasse [Arduino] ist diejenige, die bereits vom Server verwendet wird und in Abschnitt 5.4.2 vorgestellt wurde. Sie lautet wie folgt:
package android.arduinos.entities;
import java.io.Serializable;
public class Arduino implements Serializable {
// data
private String id;
private String description;
private String mac;
private String ip;
private int port;
// getters and setters
...
}
- Zeile 7: [id] ist die Kennung des Arduino;
- Zeile 8: seine Beschreibung;
- Zeile 9: seine MAC-Adresse;
- Zeile 10: seine IP-Adresse;
- Zeile 11: der Port, auf dem er auf Befehle wartet;
Diese Klasse entspricht der JSON-Zeichenkette, die vom Server empfangen wird, wenn die Liste der verbundenen Arduinos angefordert wird:
![]() |
Die Klasse [CheckedArduino] erbt von der Klasse [Arduino]:
package android.arduinos.entities;
public class CheckedArduino extends Arduino {
private static final long serialVersionUID = 1L;
// an Arduino can be selected
private boolean isChecked;
// manufacturer
public CheckedArduino(Arduino arduino, boolean isChecked) {
// parent
super(arduino.getId(), arduino.getDescription(), arduino.getMac(), arduino.getIp(), arduino.getPort());
// local
this.isChecked = isChecked;
}
// getters and setters
public boolean isChecked() {
return isChecked;
}
public void setChecked(boolean isChecked) {
this.isChecked = isChecked;
}
}
- Zeile 3: Die Klasse [CheckedArduino] erbt von der Klasse [Arduino];
- Zeile 6: Wir fügen eine boolesche Variable hinzu, die uns mitteilt, ob ein Arduino aus der angezeigten Liste der Arduinos ausgewählt wurde;
In [ConfigFragment] simulieren wir das Abrufen der Liste der verbundenen Arduinos.
![]() |
@ViewById(R.id.ListViewArduinos)
protected ListView listArduinos;
..
@Click(R.id.btn_Rafraichir)
protected void doRafraichir() {
// check entries
if (!pageValid()) {
return;
}
// we're going to launch a task - we're preparing the wait
beginWaiting(1);
// we clean up the Arduinos list
clearArduinos();
// request the list of Arduinos running in the background
getArduinosInBackground();
}
private void getArduinosInBackground() {
...
}
// raz list of Arduinos
private void clearArduinos() {
// create an empty list
List<String> strings = new ArrayList<>();
// we display it
listArduinos.setAdapter(new ArrayAdapter<String>(activity, android.R.layout.simple_list_item_1, android.R.id.text1, strings));
}
- Zeile 2: die ListView, die die mit dem Server verbundenen Arduinos anzeigt;
- Zeile 5: Die Methode, die die Liste der verbundenen Arduinos abruft;
- Zeile 11: Wir teilen der übergeordneten Klasse mit, dass wir eine asynchrone Aufgabe starten werden;
- Zeile 12: Wir löschen die aktuell angezeigte Liste der Arduinos;
- Zeile 15: Wir fordern die Liste der verbundenen Arduinos als Hintergrundaufgabe an;
- Zeilen 23–28: die Methode, die die aktuell angezeigte Liste der Arduinos löscht;
Die Methode [getArduinosInBackground] lautet wie folgt:
private void getArduinosInBackground() {
// create a fictitious arduino list
List<Arduino> arduinos = new ArrayList<>();
for (int i = 0; i < 20; i++) {
arduinos.add(new Arduino("id" + i, "desc" + i, "mac" + i, "ip" + i, i));
}
// we simulate a server response
Response<List<Arduino>> response = new Response<>();
response.setBody(arduinos);
// we cancel the wait
cancelWaitingTasks();
// change the buttons
initButtons();
// we consume the answer
consumeArduinosResponse(response);
}
- Zeilen 3–6: Erstellen einer Liste mit 20 Arduinos;
- Zeilen 8–9: Wir konstruieren die Antwort vom Typ [Response<List<Arduino>>] (Abschnitt 5.4.2), die die erstellte Liste von Arduinos kapseln wird;
- Zeile 11: Abbruch der Wartezeit;
- Zeile 13: Setze die Tasten auf ihren Ausgangszustand zurück;
- Zeile 15: Die Antwort wird verarbeitet;
Die Methode [consumeArduinosResponse] lautet wie folgt:
// response display
private void consumeArduinosResponse(Response<List<Arduino>> response) {
// mistake?
if (response.getStatus() != 0) {
// display
showAlert(response.getMessages());
// back to Ui
return;
}
// we create a list of [CheckedArduino]
List<CheckedArduino> checkedArduinos = new ArrayList<>();
for (Arduino arduino : response.getBody()) {
checkedArduinos.add(new CheckedArduino(arduino, false));
}
// we display them
showArduinos(checkedArduinos);
}
- Zeilen 4–11: Überprüfe den Fehlercode in der vom Server gesendeten Antwort:
- Zeile 4: wenn der Fehlercode ungleich Null ist;
- Zeile 6: Zeige die vom Server im Feld [messages] der Antwort gespeicherten Meldungen an;
- Zeile 8: Zurück zur Benutzeroberfläche;
- Zeilen 11–16: Wenn keine Fehler aufgetreten sind, zeige die Liste der empfangenen Arduinos an, nachdem sie in den Typ List<CheckedArduino> konvertiert wurde;
Die Methode [showArduinos] lautet wie folgt:
private void showArduinos(List<CheckedArduino> checkedArduinos) {
// create a list of Strings from the list of Arduinos
List<String> strings = new ArrayList<>();
for (CheckedArduino checkedArduino : checkedArduinos) {
strings.add(checkedArduino.toString());
}
// we display it
listArduinos.setAdapter(new ArrayAdapter<>(activity, android.R.layout.simple_list_item_1, android.R.id.text1, strings));
}
Aufgabe: Nimm die oben genannten Änderungen vor und führe dein Projekt aus.
Wenn Sie auf die Schaltfläche [Aktualisieren] klicken, sollte folgende Ansicht angezeigt werden:

Die Eingabe in [1] wird nicht verwendet. Sie können daher beliebige Zeichen eingeben, solange diese dem erwarteten Format entsprechen.
5.6.10.4. Eine Vorlage zur Anzeige eines Arduino
Derzeit werden angeschlossene Arduinos in der Ansicht [Config] wie folgt angezeigt:

Wir möchten sie nun wie folgt anzeigen:
![]()
- in [1] ein Kontrollkästchen, mit dem Sie einen Arduino auswählen können. Dieses Kontrollkästchen wird ausgeblendet, wenn Sie eine Liste nicht auswählbarer Arduinos anzeigen möchten;
- in [2] die ID des Arduinos;
- in [3] seine Beschreibung;
Das Folgende baut auf Konzepten auf, die in den Projekten [Beispiel-19] und [Beispiel-19B] in Abschnitt 1.20 entwickelt wurden. Sehen Sie sich diese bei Bedarf noch einmal an.
Zunächst erstellen wir die Ansicht, die ein Element aus der Liste der Arduinos anzeigt:
![]() |
Der Code für die obige Ansicht [listarduinos_item] lautet wie folgt:
<?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>
- Zeilen 9–15: das Kontrollkästchen;
- Zeilen 17–23: der Text [Id: ];
- Zeilen 25–33: Hier wird die Arduino-ID eingegeben;
- Zeilen 35–43: der Text [Beschreibung: ];
- Zeilen 45–53: Hier wird die Arduino-Beschreibung eingegeben;
Diese Ansicht verwendet Text (Zeilen 23, 32, 43), der in [res/values/strings.xml] definiert ist:
<string name="dummy">XXXXX</string>
<!-- listarduinos_item -->
<string name="txt_arduino_id">Id : </string>
<string name="txt_arduino_description">Description : </string>
Die Ansicht verwendet außerdem eine Farbe (Zeilen 33, 53), die in [res / values / colors.xml] definiert ist:
<?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>
Der View-Manager für ein Element in der Arduino-Liste
![]() |
Die Klasse [ListArduinosAdapter] ist die Klasse, die von der [ListView] aufgerufen wird, um die einzelnen Elemente der Arduino-Liste anzuzeigen. Ihr Code lautet wie folgt:
package istia.st.android.vues;
import istia.st.android.R;
...
public class ListArduinosAdapter extends ArrayAdapter<CheckedArduino> {
// the arduino board
private List<CheckedArduino> arduinos;
// execution context
private Context context;
// the layout id for displaying a line in the arduino list
private int layoutResourceId;
// whether or not the line contains a checkbox
private Boolean selectable;
// manufacturer
public ListArduinosAdapter(Context context, int layoutResourceId, List<CheckedArduino> arduinos, Boolean selectable) {
// parent
super(context, layoutResourceId, arduinos);
// memorize information
this.arduinos = arduinos;
this.context = context;
this.layoutResourceId = layoutResourceId;
this.selectable = selectable;
}
@Override
public View getView(final int position, View convertView, ViewGroup parent) {
...
}
}
- Zeile 18: Der Klassenkonstruktor nimmt vier Parameter entgegen: die aktuell ausgeführte Aktivität, die ID der Ansicht, die für jedes Element in der Datenquelle angezeigt werden soll, die Datenquelle, die die Liste füllt, und einen booleschen Wert, der angibt, ob das zu jedem Arduino gehörende Kontrollkästchen angezeigt werden soll oder nicht;
- Zeilen 8–15: Diese vier Informationen werden lokal gespeichert;
Zeile 29: Die Methode [getView] ist dafür zuständig, die Ansicht #[position] in der [ListView] zu generieren und deren Ereignisse zu verarbeiten. Ihr Code lautet wie folgt:
@Override
public View getView(int position, View convertView, ViewGroup parent) {
// the current arduino
final CheckedArduino arduino = arduinos.get(position);
// create the current line
View row = ((Activity) context).getLayoutInflater().inflate(layoutResourceId, parent, false);
// retrieve references on [TextView]
TextView txtArduinoId = (TextView) row.findViewById(R.id.txt_arduino_id);
TextView txtArduinoDesc = (TextView) row.findViewById(R.id.txt_arduino_description);
// fill in the line
txtArduinoId.setText(arduino.getId());
txtArduinoDesc.setText(arduino.getDescription());
// the CheckBox is not always visible
CheckBox ck = (CheckBox) row.findViewById(R.id.checkBoxArduino);
ck.setVisibility(selectable ? View.VISIBLE : View.INVISIBLE);
if (selectable) {
// we assign its value
ck.setChecked(arduino.isChecked());
// we manage the click
ck.setOnCheckedChangeListener(new OnCheckedChangeListener() {
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
arduino.setChecked(isChecked);
}
});
}
// we return the line
return row;
}
- Zeile 2: Der erste Parameter ist die Position der zu erstellenden Zeile in der [ListView]. Es ist auch die Position in der lokal gespeicherten Liste der Arduinos;
- Zeile 4: Wir rufen eine Referenz auf den Arduino ab, der der erstellten Zeile zugeordnet wird;
- Zeile 6: Die aktuelle Zeile wird aus der Ansicht [listarduinos_item.xml] erstellt;
- Zeilen 8–9: Referenzen auf die beiden [TextView]s werden abgerufen;
- Zeilen 11–12: Den beiden [TextView]s werden ihre Werte zugewiesen;
- Zeile 14: Eine Referenz auf das Kontrollkästchen wird abgerufen;
- Zeile 15: Es wird sichtbar gemacht oder nicht, je nach dem Wert von [selectable], der ursprünglich an den Konstruktor übergeben wurde;
- Zeile 16: falls das Kontrollkästchen vorhanden ist;
- Zeile 18: Der [isChecked]-Wert des aktuellen Arduino wird ihr zugewiesen;
- Zeilen 20–26: Wir verarbeiten den Klick auf das Kontrollkästchen;
- Zeile 23: Der Wert des Kontrollkästchens wird im aktuellen Arduino gespeichert;
Verwaltung der Arduino-Liste
Die Anzeige der Arduino-Liste wird derzeit durch zwei Methoden der Klasse [ConfigFragment] gesteuert:
- [clearArduinos]: Zeigt eine leere Liste an;
- [showArduinos]: zeigt die vom Server zurückgegebene Liste an;
Diese beiden Methoden funktionieren wie folgt:
// raz list of Arduinos
private void clearArduinos() {
// an empty list is displayed
ListArduinosAdapter adapter = new ListArduinosAdapter(getActivity(), R.layout.listarduinos_item, new ArrayList<CheckedArduino>(), false);
listArduinos.setAdapter(adapter);
}
// arduinos list display
private void showArduinos(List<CheckedArduino> checkedArduinos) {
// display Arduinos
ListArduinosAdapter adapter = new ListArduinosAdapter(getActivity(), R.layout.listarduinos_item, checkedArduinos, false);
listArduinos.setAdapter(adapter);
}
Aufgabe: Nimm diese Änderungen vor und teste die neue App.

5.6.10.5. Die Sitzung
In der Session speichern wir die Informationen, die zwischen den Fragmenten und der Aktivität ausgetauscht werden. Alle Fragmente müssen die Liste der verbundenen Arduinos anzeigen. Eine erste Version der Session würde also wie folgt aussehen:
package client.android.architecture.custom;
import client.android.activity.CheckedArduino;
import client.android.architecture.core.AbstractSession;
import java.util.ArrayList;
import java.util.List;
public class Session extends AbstractSession {
// data to be shared between fragments themselves and between fragments and activities
// elements that cannot be serialized as jSON must be annotated with @JsonIgnore
// don't forget the getters and setters required for serialization / deserialization jSON
// the Arduinos list
private List<CheckedArduino> checkedArduinos = new ArrayList<>();
// getters and setters
...
}
Aufgabe: Erstellen Sie die oben gezeigte Klasse [Session].
Um diese Sitzung zu erstellen, müssen wir den bestehenden Code wie folgt ändern:
// response display
private void consumeArduinosResponse(Response<List<Arduino>> response) {
// mistake?
if (response.getStatus() != 0) {
// display
showAlert(response.getMessages());
// cancellation
doAnnuler();
// back to Ui
return;
}
// we create a list of [CheckedArduino]
List<CheckedArduino> checkedArduinos = new ArrayList<>();
for (Arduino arduino : response.getBody()) {
checkedArduinos.add(new CheckedArduino(arduino, false));
}
// we put it in session
session.setCheckedArduinos(checkedArduinos);
// we display them
showArduinos(checkedArduinos);
// we cancel the wait
cancelWaitingTasks();
}
- Zeile 18: Die in den vorherigen Zeilen erstellte Liste der Arduinos wird in der Sitzung abgelegt;
5.6.10.6. Verwaltung des Fragment-Zustands
Wenn das Gerät gedreht wird, werden die visuellen Komponenten der Ansicht (standardmäßig) in dem Zustand gerendert, in dem sie sich zum Zeitpunkt der Gestaltung der Ansicht befanden:
- Die [ListView] enthält die Elemente, die der Designer dort platziert hat;
- die Fehlermeldung befindet sich in dem sichtbaren oder nicht sichtbaren Zustand, in dem der Designer sie platziert hat;
Die Zustände der visuellen Komponenten zum Zeitpunkt der Gestaltung sind beim Wiederherstellen eines Fragments möglicherweise nicht angemessen. Was ist hier der Fall?
- Die [ListView] muss die Liste der verbundenen Arduinos anzeigen. Der Wert der [ListView] zur Entwurfszeit kann daher nicht verwendet werden;
- Die [TextView] für die Fehlermeldung muss in den sichtbaren oder ausgeblendeten Zustand zurückversetzt werden, den sie zum Zeitpunkt des Speicherns hatte. Ihr Wert zur Entwurfszeit ist für diese beiden Fälle möglicherweise nicht geeignet;
Wir müssen daher den Zustand dieser beiden Komponenten speichern, wenn wir den Zustand des Fragments speichern:
- die Liste der verbundenen Arduinos;
- die Sichtbarkeit (angezeigt/ausgeblendet) der Fehlermeldung bei der Eingabe der Webservice-URL/JSON;
Da die Liste der Arduinos in der Sitzung vorhanden ist, wird sie automatisch gespeichert. Die Sichtbarkeit der Fehlermeldung wird in der folgenden [ConfigFragmentState]-Klasse gespeichert:
![]() |
package client.android.fragments.state;
import client.android.architecture.custom.CoreState;
public class ConfigFragmentState extends CoreState {
// visibility error message
private boolean txtMsgErreurUrlServiceRestVisible;
// getters and setters
...
}
Aufgabe: Erstellen Sie die oben gezeigte Klasse [ConfigFragmentState].
Um die Zustände der Fragmente korrekt wiederherzustellen, müssen deren Methoden [getNumView] und [saveFragment] geändert werden. Die Methode für das Fragment [BlinkFragment] sieht derzeit beispielsweise wie folgt aus:
@Override
public CoreState saveFragment() {
// save the fragment
DummyFragmentState state=new DummyFragmentState();
// ...
return state;
// if there's nothing to save, do [return new CoreState();] and delete class [DummyFragmentState]
}
@Override
protected int getNumView() {
// return the fragment number in the table of fragments managed by the activity (cf MainActivity)
return 0;
}
Wenn nichts unternommen wird, wird der in Zeile 6 gerenderte Zustand in Element 0 (Zeile 13) des Arrays CoreState[] coreStates der Klasse [AbstractSession] (Zeile 5 unten) gespeichert:
public class AbstractSession implements ISession {
...
// view status
private CoreState[] coreStates = new CoreState[0];
...
Er muss jedoch in dem Element gespeichert werden, das der Fragment-ID [BlinkFragment] im Fragment-Array entspricht, das in der Klasse [MainActivity] definiert ist (Zeile 9 unten):
@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_()};
}
Die Fragment-IDs wurden in der Schnittstelle [IMainActivity] definiert:
public interface IMainActivity extends IDao {
...
// view n°s
int VUE_CONFIG = 0;
int VUE_BLINK = 1;
int VUE_PINREAD = 2;
int VUE_PINWRITE = 3;
int VUE_COMMANDS = 4;
}
Letztendlich wird der Status des Fragments [BlinkFragment] korrekt verwaltet, wenn wir schreiben:
@Override
public CoreState saveFragment() {
// save the fragment
DummyFragmentState state=new DummyFragmentState();
// ...
return state;
// if there's nothing to save, do [return new CoreState();] and delete class [DummyFragmentState]
}
@Override
protected int getNumView() {
// return the fragment number in the table of fragments managed by the activity (cf MainActivity)
return IMainActivity.VUE_BLINK;
}
- Zeile 14: Gibt die Fragment-ID [BlinkFragment] im Array der von der Aktivität verwalteten Fragmente zurück;
Darüber hinaus sieht die Klasse [CoreState], die die übergeordnete Klasse der Fragmentzustände ist, derzeit wie folgt aus (siehe Abschnitt 5.6.7.2):
package client.android.architecture.custom;
import client.android.architecture.core.MenuItemState;
import client.android.fragments.state.*;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
@JsonIgnoreProperties(ignoreUnknown = true)
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY)
@JsonSubTypes({
@JsonSubTypes.Type(value = ConfigFragmentState.class),
@JsonSubTypes.Type(value = BlinkFragmentState.class),
@JsonSubTypes.Type(value = PinReadFragmentState.class),
@JsonSubTypes.Type(value = PinWriteFragmentState.class),
@JsonSubTypes.Type(value = CommandsFragmentState.class)}
)
public class CoreState {
// fragment visited or not
protected boolean hasBeenVisited = false;
// status of any fragment menu
protected MenuItemState[] menuOptionsState;
// getters and setters
....
}
- Zeilen 12–16: Die Klasse [DummyFragmentState] ist nicht unter den Unterklassen der Klasse [CoreState] aufgeführt. Die Methode [saveFragment] der Klasse [BlinkFragment] gibt jedoch derzeit einen Typ [DummyFragmentState] zurück. Wenn dies so belassen wird, schlägt die Serialisierung/Deserialisierung der Sitzung fehl und die Sitzung wird nicht wiederhergestellt, was zu einem Absturz der Anwendung führt;
Die Methode [saveFragment] des Fragments [BlinkFragment] muss wie folgt umgeschrieben werden:
@Override
public CoreState saveFragment() {
// save the fragment
BlinkFragmentState state=new BlinkFragmentState();
// ...
return state;
// if there's nothing to save, do [return new CoreState();] and delete class [DummyFragmentState]
}
Aufgabe: Ändere in jedem Fragment die Methode [getNumView] so, dass sie die Fragmentnummer zurückgibt, und die Methode [saveFragment] so, dass sie eine Instanz der Fragment-State-Klasse zurückgibt (wie oben gezeigt).
5.6.10.7. Verwaltung des Fragment-Lebenszyklus
Hier konzentrieren wir uns auf den Lebenszyklus des Fragments [ConfigFragment], insbesondere auf die folgenden vier Methoden:
- [saveFragment]: muss den Zustand des Fragments speichern, damit er später wiederhergestellt werden kann;
- [initFragment]: muss bei Bedarf bestimmte Felder des Fragments initialisieren. Diese Methode wird beim Start der Anwendung und bei jeder Drehung des Geräts aufgerufen. Genauer gesagt wird sie aufgerufen, wenn das Fragment nach einem der beiden vorangegangenen Ereignisse sichtbar wird;
- [initView]: muss bei Bedarf bestimmte Ansichtskomponenten initialisieren. Diese Methode wird jedes Mal aufgerufen, wenn [initFragment] aufgerufen wurde, sowie wenn die Ansicht neu gezeichnet werden muss, weil sich das Fragment zu einem bestimmten Zeitpunkt aus dem Bereich des angezeigten Fragments bewegt hat. Wie zuvor wird sie aufgerufen, wenn das Fragment nach einem dieser Ereignisse sichtbar wird;
- [updateOnRestore]: Diese Methode wird nach den beiden vorherigen Methoden ausgeführt, wenn das Gerät gedreht wurde, aber auch bei einer Navigation. Ihre Aufgabe ist es, den vorherigen Zustand des Fragments wiederherzustellen;
Diese Methoden lauten wie folgt:
// arduinos list adapter
private ListArduinosAdapter adapterListArduinos;
...
// fragment lifecycle management -------------------------------------
@Override
public CoreState saveFragment() {
ConfigFragmentState state = new ConfigFragmentState();
state.setTxtMsgErreurUrlServiceRestVisible(txtMsgErreurUrlServiceRest.getVisibility() == View.VISIBLE);
return state;
}
@Override
protected void initFragment(CoreState previousState) {
// adapter listArduinos
adapterListArduinos = new ListArduinosAdapter(activity, R.layout.listarduinos_item, session.getCheckedArduinos(), false);
}
@Override
protected void initView(CoreState previousState) {
// listview / adapter connection
listArduinos.setAdapter(adapterListArduinos);
// 1st visit?
if (previousState == null) {
// ListView empty - made by [initFragment]
// hidden error message
txtMsgErreurUrlServiceRest.setVisibility(View.INVISIBLE);
} else {
// error message visibility is restored
ConfigFragmentState state = (ConfigFragmentState) previousState;
txtMsgErreurUrlServiceRest.setVisibility(state.isTxtMsgErreurUrlServiceRestVisible() ? View.VISIBLE : View.INVISIBLE);
}
}
@Override
protected void updateOnSubmit(CoreState previousState) {
}
@Override
protected void updateOnRestore(CoreState previousState) {
}
@Override
protected void notifyEndOfUpdates() {
// buttons
initButtons();
}
- Zeile 2: Der ListView-Adapter für die Arduinos. Es handelt sich um eine globale Variable, da sie in verschiedenen Methoden verwendet wird;
- Zeilen 7–12: Die Methode [saveFragment] speichert die Sichtbarkeit des TextViews txtMsgErreurUrlServiceRestVisible (Zeile 10) in einem Typ [ConfigFragmentState];
- Zeilen 14–19: Die Methode [initFragment] initialisiert den Adapter aus Zeile 2 mit der Liste der Arduinos, die sich derzeit in der Sitzung befinden (Zeile 17). Beachten Sie, dass die Aufgabe von [initFragment] darin besteht, die Felder des Fragments zu initialisieren. Diese Initialisierung muss hier in jedem Fall durchgeführt werden, unabhängig davon, ob es sich um den ersten Besuch handelt (previousState == null) oder nicht;
- Zeile 17: Wir sehen, dass der Adapter an die Datenquelle [session.getCheckedArduinos] gebunden ist. Diese darf nicht den Wert null haben. Aus diesem Grund wird das Feld [session.checkedArduinos] in der Sitzung mit einer leeren Liste initialisiert:
// la liste des Arduinos
private List<CheckedArduino> checkedArduinos = new ArrayList<>();
- Zeilen 21–35: Die Methode [initView] ist für die Initialisierung bestimmter Komponenten der visuellen Benutzeroberfläche zuständig, insbesondere jener, deren Werte bei einer Drehung des Geräts nicht beibehalten werden;
- Zeile 24: Die Arduino-ListView wird an den Adapter aus Zeile 2 gebunden;
- Zeilen 28–32: Der erste Aufruf wird von anderen Aufrufen unterschieden;
- Zeile 29: Beim ersten Besuch muss eine leere [ListView] angezeigt werden. Dies ist der Fall, da beim ersten Besuch der [ListView]-Adapter mit einer leeren Liste verknüpft wurde (Zeile 17);
- Zeile 31: Die Fehlermeldung wird ausgeblendet;
- Zeilen 32–36: der Fall, in dem dies nicht der erste Besuch ist;
- die [ListView] befindet sich bereits seit Zeile 24 im korrekten Zustand. Es gibt nichts weiter zu tun;
- Zeilen 34–35: Die Fehlermeldung wird in den Zustand zurückversetzt, in dem sie sich beim letzten Speichern des Fragments befand;
- Zeilen 31–36: Die Methode [updateOnRestore] muss das Fragment in seinen Ausgangszustand zurücksetzen. Wir gelangen auf zwei Arten zur Methode [updateOnRestore]:
- entweder weil das Gerät gedreht wurde. In diesem Fall wurden alle notwendigen Initialisierungen bereits in [initView] durchgeführt;
- entweder weil wir von einer Registerkarte zur Registerkarte [Config] navigieren. Wenn das [Config]-Fragment seit dem Verlassen den Bereich der angezeigten Fragmente verlassen hat, wurde die Methode [initView] bereits ausgeführt und das Fragment befindet sich bereits im gewünschten Zustand. Wenn das [Config]-Fragment seit dem Verlassen die Liste der angezeigten Fragmente nicht verlassen hat, haben seine visuellen Komponenten ihren Zustand nicht geändert und es gibt nichts zu tun;
Wir sehen, dass die Methode [updateOnRestore] nichts zu tun hat. Dies ist manchmal der Fall, manchmal nicht. Der Unterschied ergibt sich aus der Methode [updateOnSubmit]: Wenn diese Methode etwas tut, das bestimmte in [initView] vorgenommene Initialisierungen überflüssig macht, sollten diese Initialisierungen in der Methode [updateOnRestore] durchgeführt werden. Nehmen wir das Beispiel eines Optionsfelds mit drei Werten: V1, V2 und V3. Vielleicht muss im Falle einer Navigation, die mit einer [SUBMIT]-Aktion verbunden ist, das ausgewählte Optionsfeld immer das mit dem Wert V1 sein. In diesem Fall ist das Wiederherstellen des Wertes des Radiobuttons in der Methode [initView] unnötig, da dieser Wert im Falle eines [SUBMIT] durch den von der Methode [updateOnSubmit] bereitgestellten Wert ersetzt wird. Es ist daher vorzuziehen, diese Wiederherstellung in die Methode [updateOnRestore] zu verlagern, um eine unnötige Operation zu vermeiden.
- Zeilen 48–52: Die Methode [notifyEndOfUpdates] wird nach allen vorhergehenden Methoden ausgeführt;
- Zeile 51: Die Schaltflächen werden in ihren Ausgangszustand versetzt: Die Schaltfläche [Refresh] wird angezeigt, und die Schaltfläche [Cancel] wird ausgeblendet:
Aufgabe: Fügen Sie den obigen Code zu [ConfigFragment] hinzu und führen Sie dann die App aus. Beachten Sie, dass beim Drehen des Geräts die Registerkarte [Config] ihren Zustand beibehält (Fehlermeldung, Liste der Arduinos). Vergewissern Sie sich, dass dasselbe Verhalten auftritt, wenn Sie einfach von der Registerkarte [Config] zur Registerkarte [Commands] --> [Config] navigieren. Im letzteren Fall wird die Ansicht [ConfigFragment] zerstört, wenn Sie in [IMainActivity] die Fragment-Adjazenz auf 1 gesetzt haben, beim Wechsel zur Registerkarte [Commands] und neu erstellt, wenn Sie zur Registerkarte [Config] zurückkehren. Überprüfen Sie während des Testens die Protokolle.
5.6.10.8. Code-Verbesserung
Der Code für das [ConfigFragment]-Fragment kann verbessert werden. Wir haben beispielsweise geschrieben:
// arduinos list adapter
private ListArduinosAdapter adapterListArduinos;
...
// arduinos list display
private void showArduinos(List<CheckedArduino> checkedArduinos) {
// display Arduinos
ListArduinosAdapter adapter = new ListArduinosAdapter(getActivity(), R.layout.listarduinos_item, checkedArduinos, false);
listArduinos.setAdapter(adapter);
}
// raz list of Arduinos
private void clearArduinos() {
// an empty list is displayed
ListArduinosAdapter adapter = new ListArduinosAdapter(getActivity(), R.layout.listarduinos_item, new ArrayList<CheckedArduino>(), false);
listArduinos.setAdapter(adapter);
}
- Wir sehen, dass wir in den Zeilen 9 und 16 eine lokale Variable verwenden, die nicht mit dem Feld in Zeile 2 verbunden ist, obwohl wir versuchen, dieselbe Entität zu bearbeiten;
Wir aktualisieren den Code wie folgt:
// arduinos list adapter
private ListArduinosAdapter adapterListArduinos;
@Click(R.id.btn_Rafraichir)
protected void doRafraichir() {
...
}
private void getArduinosInBackground() {
...
// it is consumed
consumeArduinosResponse(response);
}
// response display
private void consumeArduinosResponse(Response<List<Arduino>> response) {
// mistake?
if (response.getStatus() != 0) {
// display
showAlert(response.getMessages());
// cancellation
doAnnuler();
// back to Ui
return;
}
// we create a list of [CheckedArduino]
List<CheckedArduino> checkedArduinos = session.getCheckedArduinos();
checkedArduinos.clear();
for (Arduino arduino : response.getBody()) {
checkedArduinos.add(new CheckedArduino(arduino, false));
}
// we display them
adapterListArduinos.notifyDataSetChanged();
// we cancel the wait
cancelWaitingTasks();
}
@Override
protected void initFragment(CoreState previousState) {
// adapt listArduinos
adapterListArduinos = new ListArduinosAdapter(activity, R.layout.listarduinos_item, session.getCheckedArduinos(), false);
}
@Override
protected void initView(CoreState previousState) {
// listview / adapter connection
listArduinos.setAdapter(adapterListArduinos);
...
}
- Wenn die Methode in Zeile 5 ausgeführt wird, ist der Lebenszyklus des Fragments abgeschlossen. Daher:
- der Adapter in Zeile 2 mit seiner Datenquelle verknüpft (Zeile 41);
- die [ListView] der verbundenen Arduinos wurde mit diesem Adapter verknüpft (Zeile 48);
Wenn wir die Anzeige der [ListView] ändern möchten, müssen wir zwei Dinge tun:
- den Inhalt der Datenquelle [session.checkedArduinos] ändern;
- den Adapter mithilfe der Anweisung [adapterListArduinos.notifyDataSetChanged()] über diese Änderung informieren;
Es ist wichtig, den Inhalt der Datenquelle zu ändern, nicht die Datenquelle selbst. Wenn wir die Datenquelle selbst ändern, zeigt die Operation [adapterListArduinos.notifyDataSetChanged()] weiterhin die alte Datenquelle an. Wir müssten dann den Adapter mit der neuen Datenquelle verknüpfen.
Der Code lautet wie folgt:
- Zeile 27: Wir rufen die Datenquelle ab;
- Zeile 28: Wir löschen sie. Aus diesem Grund haben wir die Methode [clearArduinos] entfernt;
- Zeilen 29–31: Wir fügen dieser nun leeren Liste neue Elemente hinzu;
- Zeile 33: Wir weisen den Adapter an, die Anzeige zu aktualisieren. Dadurch wird die Anzeige der zugehörigen [ListView] aktualisiert;
Aufgabe: Nehmen Sie diese Änderungen vor und überprüfen Sie, ob Ihre Anwendung weiterhin funktioniert.
5.6.11. Kommunikation zwischen Ansichten
Um die Kommunikation zwischen den Ansichten zu überprüfen, lassen wir alle anderen Ansichten die Liste der Arduinos anzeigen, die von der [Config]-Ansicht abgerufen wurde. Beginnen wir mit der [blink.xml]-Ansicht. Während sie zuvor nichts anzeigte, zeigt sie nun die Liste der verbundenen Arduinos an:

![]() |
Der XML-Code für die Ansicht [blink.xml] sieht wie folgt aus:
<?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>
Dieser Code wurde direkt aus der Ansicht [config.xml] übernommen. Wir haben lediglich den oberen Rand in Zeile 19 geändert.
Aufgabe: Duplizieren Sie diesen Code in den Ansichten [commands.xml, pinread.xml, pinwrite.xml].
Der Code für das Fragment [BlinkFragment], das mit der Ansicht [blink.xml] verknüpft ist, ändert sich ebenfalls:
![]() |
// visual components
@ViewById(R.id.ListViewArduinos)
protected ListView listArduinos;
// arduinos list adapter
private ListArduinosAdapter adapterListArduinos;
...
// methods imposed by the parent class -------------------------------------------------------
...
@Override
protected void initFragment(CoreState previousState) {
// adapter listArduinos
adapterListArduinos = new ListArduinosAdapter(activity, R.layout.listarduinos_item, session.getCheckedArduinos(), true);
}
@Override
protected void initView(CoreState previousState) {
// listview / adapter connection
listArduinos.setAdapter(adapterListArduinos);
}
...
- Zeilen 2–3: die [ListView]-Komponente für die angeschlossenen Arduinos;
- Zeile 6: der Adapter für diese [ListView];
- Zeilen 12–23: Der Code für die Methoden [initFragment] und [initView] entspricht dem bereits für das Fragment [ConfigFragment] verwendeten;
- Zeile 15: Wenn das Fragment zurückgesetzt werden muss, setzen wir den Adapter aus Zeile 2 zurück, indem wir ihn mit der in der Sitzung gespeicherten Liste der Arduinos verknüpfen. Der letzte Parameter [true] des [ListArduinosAdapter]-Konstruktors bedeutet, dass wir neben jedem Arduino ein Kontrollkästchen sehen möchten;
- Zeile 22: Wenn die Ansicht des Fragments zurückgesetzt werden muss, binden wir die [ListView] der verbundenen Arduinos an den Adapter aus Zeile 6;
Aufgabe: Duplizieren Sie diesen Code in den anderen Fragmenten [CommandsFragment, PinReadFragment, PinWriteFragment]. Führen Sie die Anwendung aus und überprüfen Sie, ob nun auf jeder Registerkarte die Liste der verbundenen Arduinos angezeigt wird. Überprüfen Sie außerdem, ob Arduinos, die Sie auf einer Registerkarte markiert haben, auch auf einer anderen Registerkarte markiert bleiben, wenn Sie dorthin wechseln.
Hinweis: Der Grund dafür, dass die Arduinos markiert bleiben, ist folgender: Die Klasse [ListArduinosAdapter] wurde in Abschnitt 5.6.10.4 vorgestellt. Der Code für das Kontrollkästchen lautet wie folgt:
// the current arduino
final CheckedArduino arduino = arduinos.get(position);
...
// the CheckBox is not always visible
CheckBox ck = (CheckBox) row.findViewById(R.id.checkBoxArduino);
ck.setVisibility(selectable ? View.VISIBLE : View.INVISIBLE);
if (selectable) {
// we assign its value
ck.setChecked(arduino.isChecked());
// we manage the click
ck.setOnCheckedChangeListener(new OnCheckedChangeListener() {
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
arduino.setChecked(isChecked);
}
});
}
- Zeilen 11–15: Wenn ein Kontrollkästchen in Registerkarte X aktiviert ist, wird die Eigenschaft [checked] des Arduino in Zeile 2 auf „true“ gesetzt (Zeile 14);
- Beim Wechsel zum Reiter Y wird die [ListView] der Arduinos in diesem Reiter angezeigt. In Zeile 9 sehen wir, dass, wenn die Eigenschaft [checked] des Arduinos in Zeile 2 auf „true“ gesetzt ist, das Kontrollkästchen [ck] in Zeile 5 aktiviert wird;
5.6.12. Die [DAO]-Schicht
![]() |
Hinweis: Sehen Sie sich für diesen Abschnitt die Implementierung der [DAO]-Schicht im Projekt [example-16B] an (siehe Abschnitt 2.8.3).
Bisher haben wir die Liste der verbundenen Arduinos manuell erstellt. Wir werden sie nun vom Webserver / jSON anfordern. Dazu erstellen wir die [DAO]-Schicht:
![]() |
5.6.12.1. Die IDao-Schnittstelle
Die [IDao]-Schnittstelle der [DAO]-Schicht sieht wie folgt aus:
package client.android.dao.service;
import client.android.dao.entities.Arduino;
import client.android.dao.entities.Response;
import rx.Observable;
import java.util.List;
public interface IDao {
// Web service url
void setUrlServiceWebJson(String url);
// user
void setUser(String user, String mdp);
// customer timeout
void setTimeout(int timeout);
// basic authentication
void setBasicAuthentification(boolean isBasicAuthentificationNeeded);
// debug mode
void setDebugMode(boolean isDebugEnabled);
// client wait time in milliseconds before request
void setDelay(int delay);
// spécifique ----------------------------------------
// list of arduinos
Observable<Response<List<Arduino>>> getArduinos();
}
- Zeilen 11–26: Diese Zeilen sind bereits in der [IDao]-Schnittstelle des [client-android-skel]-Vorlagenprojekts vorhanden;
- Zeile 30: Die Methode [getArduinos] gibt die Liste der verbundenen Arduinos als Observable vom Typ Observable<[Response<List<Arduino>>>] zurück;
Beachten Sie, dass [Response<T>] der Typ aller vom Server in Form einer JSON-Zeichenkette gesendeten Antworten ist:
package client.android.dao.entities;
import java.util.List;
public class Response<T> {
// ----------------- properties
// operation status
private int status;
// any error messages
private List<String> messages;
// the body of the reply
private T body;
// manufacturers
public Response() {
}
public Response(int status, List<String> messages, T body) {
this.status = status;
this.messages = messages;
this.body = body;
}
// getters and setters
...
}
5.6.12.2. Die [WebClient]-Schnittstelle
![]() |
Die [WebClient]-Schnittstelle ist eine Schnittstelle, für die die AA-Bibliothek eine Implementierung bereitstellt. Diese Schnittstelle sieht wie folgt aus:
package client.android.dao.service;
import client.android.dao.entities.Arduino;
import client.android.dao.entities.Response;
import org.androidannotations.rest.spring.annotations.Get;
import org.androidannotations.rest.spring.annotations.Path;
import org.androidannotations.rest.spring.annotations.Rest;
import org.androidannotations.rest.spring.api.RestClientRootUrl;
import org.androidannotations.rest.spring.api.RestClientSupport;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.client.RestTemplate;
import java.util.List;
@Rest(converters = {MappingJackson2HttpMessageConverter.class})
public interface WebClient extends RestClientRootUrl, RestClientSupport {
// RestTemplate
void setRestTemplate(RestTemplate restTemplate);
// spécifique --------------------------------------
// list of arduinos
@Get("/arduinos")
Response<List<Arduino>> getArduinos();
}
- Zeilen 15–19: Diese Zeilen sind standardmäßig in der [WebClient]-Schnittstelle des Vorlagenprojekts [client-android-skel] enthalten;
- Zeile 23: Die Server-URL, die verwendet wird, um die Liste der Arduinos über eine GET-Anfrage abzurufen. Beachten Sie, dass diese URL relativ zur Stamm-URL [RestClientRootUrl] in Zeile 16 ist;
- Zeile 24: Der Server gibt eine JSON-Zeichenkette vom Typ [Response<List<Arduino>>] zurück. Diese JSON-Zeichenkette wird mithilfe des JSON-Konverters [MappingJackson2HttpMessageConverter] aus Zeile 15 automatisch in den Typ [Response<List<Arduino>>] deserialisiert;
5.6.12.3. Die Klasse [Dao]
Die Klasse [Dao] implementiert die Schnittstelle [IDao] wie folgt:
package client.android.dao.service;
import android.util.Log;
import client.android.dao.entities.Arduino;
import client.android.dao.entities.Response;
import org.androidannotations.annotations.AfterInject;
import org.androidannotations.annotations.Bean;
import org.androidannotations.annotations.EBean;
import org.androidannotations.rest.spring.annotations.RestService;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.client.RestTemplate;
import rx.Observable;
import java.util.ArrayList;
import java.util.List;
@EBean(scope = EBean.Scope.Singleton)
public class Dao extends AbstractDao implements IDao {
// web service customer
@RestService
protected WebClient webClient;
// safety
@Bean
protected MyAuthInterceptor authInterceptor;
// on RestTemplate
private RestTemplate restTemplate;
// factory du RestTemplate
private SimpleClientHttpRequestFactory factory;
@AfterInject
public void afterInject() {
// log
Log.d(className, "afterInject");
// we build the restTemplate
factory = new SimpleClientHttpRequestFactory();
restTemplate = new RestTemplate(factory);
// set the jSON converter
restTemplate.getMessageConverters().add(new MappingJackson2HttpMessageConverter());
// set the restTemplate of the web client
webClient.setRestTemplate(restTemplate);
}
@Override
public void setUrlServiceWebJson(String url) {
// set the URL of the web service
webClient.setRootUrl(url);
}
@Override
public void setUser(String user, String mdp) {
// the user is registered in the interceptor
authInterceptor.setUser(user, mdp);
}
@Override
public void setTimeout(int timeout) {
if (isDebugEnabled) {
Log.d(className, String.format("setTimeout thread=%s, timeout=%s", Thread.currentThread().getName(), timeout));
}
// factory configuration
factory.setReadTimeout(timeout);
factory.setConnectTimeout(timeout);
}
@Override
public void setBasicAuthentification(boolean isBasicAuthentificationNeeded) {
if (isDebugEnabled) {
Log.d(className, String.format("setBasicAuthentification thread=%s, isBasicAuthentificationNeeded=%s", Thread.currentThread().getName(), isBasicAuthentificationNeeded));
}
// authentication interceptor?
if (isBasicAuthentificationNeeded) {
// add the authentication interceptor
List<ClientHttpRequestInterceptor> interceptors = new ArrayList<ClientHttpRequestInterceptor>();
interceptors.add(authInterceptor);
restTemplate.setInterceptors(interceptors);
}
}
// méthodes privées -------------------------------------------------
private void log(String message) {
if (isDebugEnabled) {
Log.d(className, message);
}
}
// specific IDao implementation -----------------------------------------------
@Override
public Observable<Response<List<Arduino>>> getArduinos() {
// web client execution
return getResponse(new IRequest<Response<List<Arduino>>>() {
@Override
public Response<List<Arduino>> getResponse() {
return webClient.getArduinos();
}
});
}
}
- Zeilen 19–87: Diese Zeilen sind Teil der Klasse [Dao] im Projekt [client-android-skel];
- Zeilen 91–100: Implementierung der Methode [getArduinos];
- Zeile 94: Die Methode [getResponse] der übergeordneten Klasse wird aufgerufen. Der einzige Parameter dieser Methode ist eine Instanz der Schnittstelle [IRequest<T>];
- Zeilen 95–99: Die einzige Methode der Schnittstelle [IRequest<T>] ist die Methode [T getResponse()];
- Zeile 94: Der Typ T von [IRequest<T>] muss der Typ T des Observable<T>-Ergebnisses der Methode in Zeile 92 sein, also hier ein Typ [Response<List<Arduino>>];
- Zeile 97: Die Methode [IRequest.getResponse()] delegiert die Arbeit an die Methode [webClient.getArduinos()], die wir eingeführt haben. [webClient], definiert in Zeile 24, wird von der AA-Bibliothek instanziiert und ist eine Instanz der von uns eingeführten [WebClient]-Schnittstelle;
5.6.13. Die [MainActivity]
![]() |
Wir haben die Aktivität [MainActivity] bereits in Abschnitt 5.6.8 vorgestellt. Sie erweitert die Klasse [AbstractActivity] und implementiert somit die Schnittstelle [IMainActivity], die ihrerseits die Schnittstelle [IDao] erweitert. Immer wenn der Schnittstelle [IDao] eine Methode hinzugefügt wird, muss diese in der Klasse [MainActivity] implementiert werden. Die der Schnittstelle [IDao] hinzugefügte Methode [IDao.getArduinos] wird in [MainActivity] wie folgt implementiert:
...
@EActivity
@OptionsMenu(R.menu.menu_main)
public class MainActivity extends AbstractActivity {
// layer [DAO]
@Bean(Dao.class)
protected IDao dao;
// session
private Session session;
...
// implémentation IDao -----------------------------------------
@Override
public Observable<Response<List<Arduino>>> getArduinos() {
return dao.getArduinos();
}
}
- Zeilen 15–18: Die Methode [getArduinos] wird implementiert, indem die Arbeit an die soeben vorgestellte Klasse [Dao] delegiert wird, auf die wir in Zeile 8 verweisen;
5.6.14. Das [ConfigFragment]-Fragment noch einmal betrachtet
In der Klasse [ConfigFragment] lautet der Code, der beim Klicken auf die Schaltfläche [Refresh] ausgeführt wird, derzeit wie folgt:
@Click(R.id.btn_Rafraichir)
protected void doRafraichir() {
...
// request the list of Arduinos running in the background
getArduinosInBackground();
}
private void getArduinosInBackground() {
// create a fictitious arduino list
List<Arduino> arduinos = new ArrayList<>();
for (int i = 0; i < 20; i++) {
arduinos.add(new Arduino("id" + i, "desc" + i, "mac" + i, "ip" + i, i));
}
// we simulate a server response
Response<List<Arduino>> response = new Response<>();
response.setBody(arduinos);
// it is consumed
consumeArduinosResponse(response);
}
// response display
private void consumeArduinosResponse(Response<List<Arduino>> response) {
...
}
Wir müssen die Zeilen 10–16 umschreiben, in denen eine Antwort vom Typ [Response<List<Arduino>>] fest codiert war. Wir müssen diese Liste nun über die Aktivität von der [DAO]-Schicht anfordern. Der Code sieht nun wie folgt aus:
@Click(R.id.btn_Rafraichir)
protected void doRafraichir() {
// check entries
if (!pageValid()) {
return;
}
// save input
mainActivity.setUrlServiceWebJson(urlServiceRest);
// we prepare to wait
beginWaiting(1);
// the asynchronous task is executed
executeInBackground(mainActivity.getArduinos(), new Action1<Response<List<Arduino>>>() {
@Override
public void call(Response<List<Arduino>> response) {
// we consume the answer
consumeArduinosResponse(response);
}
});
}
- Zeile 8: Die vom Benutzer eingegebene Stamm-URL des Webdienstes/JSON wird über die Aktivität an die [DAO]-Schicht übergeben. Dies ist die Stamm-URL der [WebClient]-Schnittstelle (siehe Abschnitt 5.6.12.2);
- Zeile 10: Die übergeordnete Klasse wird darüber informiert, dass eine asynchrone Aufgabe gestartet werden soll;
- Zeilen 12–19: Start der asynchronen Aufgabe, die die Liste der mit dem Server verbundenen Arduinos zurückgibt;
- Zeile 12: Aufruf der Methode [executeInBackground] der übergeordneten Klasse. Diese Methode erwartet zwei Parameter:
- Zeile 12: den zu beobachtenden Prozess. Dieser Prozess wird hier von der Methode [mainActivity.getArduinos()] bereitgestellt;
- Zeilen 12–19: eine Instanz der Schnittstelle [Action1<T>], wobei der Typ T der vom Prozess bereitgestellte Typ ist, hier ein Typ [Response<List<Arduino>>];
- Zeilen 14–18: die Methode, die aufgerufen wird, wenn die asynchrone Aufgabe ihr Ergebnis vom Typ [Response<List<Arduino>>] zurückgibt;
- Zeile 17: Die empfangene Antwort wird an die zuvor geschriebene Methode [consumeArduinosResponse] übergeben;
Aufgabe: Starten Sie den Server wie in Abschnitt 5.4 beschrieben. Schließen Sie einen oder mehrere Arduinos an den PC an, auf dem der Server läuft. Starten Sie dann den Android-Client und überprüfen Sie, ob Sie die Liste der verbundenen Arduinos erfolgreich abrufen können. Beobachten Sie die Protokolle.

- Geben Sie die unter [1] angegebene URL ein. Dabei handelt es sich um eine der IP-Adressen Ihres Servers;
- Klicken Sie auf die Schaltfläche [2];
- Sie sollten nun die Liste der verbundenen Arduinos unter [3] sehen;
Vergewissern Sie sich, dass diese Liste auch in den anderen Registerkarten angezeigt wird.
5.7. Nächste Schritte
Führen Sie nach dem gleichen Verfahren wie für die Ansicht [Config] nacheinander die vier anderen Ansichten der Anwendung durch und testen Sie sie: [Blink], [PinRead], [PinWrite] und [Commands].
Die zu erstellenden Ansichten wurden in Abschnitt 5.5 vorgestellt.
Für jede Ansicht müssen Sie:
- die XML-Ansicht zeichnen (siehe Abschnitt 5.6.9);
- das zugehörige Fragment erstellen (siehe Abschnitt 5.6.10);
- eine Methode zur [WebClient]-Schnittstelle hinzufügen (siehe Abschnitt 5.6.12.2);
- eine Methode zur [IDao]-Schnittstelle hinzufügen (siehe Abschnitt 5.6.12.2);
- eine Methode zur Klasse [Dao] hinzufügen (siehe Abschnitt 5.6.12.3);
- eine Methode zur [MainActivity]-Aktivität hinzufügen (siehe Abschnitt 5.6.13);
- die Ereignisbehandler des Fragments schreiben (siehe Abschnitt 5.6.14);
- Testen und Beobachten der Protokolle;
Anmerkung 1: Als Beispiel dient das Projekt [Example-16B] aus dem Kurs (siehe Abschnitt 2.8.3).
Anmerkung 2: Die abzufragenden URLs und die Art ihrer Antworten wurden in Abschnitt 5.4.2 vorgestellt.
Anmerkung 3:
Die Klasse [CommandsFragment] sendet eine Liste, die einen einzelnen Befehl enthält, der von einem oder mehreren Arduinos ausgeführt werden soll. Dieser Befehl wird in die folgende Klasse [ArduinoCommand] gekapselt:
package android.arduinos.dao;
import java.util.Map;
public class ArduinoCommand {
// data
private String id;
private String ac;
private Map<String, Object> pa;
// manufacturers
public ArduinoCommand() {
}
public ArduinoCommand(String id, String ac, Map<String, Object> pa) {
this.id = id;
this.ac = ac;
this.pa = pa;
}
// getters and setters
...
}
In der [WebClient]-Schnittstelle sieht die Methode zur Ausführung dieser Befehlsliste wie folgt aus:
// envoi de commandes JSON
@Post("/arduinos/commands/{idArduino}")
Response<List<ArduinoResponse>> sendCommands(@Body List<ArduinoCommand> commands, @Path String idArduino);
- Zeile 2: Die URL wird mit einer HTTP-POST-Anfrage aufgerufen;
- Zeile 3: Der gesendete Wert muss die Annotation [@Body] tragen;
Hinweis 4: Es wird empfohlen, diese Aufgabe wie folgt anzugehen:
- Fahren Sie erst dann mit der nächsten Ansicht fort, wenn die aktuelle Ansicht erstellt und getestet wurde;
- Verwalten Sie den Status der Ansichten erst, nachdem Sie eine unter normalen Bedingungen funktionsfähige Anwendung erhalten haben. Durchlaufen Sie dann für jede Ansicht die verschiedenen Zustände der Ansicht und notieren Sie alle verlorenen Informationen. Dies sind die Daten, die gespeichert und anschließend wiederhergestellt werden müssen. Überprüfen Sie als Nächstes die Navigation: Wenn Sie eine Registerkarte verlassen und später dorthin zurückkehren, sollte sie sich im gleichen Zustand befinden wie beim Verlassen;


















































