Skip to content

5. TP 2 — Керування Arduino за допомогою планшета на базі Android

Тепер ми навчимося керувати платою Arduino за допомогою планшета. Прикладом для наслідування є проект [client-android-skel] з курсу (див. параграф 2).

5.1. Архітектура проєкту

Проект у цілому матиме таку архітектуру:

  • блок [1], веб-сервер / jSON та Arduino вам нададуть;
  • вам потрібно буде зібрати блок [2] та запрограмувати планшет на базі Android для взаємодії з веб-сервером / jSON.

5.2. Обладнання

У вашому розпорядженні є такі компоненти:

  • Arduino з розширенням Ethernet, світлодіод та датчик температури;
  • miniHub, який слід розділити з іншим студентом;
  • кабель USB для живлення Arduino;
  • два мережеві кабелі для підключення Arduino та PC до однієї приватної мережі;
  • планшет на базі Android;

5.2.1. Arduino

Ось як підключити всі елементи між собою:

  • від'єднайте мережевий кабель від вашого PC;
  • підключіть PC та Arduino мережевим кабелем;
  • Arduino, яке у вас є, вже буде запрограмоване. Його адреса IP буде [192.168.2.2]. Щоб ваш PC «побачив» Arduino, йому потрібно присвоїти адресу IP у мережі [192.168.2]. Arduino запрограмовано для взаємодії з PC, що має адресу IP [192.168.2.1]. Ось як це зробити:

Перейдіть на [Panneau de configuration\Réseau et Internet\Centre Réseau et partage]:

 
  • на [1] натисніть на посилання [réseau local];
  • на сторінці [2] натисніть кнопку [Propriétés] у локальній мережі;
  • у [3] натисніть на властивості [IPv4] карти [réseau local];
  • у [4] присвойте цій карті адресу IP [192.168.2.1] та маску підмережі [255.255.255.0];
  • у [5] натисніть на [OK] стільки разів, скільки потрібно, щоб вийти з майстра.

5.2.2. Планшет

  • за допомогою вашого Wi-Fi-ключа підключіть свій комп’ютер до мережі Wi-Fi, яку вам вкажуть. Зробіть те саме зі своїм планшетом;
  • Перевірте адресу Wi-Fi вашого пристрою IP, ввівши [ipconfig] у вікні DOS. Ви знайдете адресу у форматі [192.168.x.y];

dos>ipconfig

Configuration IP de Windows

Carte réseau sans fil Wi-Fi :

   Suffixe DNS propre à la connexion. . . :
   Adresse IPv6 de liaison locale. . . . .: fe80::39aa:47f6:7537:f8e1%2
   Adresse IPv4. . . . . . . . . . . . . .: 192.168.1.25
   Masque de sous-réseau. . . . . . . . . : 255.255.255.0
   Passerelle par défaut. . . . . . . . . : 192.168.1.1
  • перевірте адресу Wi-Fi вашого планшета (IP). Якщо не знаєте, як це зробити, зверніться до свого наставника. Ви знайдете адресу у форматі [192.168.x.z];
  • вимкніть брандмауер вашого PC, якщо він увімкнений [Panneau de configuration\Système et sécurité\Pare-feu Windows];
  • у вікні командного рядка перевірте, чи PC і планшет можуть взаємодіяти, ввівши команду [ping 192.168.x.z], де [192.168.x.z] — це адреса IP вашого планшета. Планшет повинен відповісти:
dos>ping 192.168.1.26

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

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

Мережеві налаштування вашої системи тепер готові.

5.2.3. Емулятор [Genymotion]

Емулятор [Genymotion] (див. розділ 6.9) є кращою альтернативою планшету. Він працює майже так само швидко і не потребує Wi-Fi-мережі. Рекомендується використовувати саме цей метод. Планшет можна використовувати для остаточної перевірки вашого додатка.

5.3. Програмування Arduino

Тут ми розглянемо написання коду на мові C для Arduino:

Ознайомтеся з

  • встановлення середовища розробки Arduino (див. розділ 6.1);
  • використання бібліотек jSON (Додатки, параграф 6.6);
  • у середовищі розробки Arduino протестувати приклад сервера IDE (наприклад, веб-сервер) та приклад клієнта TCP (наприклад, клієнт Telnet);
  • додатки щодо середовища програмування Arduino у пункті 6.1.

Arduino — це набір контактів, підключених до апаратного забезпечення. Ці контакти є входами або виходами. Їхнє значення може бути двійковим або аналоговим. Для керування Arduino використовуються дві основні операції:

  • запис двійкового/аналогового значення на вивід, позначений його номером;
  • зчитування двійкового/аналогового значення з виводу, позначеного його номером;

До цих двох основних операцій ми додамо третю:

  • забезпечити миготіння світлодіода протягом певного часу та з певною частотою. Цю операцію можна виконати шляхом багаторазового виклику двох попередніх базових операцій. Однак під час тестування ми побачимо, що обмін даними між модулем [DAO] та Arduino відбувається з частотою в межах секунди. Тому неможливо, наприклад, змусити світлодіод блимати кожні 100 мілісекунд. Тому ми реалізуємо цю функцію блимання безпосередньо на самому Arduino.

Arduino працюватиме наступним чином:

  • обмін даними між шаром [DAO] та Arduino відбувається через мережу TCP-IP шляхом обміну рядками тексту у форматі jSON (JavaScript Object Notation);
  • під час запуску Arduino підключається до порту 100 сервера реєстрації, що знаходиться в шарі [DAO]. Воно надсилає серверу єдиний рядок тексту:
{"id":"cuisine","desc":"duemilanove","mac":"90:A2:DA:00:1D:A7","port":102}

Це рядок jSON, що характеризує Arduino, який підключається:

  • id: ідентифікатор Arduino;
  • desc: опис можливостей Arduino. Тут просто вказано тип Arduino;
  • mac — MAC-адреса Arduino;
  • port: номер порту, на якому Arduino очікуватиме команди від рівня [DAO].

Усі ці дані мають тип «рядок символів», за винятком номера порту, який є цілим числом.

  • Як тільки Arduino зареєструється на сервері реєстрації, він починає прослуховувати порт, який вказав серверу (102 вище). Він очікує команди jSON такого формату:
{"id":"identifiant","ac":"une_action","pa":{"param1":"valeur1","param2":"valeur2",...}}

Це рядок jSON, що містить такі елементи:

  • id: ідентифікатор команди. Може бути будь-яким;
  • ac: дія. Їх є три:
  • pw (pin write) — для запису значення на вивід,
  • pr (pin read) — для зчитування значення виводу,
  • cl (блимати) — для мигання світлодіода;
  • pa: параметри дії. Вони залежать від конкретної дії.
  • Arduino завжди повертає відповідь своєму клієнту. Це рядок jSON такого вигляду:
{"id":"1","er":"0","et":{"pinx":"valx"}}

де

  • id: ідентифікатор команди, на яку надається відповідь;
  • er (помилка): код помилки, якщо сталася помилка, інакше — 0;
  • та (статус): словник, який завжди порожній, за винятком команди читання pr. У цьому випадку словник містить значення виведеного виводу № x, що запитується.

Ось приклади, що пояснюють наведені вище специфікації:

Змусити світлодіод № 8 блимати 10 разів з періодом 100 мілісекунд:

Команда
{"id":"1","ac":"cl","pa":{"pin":"8","dur":"100","nb":"10"}}
Відповідь
{"id":"1","er":"0","et":{}}

Параметри команди cl: тривалість одного миготіння (dur) у мілісекундах, кількість миготінь (nb), номер виводу світлодіода.

Записати двійкове значення 1 на вивід № 7:

Команда
{"id":"2","ac":"pw","pa":{"pin":"7","mod":"b","val":"1"}}
Відповідь
{"id":"2","er":"0","et":{}}

Параметри pa команди pw: режим запису mod b (двійковий) або a (аналоговий), значення val, яке потрібно записати, номер виводу. Для двійкового запису val дорівнює 0 або 1. Для аналогового запису val знаходиться в діапазоні [0,255].

Записати аналогове значення 120 на вивід № 2:

Команда
{"id":"3","ac":"pw","pa":{"pin":"2","mod":"a","val":"120"}}
Відповідь
{"id":"3","er":"0","et":{}}

Зчитати аналогове значення з виводу № 0:

Команда
{"id":"4","ac":"pr","pa":{"pin":"0","mod":"a"}}
Відповідь
{"id":"4","er":"0","et":{"pin0":"1023"}}

Параметри команди pr такі: режим зчитування mod b (бінарний) або a (аналоговий), номер виводу. Якщо помилок немає, Arduino вносить у поле «et» своєї відповіді значення запитуваного виводу. Тут pin0 вказує, що запитувалася величина виводу № 0, а 1023 — це саме ця величина. Під час зчитування аналогова величина буде знаходитися в діапазоні [0, 1024].

Ми розглянули три команди: cl, pw та pr. Можна запитати, чому в рядках jSON не використано більш зрозумілі поля, наприклад action замість ac, pinwrite замість pw, параметри замість pa тощо. Arduino має дуже обмежений обсяг пам’яті. Однак рядки jSON, що обмінюються з Arduino, займають місце в пам’яті. Тому ми вирішили максимально скоротити їх.

Тепер розглянемо кілька прикладів помилок:

Команда
xx
Відповідь
{"id":"","er":"100","et":{}}

Було надіслано команду, яка не відповідає формату jSON. Arduino повернув код помилки 100.

Команда
{"id":"4","ac":"pr","pa":{"mod":"a"}}
Відповідь
{"id":"4","er":"302","et":{}}

Було надіслано команду pr, забувши вказати параметр pin. Arduino повернув код помилки 302.

Команда
{"id":"4","ac":"pinread","pa":{"pin":"0","mod":"a"}}
Відповідь
{"id":"4","er":"104","et":{}}

Було надіслано невідому команду pinread (це pr). Arduino повернуло код помилки 104.

Ми не будемо продовжувати приклади. Правило просте. Arduino не повинен зависати, незалежно від того, яку команду йому надсилають. Перед виконанням команди jSON він перевіряє, чи вона правильна. Як тільки виникає помилка, Arduino припиняє виконання команди та повертає клієнту рядок помилки jSON. І знову ж таки, через обмеження обсягу пам’яті, ми повертаємо код помилки, а не повне повідомлення.

Код програми, що виконується на Arduino, наведено у прикладах цього документа:

  

Щоб перенести його на Arduino:

  • підключіть його до вашого PC;
  • у [1] відкрийте файл [arduino_uno.ino]. Arduino запуститься та завантажить файл;

Примітка: код був спочатку створений і протестований з IDE ARDUINO версії 1.5.x. З того часу з’явилися інші версії IDE. Код не працював із IDE ARDUINO 1.6.x. Схоже, що існує проблема зворотної сумісності між версіями 1.6 та 1.5.

  • У [2-4] вкажіть тип використовуваного Arduino;
  • у [5-7] вкажіть, на якому послідовному порту PC він знаходиться;
  • у [8] завантажте програму [arduino_uno] на Arduino;

Код програми містить багато коментарів. Зацікавлений читач може ознайомитися з ним. Ми лише вкажемо рядки коду, які дозволяють налаштувати двосторонній обмін даними між Arduino та PC у режимі «клієнт/сервер»:


#include <SPI.h>
#include <Ethernet.h>
#include <ajSON.h>

// ---------------------------------- CONFIGURATION DE ARDUINO UNO
// адреса MAC Arduino UNO
byte macArduino[] = { 
  0x90, 0xA2, 0xDA, 0x0D, 0xEE, 0xC7 };
char * strMacArduino="90:A2:DA:0D:EE:C7";
// адреса Arduino IP
IPAddress ipArduino(192,168,2,2);
// його ідентифікатор
char * idArduino="cuisine";
// порт сервера Arduino
int portArduino=102;
// опис Arduino
char * descriptionArduino="contrôle domotique";
// сервер Arduino працюватиме на порту 102
EthernetServer server(portArduino);
// IP — ідентифікатор сервера реєстрації
IPAddress ipServeurEnregistrement(192,168,2,1); 
// порт сервера реєстрації
int portServeurEnregistrement=100;
// клієнт Arduino сервера реєстрації
EthernetClient clientArduino;
// команда клієнта
char commande[100];
// відповідь Arduino
char message[100];

// ініціалізація
void setup() {
  // Послідовний монітор дозволить відстежувати обмін даними
  Serial.begin(9600);
  // запуск з'єднання Ethernet
  Ethernet.begin(macArduino,ipArduino);  
  // доступна пам'ять
  Serial.print(F("Memoire disponible : "));
  Serial.println(freeRam());
}

// нескінченний цикл
void loop()
{
  ...
}
  • рядок 8: MAC-адреса Arduino. Вона тут не має великого значення, оскільки Arduino буде працювати в приватній мережі, де є PC та один або кілька Arduino. Головне, щоб MAC-адреса була унікальною в цій приватній мережі. Зазвичай на мережевій платі Arduino є наклейка, на якій вказано її MAC-адресу. Якщо такої наклейки немає і ви не знаєте MAC-адресу плати, у рядку 8 можна вказати будь-яке значення, за умови дотримання правила унікальності MAC-адреси в приватній мережі;
  • рядок 11: адреса IP мережевої карти. Знову ж таки, можна вказати будь-яке значення типу [192.168.2.x], змінюючи значення x для різних Arduino в приватній мережі;
  • рядок 13: ідентифікатор Arduino. Повинен бути унікальним серед ідентифікаторів Arduino в одній приватній мережі;
  • рядок 15: службовий порт Arduino. Можна вказати будь-яке значення;
  • рядок 17: опис функції Arduino. Можна вказати будь-що. Слід бути обережними з довгими рядками через обмежену пам'ять Arduino;
  • рядок 21: адреса IP сервера реєстрації Arduino на PC. Не слід змінювати;
  • рядок 23: порт цієї служби реєстрації. Не слід змінювати;

5.4. Веб-сервер / jSON

5.4.1. Встановлення

Image

Вам надано Java-бінарний файл веб-сервера / jSON:

 

Відкрийте командний рядок і введіть таку команду:

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

Якщо [java.exe] відсутній у PATH командного вікна, потрібно ввести повний шлях до [java.exe] (зазвичай C:\Program Files\java\...).

Відкриється вікно DOS, у якому будуть відображатися журнали:


.   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::             (v0.5.0.M6)

2014-01-06 11:11:35.550  INFO 8408 --- [           main] arduino.rest.metier.Application          : Starting Application on Gportpers3 with PID 8408 (C:\Users\SergeTahÚ\Desktop\part2\server.jar started by ST)
2014-01-06 11:11:35.587  INFO 8408 --- [           main] ationConfigEmbeddedWebApplicationContext : Refreshing org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext@6a4ba620: startup date [Mon Jan 06 11:11:35 CET 2014]; root of context hierarchy
2014-01-06 11:11:36.765  INFO 8408 --- [           main] o.apache.catalina.core.StandardService   : Starting service Tomcat
2014-01-06 11:11:36.766  INFO 8408 --- [           main] org.apache.catalina.core.StandardEngine  : Starting Servlet Engine: Apache Tomcat/7.0.42
2014-01-06 11:11:36.876  INFO 8408 --- [ost-startStop-1] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring embedded WebApplicationContext
2014-01-06 11:11:36.877  INFO 8408 --- [ost-startStop-1] o.s.web.context.ContextLoader            : Root WebApplicationContext: initialization completed in 1293 ms
2014-01-06 11:11:37.084  INFO 8408 --- [ost-startStop-1] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring FrameworkServlet 'dispatcherServlet'
2014-01-06 11:11:37.084  INFO 8408 --- [ost-startStop-1] o.s.web.servlet.DispatcherServlet        : FrameworkServlet 'dispatcherServlet': initialization started
2014-01-06 11:11:37.184  INFO 8408 --- [ost-startStop-1] o.s.w.s.handler.SimpleUrlHandlerMapping  : Mapped URL path [/**/favicon.ico] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2014-01-06 11:11:37.386  INFO 8408 --- [ost-startStop-1] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/arduinos/blink/{idCommande}/{idArduino}/{pin}/{duree}/{nombre}],methods=[GET],params=[],headers=[],consumes=[],produces=[],custom=[]}" onto public java.lang.String arduino.rest.metier.RestMetier.faireClignoterLed(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,javax.servlet.http.HttpServletResponse)
2014-01-06 11:11:37.388  INFO 8408 --- [ost-startStop-1] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/arduinos/commands/{idArduino}],methods=[POST],params=[],headers=[],consumes=[],produces=[],custom=[]}" onto public java.lang.String arduino.rest.metier.RestMetier.sendCommandesJson(java.lang.String,java.lang.String,javax.servlet.http.HttpServletResponse)
2014-01-06 11:11:37.388  INFO 8408 --- [ost-startStop-1] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/arduinos/],methods=[GET],params=[],headers=[],consumes=[],produces=[],custom=[]}" onto public java.lang.String arduino.rest.metier.RestMetier.getArduinos(javax.servlet.http.HttpServletResponse)
2014-01-06 11:11:37.389  INFO 8408 --- [ost-startStop-1] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/arduinos/pinRead/{idCommande}/{idArduino}/{pin}/{mode}],methods=[GET],params=[],headers=[],consumes=[],produces=[],custom=[]}" onto public java.lang.String arduino.rest.metier.RestMetier.pinRead(java.lang.String,java.lang.String,java.lang.String,java.lang.String,javax.servlet.http.HttpServletResponse)
2014-01-06 11:11:37.390  INFO 8408 --- [ost-startStop-1] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/arduinos/pinWrite/{idCommande}/{idArduino}/{pin}/{mode}/{valeur}],methods=[GET],params=[],headers=[],consumes=[],produces=[],custom=[]}" onto public java.lang.String arduino.rest.metier.RestMetier.pinWrite(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,javax.servlet.http.HttpServletResponse)
2014-01-06 11:11:37.463  INFO 8408 --- [ost-startStop-1] o.s.w.s.handler.SimpleUrlHandlerMapping  : Mapped URL path [/**] до обробника типу [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/**] обробник типу [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2014-01-06 11:11:37.881  INFO 8408 --- [ost-startStop-1] o.s.web.servlet.DispatcherServlet        : FrameworkServlet 'dispatcherServlet': initialization completed in 796 ms
Serveur d'enregistrement lancÚ sur 192.168.2.1:100
2014-01-06 11:11:38.101  INFO 8408 --- [       Thread-4] arduino.dao.Recorder                  : Recorder : [11:11:38:101] : [Serveur d'enregistrement : attente d'un client]
2014-01-06 11:11:38.142  INFO 8408 --- [           main] arduino.rest.metier.Application : Started Application in 3.257 seconds
  • рядок 11: запускається вбудований сервер Tomcat;
  • рядок 15: сервлет [dispatcherServlet] з Spring MVC завантажується та виконується;
  • рядок 18: виявлено сервлет URL Rest [/arduinos/blink/{idCommande}/{idArduino}/{pin}/{duree}/{nombre}];
  • рядок 19: виявлено сервлет URL Rest [/arduinos/commands/{idArduino}];
  • рядок 20: виявлено URL Rest [/arduinos/];
  • рядок 21: виявлено URL Rest [/arduinos/pinRead/{idCommande}/{idArduino}/{pin}/{mode}];
  • рядок 22: виявлено URL Rest [/arduinos/pinWrite/{idCommande}/{idArduino}/{pin}/{mode}/{valeur}];
  • рядок 26: запущено сервер реєстрації Arduino;

Підключіть Arduino до PC, якщо ви цього ще не зробили. Брандмауер PC має бути вимкнений. Потім за допомогою браузера зверніться до URL [http://localhost:8080/arduinos]:

Ви повинні побачити ідентифікатор підключеного Arduino. Якщо нічого не відображається, спробуйте перезапустити Arduino. Для цього на ньому є кнопка.

Веб-сервер / jSON тепер встановлено.

5.4.2. URL, що надаються веб-сервісом / jSON

До уваги: проект [Exemple-15] (див. параграф 1.16.1);

Веб-сервіс / jSON було реалізовано за допомогою Spring MVC і він надає доступ до таких URL:


@Controller
public class WebController {

  // бізнес-шар
  @Autowired
  private IMetier métier;

  // перелік Arduino
  @RequestMapping(value = "/arduinos", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
  @ResponseBody
  public String getArduinos() throws JsonProcessingException {
    ...
  }

  // блимання
  @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 {
...
  }

  // відправлення команд 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 {
    ...
  }

  // зчитування виводу
  @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 {
    ....
  }

  // запис даних з виводу
  @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 {
  ...
  }
}

Відповіді, що надсилаються сервером, є представленнями jSON наступного класу [Response<T>]:


package client.android.dao.service;

import java.util.List;

public class Response<T> {

    // ----------------- властивості
    // статус операції
    private int status;
    // можливі повідомлення про стан
    private List<String> messages;
    // тіло відповіді
    private T body;

    // конструктори
    public Response() {

    }

    public Response(int status, List<String> messages, T body) {
        this.status = status;
        this.messages = messages;
        this.body = body;
    }

    // методи отримання та встановлення значень
...
}

URL [/arduinos] надсилає відповідь типу [Response<List<Arduino>>], де [Arduino] є наступним класом:


package android.arduinos.entities;

import java.io.Serializable;

public class Arduino implements Serializable {
  // дані
  private String id;
  private String description;
  private String mac;
  private String ip;
  private int port;

// методи getter та setter
...
}
  • рядок 7: [id] — ідентифікатор Arduino;
  • рядок 8: його опис;
  • рядок 9: його адреса MAC;
  • рядок 10: його адреса IP;
  • рядок 11: порт, на якому він очікує команд;

URL:

  • [/arduinos/blink/{idCommande}/{idArduino}/{pin}/{duree}/{nombre}];
  • [/arduinos/pinRead/{idCommande}/{idArduino}/{pin}/{mode}];
  • [/arduinos/pinWrite/{idCommande}/{idArduino}/{pin}/{mode}/{valeur}];
  • [/arduinos/commands/{idArduino}];

надсилають відповідь типу [Response<ArduinoResponse>], де клас [ArduinoResponse] представляє стандартну відповідь Arduino:


public class ArduinoResponse implements Serializable {
  
  private String json;
  private String id;
  private String erreur;
  private Map<String, Object> etat;

  // методи getter та setter
...
}
  • [json]: рядок jSON, надісланий Arduino, який не вдалося декодувати (випадок помилки), інакше — null;
  • [id]: ідентифікатор команди, на яку відповідає Arduino;
  • [erreur]: код помилки, 0, якщо OK, інакше — інше значення;
  • [etat]: словник, що містить конкретну відповідь на команду. Найчастіше він порожній, за винятком випадків, коли команда вимагала зчитування значення з Arduino — тоді це значення буде розміщено в цьому словнику;

5.4.3. Тестування веб-сервісу / jSON

Ознайомтеся з веб-сервером / jSON, протестувавши такі URL:

URL
rôle
http://localhost:8080/arduinos/
rend la liste des Arduinos connectés
http://localhost:8080/arduinos/
blink/1/cuisine/8/100/20/
fait clignoter la led de la pin n° 8
 de l'Arduino identifié par cuisine,
 20 fois toutes les 100 ms.
http://localhost:8080/arduinos/
pinRead/1/cuisine/0/a/
lecture analogique de la pin n° 0 de
 l'Arduino identifié par cuisine
http://localhost:8080/arduinos/
pinRead/1/cuisine/5/b/
lecture binaire de la pin n° 5 de
 l'Arduino identifié par cuisine
http://localhost:8080/arduinos/
pinWrite/1/cuisine/8/b/1/
écriture binaire de la valeur 1 sur la pin n° 8 de l'Arduino identifié par
 cuisine
http://localhost:8080/arduinos/
pinWrite/1/cuisine/4/a/100/
écriture analogique de la valeur 100 sur la pin n° 4 de l'Arduino identifié
 par cuisine

Ось кілька скріншотів того, що ви повинні отримати:

Отримати список підключених Arduino:

Строка jSON, отримана від веб-сервера / jSON, є об’єктом із такими полями:

  • [status]: значення 0 означає, що помилки не було — в іншому випадку помилка сталася;
  • [messages]: список повідомлень, що пояснюють помилку, якщо вона сталася:
  • [body]: список Arduino, якщо помилки не сталося. Кожен Arduino описується об’єктом із такими полями:
    • [id]: ідентифікатор Arduino. Два Arduino не можуть мати однаковий ідентифікатор;
    • [description]: короткий опис функціональних можливостей Arduino;
    • [mac]: MAC-адреса Arduino;
    • [ip]: адреса Arduino IP;
    • [port]: порт, на якому він очікує команд;

Змусити блимати світлодіод на виводі № 8 Arduino, ідентифікованого за [cuisine], 20 разів кожні 100 мс:

 

Строка jSON, отримана від веб-сервера / jSON, є об’єктом із такими полями:

  • [status]: значення 0 означає, що помилки не було — в іншому випадку помилка сталася;
  • [messages]: список повідомлень, що пояснюють причину помилки, якщо вона сталася:
  • [body]: відповідь Arduino, якщо помилки не сталося:
    • [id]: ідентифікатор команди. Цей ідентифікатор — це цифра 1 у [/blink/1]. Arduino повторює цей ідентифікатор команди у своїй відповіді;
    • [erreur]: номер помилки. Значення, відмінне від 0, вказує на наявність помилки;
    • [etat]: використовується лише для зчитування значення виводу. У цьому випадку його значенням є значення виводу;
    • [json]: використовується лише у разі помилки jSON між клієнтом і сервером. У цьому випадку його значенням є помилковий рядок jSON, надісланий Arduino;

Аналогове зчитування виводу № 0 Arduino, ідентифікованого за [cuisine]:

 

Рядок jSON, отриманий від веб-сервера / jSON, аналогічний попередньому, за винятком поля [etat], яке представляє значення виводу № 0.

Бінарне зчитування виводу № 5 Arduino, ідентифікованого за [cuisine]:

 

Рядок jSON, отриманий від веб-сервера / jSON, аналогічний попередньому.

Бінарне записування значення 1 на вивід № 8 Arduino, ідентифікованого як [cuisine]:

 

Рядок jSON, отриманий від веб-сервера / jSON, аналогічний попередньому.

Тестування URL та [http://localhost:8080/arduinos/commands/cuisine] є складнішим. Метод веб-сервера / jSON, який обробляє цей URL, очікує запит POST, який неможливо просто імітувати за допомогою браузера. Щоб протестувати цей URL, можна використати браузер Chrome із розширенням [Advanced REST Client] (див. параграф 6.13):

 
  • у [1] — URL методу web / jSON, що тестується;
  • у [2] — метод POST для відправлення запиту;
  • у [3-4] значення, відправлене методом POST, — це jSON;
  • у [5] — відправлено рядок jSON. Зверніть увагу на дужки, що починають і закінчують список. Тут у списку є лише одна команда jSON, яка змушує блимати контакт № 8 10 разів кожні 100 мс;
  • у [6] надсилається запит;
 
  • у [7] — відповідь jSON, надіслана сервером. Об'єкт отримав об'єкт із двома звичними полями [status, messages] та полем [body], значенням якого є список відповідей Arduino на кожну з надісланих команд jSON.

Давайте подивимося, що відбувається, коли ми надсилаємо команду jSON, яка є синтаксично некоректною для Arduino:

У відповідь ми отримуємо таке:

 

Бачимо, що у відповіді Arduino номер помилки — [104], що вказує на те, що команда [xx] не була розпізнана.

5.5. Тестування клієнта для Android

Ось готовий виконуваний файл клієнта для Android:

  

Перетягніть мишкою вищевказаний файл [app-debug.apk] на емулятор планшета [GenyMotion]. Він буде збережений, а потім запущений. Також запустіть веб-сервер / jSON, якщо ви цього ще не зробили. Підключіть Arduino до PC із світлодіодом на ньому. Клієнт для Android дозволяє дистанційно керувати Arduino. Він відображає користувачеві такі екрани.

Вкладка [CONFIG] дозволяє підключитися до сервера та отримати список підключених Arduino:

Image

  • у полі [1] введіть адресу IP [192.168.2.1], надану вашому PC (див. розділ 5.2).

Вкладка [PINWRITE] дозволяє записати значення на вивід Arduino:

Image

Image

Вкладка [PINREAD] дозволяє зчитати значення з виводу Arduino:

Image

Вкладка [BLINK] дозволяє змусити блимати світлодіод Arduino:

Image

Вкладка [COMMAND] дозволяє надіслати команду jSON на Arduino:

Image

5.6. Android-клієнт веб-сервісу / jSON

Тепер перейдемо до написання клієнта для Android.

5.6.1. Архітектура клієнта

Архітектура клієнта для Android відповідатиме архітектурі проєкту [Exemple-15] (див. розділ 1.16.2);

  • шар [DAO] взаємодіє з веб-сервером / jSON;

Клієнт для Android повинен мати можливість керувати кількома Arduino одночасно. Наприклад, ми хочемо, щоб два світлодіоди, розміщені на двох Arduino, блимали одночасно, а не по черзі. Тому наш клієнт для Android використовуватиме асинхронне завдання для кожного Arduino, і ці завдання виконуватимуться паралельно.

5.6.2. Проєкт клієнта в Android Studio

Скопіюйте проєкт [client-android-skel] (див. параграф 2) у проєкт [client-arduinos-01] (за потреби перегляньте інструкцію щодо копіювання проєкту Gradle у параграфі 1.15):

Image

5.6.3. П’ять подання XML

  

Буде п’ять подань XML:

  • [blink]: для мигання світлодіода на Arduino. Він пов’язаний із фрагментом [BlinkFragment];
  • [commands]: для надсилання команди jSON на Arduino. Він пов’язаний із фрагментом [CommandsFragment];
  • [config]: для налаштування URL веб-сервісу / jSON та отримання початкового списку підключених Arduino. Вона пов’язана з фрагментом [ConfigFragment];
  • [pinread]: для зчитування двійкового або аналогового значення з виводу Arduino. Пов’язана з фрагментом [PinReadFragment];
  • [pinwrite]: для запису двійкового або аналогового значення на вивід Arduino. Він пов'язаний із фрагментом [PinWriteFragment];

Наразі ці п’ять переглядів XML матимуть однаковий порожній вміст:


<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
            android:id="@+id/scrollView1"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content">

  <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
                  android:layout_width="match_parent"
                  android:layout_height="match_parent">
  </RelativeLayout>
</ScrollView>
  • вигляд знаходиться в контейнері [RelativeLayout] (рядки 7–10), який, у свою чергу, входить до контейнера [ScrollView] (рядки 2–11). Це гарантує можливість прокручувати вигляд, якщо його розмір перевищує розмір екрану планшета;

Завдання: створіть п’ять подань XML.


5.6.4. Меню фрагментів

Ми знаємо, що фрагменти проекту, створеного за допомогою [client-android-skel], повинні бути пов’язані з меню, навіть порожнім. У цьому випадку додаток не матиме меню. Порожнє меню вже є в проекті;

  

5.6.5. П’ять фрагментів додатка

 

Завдання: скопіюйте фрагмент [DummyFragment] у п’ять фрагментів додатка, як показано в [2].


Фрагмент [ConfigFragment] має таку структуру:


package client.android.fragments.behavior;

import client.android.R;
import client.android.architecture.core.AbstractFragment;
import client.android.architecture.custom.CoreState;
import client.android.fragments.state.DummyFragmentState;
import org.androidannotations.annotations.EFragment;
import org.androidannotations.annotations.OptionsMenu;

@EFragment
@OptionsMenu(R.menu.menu_vide)
public class ConfigFragment extends AbstractFragment {

  // поля, успадковані від батьківського класу -------------------------------------------------------
...

Замініть рядок 10 на такий:


@EFragment(R.layout.config)

Завдання: зробіть те саме для інших чотирьох фрагментів, адаптувавши атрибут [@EFragment] класу.


Фрагмент
Вигляд
ConfigFragment

R.layout.config
PinReadFragment

R.layout.pinread
PinWriteFragment

R.layout.pinwrite
CommandsFragment

R.layout.commands
BlinkFragment

R.layout.blink

5.6.6. Стани фрагментів

Кожен фрагмент матиме свій стан.


Завдання: скопіюйте клас [DummyFragmentState] п’ять разів, щоб створити п’ять станів, представлених у [2].


5.6.7. Налаштування проекту

 

Пакет [architecture / custom] містить елементи архітектури додатка, які можна налаштувати.

5.6.7.1. Інтерфейс [IMainActivity]

Інтерфейс [IMainActivity] визначає, що фрагменти можуть запитувати в активності, а також константи додатка. Цей інтерфейс матиме такий вигляд:


package client.android.architecture.custom;

import client.android.architecture.core.ISession;
import client.android.dao.service.IDao;

public interface IMainActivity extends IDao {

  // доступ до сесії
  ISession getSession();

  // зміна виду
  void navigateToView(int position, ISession.Action action);

  // управління очікуванням
  void beginWaiting();

  void cancelWaiting();

  // константи додатка -------------------------------------

  // режим налагодження
  boolean IS_DEBUG_ENABLED = true;

  // максимальний час очікування відповіді від сервера
  int TIMEOUT = 1000;

  // час очікування перед виконанням клієнтського запиту
  int DELAY = 000;

  // базова автентифікація
  boolean IS_BASIC_AUTHENTIFICATION_NEEDED = false;

  // суміжність фрагментів
  int OFF_SCREEN_PAGE_LIMIT = 1;

  // панель вкладок
  boolean ARE_TABS_NEEDED = true;

  // зображення очікування
  boolean IS_WAITING_ICON_NEEDED = true;

  // кількість фрагментів
  int FRAGMENTS_COUNT = 5;

  // кількість переглядів
  int VUE_CONFIG = 0;
  int VUE_BLINK = 1;
  int VUE_PINREAD = 2;
  int VUE_PINWRITE = 3;
  int VUE_COMMANDS = 4;
}
  • рядки 25, 28, 31, 40: конфігурація шару [DAO]. Цей додаток звертається до веб-сервера / jSON;
  • рядок 37: цей додаток має вкладки;
  • рядок 43: цей додаток має п’ять фрагментів;
  • рядки 46–50: номери п’яти фрагментів;
  • рядок 34: суміжність фрагментів. Розробник може вказати тут значення в діапазоні [1, FRAGMENTS_COUNT-1];

5.6.7.2. Клас [CoreState]

Клас [CoreState] є батьківським класом для станів фрагментів:


package client.android.architecture.custom;

import client.android.architecture.core.MenuItemState;
import client.android.fragments.state.*;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;

@JsonIgnoreProperties(ignoreUnknown = true)
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY)
@JsonSubTypes({
  @JsonSubTypes.Type(value = ConfigFragmentState.class),
  @JsonSubTypes.Type(value = BlinkFragmentState.class),
  @JsonSubTypes.Type(value = PinReadFragmentState.class),
  @JsonSubTypes.Type(value = PinWriteFragmentState.class),
  @JsonSubTypes.Type(value = CommandsFragmentState.class)}
)
public class CoreState {
  // чи відвідано фрагмент
  protected boolean hasBeenVisited = false;
  // стан можливого меню фрагмента
  protected MenuItemState[] menuOptionsState;

  // гетери та сеттери
...
}
  • рядки 12–16: тут слід оголосити класи станів п’яти фрагментів;

5.6.8. Клас [MainActivity]

  

Клас [MainActivity] матиме такий вигляд:


package client.android.activity;

import android.support.design.widget.TabLayout;
import android.util.Log;
import client.android.R;
import client.android.architecture.core.AbstractActivity;
import client.android.architecture.core.AbstractFragment;
import client.android.architecture.core.ISession;
import client.android.architecture.custom.IMainActivity;
import client.android.architecture.custom.Session;
import client.android.dao.entities.Arduino;
import client.android.dao.entities.ArduinoCommand;
import client.android.dao.entities.ArduinoResponse;
import client.android.dao.service.Dao;
import client.android.dao.service.IDao;
import client.android.dao.service.Response;
import client.android.fragments.behavior.*;
import org.androidannotations.annotations.Bean;
import org.androidannotations.annotations.EActivity;
import org.androidannotations.annotations.OptionsMenu;
import rx.Observable;

import java.util.List;
import java.util.Locale;

@EActivity
@OptionsMenu(R.menu.menu_main)
public class MainActivity extends AbstractActivity {

  // шар [DAO]
  @Bean(Dao.class)
  protected IDao dao;
  // сесія
  private Session session;

  // методи батьківського класу -----------------------
  @Override
  protected void onCreateActivity() {
    // журнал
    if (IS_DEBUG_ENABLED) {
      Log.d(className, "onCreateActivity");
    }
    // сесія
    this.session = (Session) super.session;
    // створення п'яти вкладок
    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) {
    // відображення фрагмента № позиція
    navigateToView(position, ISession.Action.NAVIGATION);
  }

  @Override
  protected int getFirstView() {
    return IMainActivity.VUE_CONFIG;
  }

  // реалізація IDao -----------------------------------------
}
  • рядки 46–50: створення п’яти вкладок додатка;
  • рядок 48: назви вкладок задаються методом, описаним у рядках 63–79;
  • п’ять фрагментів інстанціюються у рядку 60. Завдяки анотаціям AA класи фрагментів відповідають наведеним вище, але мають суфікс у вигляді підкреслення;
  • рядки 63–79: для кожного фрагмента визначається заголовок. Ці заголовки будуть шукатися у файлі [res / values / strings.xml]
  

Вміст файлу [strings.xml] такий:


<?xml version="1.0" encoding="utf-8"?>
<resources>

  <!-- назва програми -->
  <string name="app_name">[arduinos-client-01]</string>
  <!-- Фрагменти та вкладки -->
  <string name="config_titre">[Config]</string>
  <string name="blink_titre">[Blink]</string>
  <string name="pinread_titre">[PinRead]</string>
  <string name="pinwrite_titre">[PinWrite]</string>
  <string name="commands_titre">[Commands]</string>

</resources>

Завдання: створіть наведені вище елементи та скомпілюйте проект. Помилок бути не повинно.


Запустіть проект. На емуляторі має з’явитися таке вікно:

Image

Перегляньте журнали, що супроводжували відображення першого екрану, і простежте за виконаними етапами. Переходьте з однієї вкладки на іншу та продовжуйте стежити за журналами.

5.6.9. Вигляд XML [config]

Вигляд XML [config] буде таким:

Вищезазначений вигляд отримується за допомогою такого коду XML:


<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
            android:id="@+id/scrollView1"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content">

  <RelativeLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <TextView
      android:id="@+id/txt_TitreConfig"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:layout_alignParentTop="true"
      android:layout_centerHorizontal="true"
      android:layout_marginTop="150dp"
      android:text="@string/txt_TitreConfig"
      android:textSize="@dimen/titre"/>

    <TextView
      android:id="@+id/txt_UrlServiceRest"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:layout_alignParentLeft="true"
      android:layout_below="@+id/txt_TitreConfig"
      android:layout_marginTop="50dp"
      android:text="@string/txt_UrlServiceRest"
      android:textSize="20sp"/>

    <EditText
      android:id="@+id/edt_UrlServiceRest"
      android:layout_width="300dp"
      android:layout_height="wrap_content"
      android:layout_alignBaseline="@+id/txt_UrlServiceRest"
      android:layout_alignBottom="@+id/txt_UrlServiceRest"
      android:layout_marginLeft="20dp"
      android:layout_toRightOf="@+id/txt_UrlServiceRest"
      android:ems="10"
      android:hint="@string/hint_UrlServiceRest"
      android:inputType="textUri">

      <requestFocus/>
    </EditText>

    <TextView
      android:id="@+id/txt_MsgErreurIpPort"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:layout_alignParentLeft="true"
      android:layout_below="@+id/txt_UrlServiceRest"
      android:layout_marginTop="20dp"
      android:text="@string/txt_MsgErreurUrlServiceRest"
      android:textColor="@color/red"
      android:textSize="20sp"/>

    <TextView
      android:id="@+id/txt_arduinos"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:layout_alignParentLeft="true"
      android:layout_below="@+id/txt_MsgErreurIpPort"
      android:layout_marginTop="40dp"
      android:text="@string/titre_list_arduinos"
      android:textColor="@color/blue"
      android:textSize="20sp"/>

    <Button
      android:id="@+id/btn_Rafraichir"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:layout_alignBaseline="@+id/txt_arduinos"
      android:layout_alignBottom="@+id/txt_arduinos"
      android:layout_marginLeft="20dp"
      android:layout_toRightOf="@+id/txt_arduinos"
      android:text="@string/btn_rafraichir"/>

    <Button
      android:id="@+id/btn_Annuler"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:layout_alignBaseline="@+id/txt_arduinos"
      android:layout_alignBottom="@+id/txt_arduinos"
      android:layout_marginLeft="20dp"
      android:layout_toRightOf="@+id/txt_arduinos"
      android:text="@string/btn_annuler"
      android:visibility="invisible"/>

    <ListView
      android:id="@+id/ListViewArduinos"
      android:layout_width="match_parent"
      android:layout_height="200dp"
      android:layout_alignParentLeft="true"
      android:layout_below="@+id/txt_arduinos"
      android:layout_marginTop="30dp"
      android:background="@color/wheat">
    </ListView>

  </RelativeLayout>
</ScrollView>

У цьому поданні використовуються символьні рядки (android:text у рядках 15, 25, 37, 50, 61, 73), які визначені у файлі [res / values / strings]:

  

<?xml version="1.0" encoding="utf-8"?>
<resources>

    <string name="app_name">android-domotique</string>

    <!-- Фрагменти та вкладки -->
    <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>

    <!-- Налаштування -->
    <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>

У цьому вікні використовуються кольори (android:textColor у рядках 51 та 62), визначені у файлі [res / values / colors]:

  

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

Вигляд використовує розміри (android:textSize у рядку 16), які визначені у файлі [res / values / dimens]:

  

<resources>
  <!-- Стандартні поля екрана відповідно до рекомендацій щодо дизайну Android. -->
  <dimen name="activity_horizontal_margin">16dp</dimen>
  <dimen name="activity_vertical_margin">16dp</dimen>
  <dimen name="fab_margin">16dp</dimen>
  <dimen name="appbar_padding_top">8dp</dimen>
  <!-- додаток -->
  <dimen name="titre">30dp</dimen>
</resources>

Цей метод не використовувався для всіх розмірів. Однак саме його рекомендується застосовувати. Він дозволяє змінювати розміри в одному місці.


Завдання: створіть наведені вище елементи.


Знову запустіть свій проєкт. Ви повинні отримати такий вигляд:

Image

5.6.10. Фрагмент [ConfigFragment]

  

Щоб обробити новий вигляд [config], код фрагмента [ConfigFragment] змінюється наступним чином:


package client.android.fragments.behavior;

import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ListView;
import android.widget.TextView;
import client.android.R;
import client.android.architecture.core.AbstractFragment;
import client.android.architecture.custom.CoreState;
import client.android.architecture.custom.IMainActivity;
import client.android.fragments.state.ConfigFragmentState;
import org.androidannotations.annotations.Click;
import org.androidannotations.annotations.EFragment;
import org.androidannotations.annotations.OptionsMenu;
import org.androidannotations.annotations.ViewById;

@EFragment(R.layout.config)
@OptionsMenu(R.menu.menu_vide)
public class ConfigFragment extends AbstractFragment {

  // елементи візуального інтерфейсу
  @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() {
  }

  // Управління життєвим циклом фрагмента -------------------------------------

  @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) {
    // Перше відвідування?
    if(previousState==null){
      txtMsgErreurUrlServiceRest.setVisibility(View.INVISIBLE);
    }
  }

  @Override
  protected void updateOnSubmit(CoreState previousState) {

  }

  @Override
  protected void updateOnRestore(CoreState previousState) {
  }

  @Override
  protected void notifyEndOfUpdates() {
    // кнопки
    initButtons();
  }

  @Override
  protected void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled) {
  }

  // приватні методи --------------------------------------------

  private void initButtons() {
    // кнопка [Exécuter] замінює кнопку [Annuler]
    btnAnnuler.setVisibility(View.INVISIBLE);
    btnRafraichir.setVisibility(View.VISIBLE);
  }
}
  • рядки 23–32: елементи візуального інтерфейсу;
  • рядки 58–60: під час першого відвідування фрагмента повідомлення про помилку приховано;
  • рядки 73–76: щоразу, коли фрагмент відображатиметься, кнопка [Annuler] приховуватиметься (рядок 82), а кнопка [Rafraîchir] відображатиметься (рядки 86–87). Адже в цьому додатку фрагмент не може відображатися, поки триває асинхронна операція, а отже, кнопка [Annuler] залишається видимою;

Завдання: створіть наведені вище елементи.


Запустіть цю нову версію. Перший вигляд тепер має бути таким:

Image

5.6.10.1. Кнопка [Rafraîchir]

Наразі ми будемо обробляти натискання кнопки [Rafraîchir] наступним чином:


@Click(R.id.btn_Rafraichir)
  protected void doRafraichir() {
    // запускаємо завдання — готуємося до очікування
    beginWaiting(1);
  }

  @Click(R.id.btn_Annuler)
  protected void doAnnuler() {
    if (isDebugEnabled) {
      Log.d(className, "Annulation demandée");
    }
    // скасовуємо асинхронні завдання
    cancelRunningTasks();
  }

  protected void beginWaiting(int numberOfRunningTasks) {
    // готуємося до очікування завдань
    beginRunningTasks(numberOfRunningTasks);
    // кнопка [Annuler] замінює кнопку [Rafraîchir]
    btnRafraichir.setVisibility(View.INVISIBLE);
    btnAnnuler.setVisibility(View.VISIBLE);
}
  // управління життєвим циклом фрагмента -------------------------------------
...
  @Override
  protected void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled) {
    // кнопки у початковому стані
    initButtons();
  }

  // приватні методи --------------------------------------------

  private void initButtons() {
    // кнопка [Exécuter] замінює кнопку [Annuler]
    btnAnnuler.setVisibility(View.INVISIBLE);
    btnRafraichir.setVisibility(View.VISIBLE);
  }
  • рядки 1–5: метод, що виконується при натисканні кнопки [Rafraîchir];
  • рядок 4: починаємо очікування;
  • рядок 18: передаємо батьківському класу кількість асинхронних завдань, які будемо запускати. З’явиться зображення очікування;
  • рядки 20–21: це очікування призведе до появи кнопки [Annuler], зникнення кнопки [Rafraîchir] та появи зображення очікування. Більше нічого не відбувається. Однак користувач може натиснути кнопку [Annuler]. Тоді виконається метод у рядках 7–14;
  • рядок 13: батьківський клас отримує запит на скасування всіх завдань. Клас виконає це і, у свою чергу, викличе метод у рядках 25–29, щоб повідомити про завершення всіх завдань. Параметр [runningTasksHaveBeenCanceled] матиме значення true, що вказує на скасування завдань;
  • рядки 35–36: кнопка [Annuler] зникне, а кнопка [Rafraîchir] знову з’явиться.

Завдання: Внесіть ці зміни, а потім запустіть проект. Перевірте, чи кнопка [Rafraîchir] запускає режим очікування, а кнопка [Annuler] — зупиняє його. Проаналізуйте журнали.


5.6.10.2. Перевірка введених даних

У попередній версії ми не перевіряли правильність введених даних. Щоб це зробити, додаємо такий код у файл [ConfigFragment]:


// введені значення
  private String urlServiceRest;

  @Click(R.id.btn_Rafraichir)
  protected void doRafraichir() {
    // перевіряються введені дані
    if (!pageValid()) {
      return;
    }
    // запускається завдання — готується режим очікування
    beginWaiting(1);
  }

  // перевірка введених даних
  private boolean pageValid() {
    // спочатку немає повідомлення про помилку
    txtMsgErreurUrlServiceRest.setVisibility(View.INVISIBLE);
    // отримано IP-адресу та порт сервера
    urlServiceRest = String.format("http://%s", edtUrlServiceRest.getText().toString().trim());
    // перевіряємо їхню дійсність
    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) {
      // виведення повідомлення про помилку
      txtMsgErreurUrlServiceRest.setVisibility(View.VISIBLE);
      // повернення до UI
      return false;
    }
    // все гаразд
    return true;
  }
  • рядок 2: введене значення URL;
  • рядки 7–9: перш ніж робити що-небудь, перевіряємо правильність введених даних;
  • рядок 19: отримуємо введене значення URL і додаємо до нього префікс [http://];
  • рядок 22: намагаємося створити об’єкт URI (Uniform Resource Identifier) на його основі. Якщо введений URL є синтаксично некоректним, виникне виняток;
  • рядки 23–27: генерується виняток, якщо URI є правильним, але при цьому присутні [host==null] та [port==-1]. Це можливий випадок;
  • рядок 30: сталося виключення. Виводиться повідомлення про помилку;
  • рядок 32: повертається [false], щоб вказати, що сторінка є недійсною;
  • рядок 35: помилок не сталося. Повертається [true], щоб вказати, що сторінка є дійсною;

Завдання: створіть наведені вище елементи.


Випробуйте цю нову версію та переконайтеся, що недійсні URL правильно позначаються.

5.6.10.3. Відображення списку пристроїв Arduino

  

Різні подання потребуватимуть відображення списку підключених модулів Arduino. Для цього ми визначимо різні класи та подання XML:

  • Arduino буде представлений класом [Arduino] [1];
  • клас [CheckedArduino] [1] успадковується від класу [Arduino], до якого додано булеве значення для визначення, чи було Arduino вибрано у списку;

Клас [Arduino] — це той самий клас, який уже використовується сервером і представлений у розділі 5.4.2. Він має такий вигляд:


package android.arduinos.entities;

import java.io.Serializable;

public class Arduino implements Serializable {
  // дані
  private String id;
  private String description;
  private String mac;
  private String ip;
  private int port;

// гетери та сеттери
...
}
  • рядок 7: [id] — ідентифікатор Arduino;
  • рядок 8: його опис;
  • рядок 9: його адреса MAC;
  • рядок 10: його адреса IP;
  • рядок 11: порт, на якому він очікує команд;

Цей клас відповідає рядку jSON, отриманому від сервера при запиті списку підключених Arduino:

Клас [CheckedArduino] успадковує клас [Arduino]:


package android.arduinos.entities;

public class CheckedArduino extends Arduino {
    private static final long serialVersionUID = 1L;
    // можна вибрати Arduino
    private boolean isChecked;

    // конструктор
    public CheckedArduino(Arduino arduino, boolean isChecked) {
        // батьківський об’єкт
        super(arduino.getId(), arduino.getDescription(), arduino.getMac(), arduino.getIp(), arduino.getPort());
        // локальний
        this.isChecked = isChecked;
    }

    // геттери та сеттери
    public boolean isChecked() {
        return isChecked;
    }

    public void setChecked(boolean isChecked) {
        this.isChecked = isChecked;
    }

}
  • рядок 3: клас [CheckedArduino] успадковує клас [Arduino];
  • рядок 6: до нього додається логічна змінна, яка допоможе нам визначити, чи було вибрано якийсь Arduino у відображеному списку Arduino;

У [ConfigFragment] ми будемо імітувати отримання списку підключених Arduino.

  

  @ViewById(R.id.ListViewArduinos)
  protected ListView listArduinos;
..
  @Click(R.id.btn_Rafraichir)
  protected void doRafraichir() {
    // перевіряємо введені дані
    if (!pageValid()) {
      return;
    }
    // запускаємо завдання — готуємося до очікування
    beginWaiting(1);
    // очищення списку Arduino
    clearArduinos();
    // запит списку Arduino у фоновому режимі
    getArduinosInBackground();
  }

  private void getArduinosInBackground() {
   ...
  }

  // очищення списку Arduino
  private void clearArduinos() {
    // створюємо порожній список
    List<String> strings = new ArrayList<>();
    // виводимо список
    listArduinos.setAdapter(new ArrayAdapter<String>(activity, android.R.layout.simple_list_item_1, android.R.id.text1, strings));
}
  • рядок 2: ListView, що відображає Arduino, підключені до сервера;
  • рядок 5: метод, який запитує список підключених Arduino;
  • рядок 11: повідомляємо батьківському класу, що збираємося запустити асинхронне завдання;
  • рядок 12: очищається список підключених Arduino, що відображається в даний момент;
  • рядок 15: у фоновому режимі запитується список підключених модулів Arduino;
  • рядки 23–28: метод, який очищає список Arduino, що наразі відображається;

Метод [getArduinosInBackground] має такий вигляд:


  private void getArduinosInBackground() {
    // створюється фіктивний список Arduino
    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));
    }
    // імітуємо відповідь сервера
    Response<List<Arduino>> response = new Response<>();
    response.setBody(arduinos);
    // скасовується очікування
    cancelWaitingTasks();
    // змінюємо кнопки
    initButtons();
    // обробляємо відповідь
    consumeArduinosResponse(response);
}
  • рядки 3–6: створюється список із 20 модулів Arduino;
  • рядки 8–9: формується відповідь типу [Response<List<Arduino>>] (параграф 5.4.2), яка буде містити створений список Arduino;
  • рядок 11: скасовується очікування;
  • рядок 13: повертаємо кнопки у початковий стан;
  • рядок 15: обробляємо відповідь;

Метод [consumeArduinosResponse] виглядає наступним чином:


  // відображення відповіді
  private void consumeArduinosResponse(Response<List<Arduino>> response) {
    // помилка?
    if (response.getStatus() != 0) {
      // відображення
      showAlert(response.getMessages());
      // повернення до інтерфейсу
      return;
    }
    // створюється список [CheckedArduino]
    List<CheckedArduino> checkedArduinos = new ArrayList<>();
    for (Arduino arduino : response.getBody()) {
      checkedArduinos.add(new CheckedArduino(arduino, false));
    }
    // вони відображаються
    showArduinos(checkedArduinos);
}
  • рядки 4–11: перевіряємо код помилки у відповіді, надісланій сервером:
  • рядок 4: якщо код помилки відмінний від нуля;
  • рядок 6: виводимо повідомлення, збережені сервером у полі [messages] відповіді;
  • рядок 8: повертаємося до інтерфейсу користувача;
  • рядки 11–16: якщо помилок не було, виводимо отриманий список Arduino, попередньо перетворивши його на тип List<CheckedArduino>;

Метод [showArduinos] виглядає наступним чином:


  private void showArduinos(List<CheckedArduino> checkedArduinos) {
    // створюємо список рядків на основі списку Arduino
    List<String> strings = new ArrayList<>();
    for (CheckedArduino checkedArduino : checkedArduinos) {
      strings.add(checkedArduino.toString());
    }
    // виводимо його
    listArduinos.setAdapter(new ArrayAdapter<>(activity, android.R.layout.simple_list_item_1, android.R.id.text1, strings));
}

Завдання: внесіть зазначені вище зміни та запустіть свій проєкт.


Після натискання кнопки [Rafraîchir] ви повинні побачити таке вікно:

Image

Значення [1] не використовується. Тому ви можете ввести будь-що, якщо це відповідає очікуваному формату.

5.6.10.4. Шаблон для відображення Arduino

Наразі підключені Arduino відображаються у вікні [Config] таким чином:

Image

Тепер ми хочемо відображати їх так:

Image

  • у [1] — прапорець, який дозволить вибрати Arduino. Цей прапорець буде приховано, коли потрібно відобразити список Arduino, які не можна вибрати;
  • у [2] — ідентифікатор Arduino;
  • у [3] — його опис;

Далі наведено концепції, розроблені в проектах [exemple-19] та [exemple-19B] з розділу 1.20. За необхідності перегляньте їх.

Спочатку створимо представлення, яке відображатиме елемент зі списку Arduino:

 

Код вищезазначеного виду [listarduinos_item] такий:


<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/RelativeLayout1"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@color/wheat"
    android:orientation="vertical" >

    <CheckBox
        android:id="@+id/checkBoxArduino"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentLeft="true"
        android:layout_alignParentTop="true"
        android:layout_toRightOf="@+id/txt_arduino_description" />

    <TextView
        android:id="@+id/TextView1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignBaseline="@+id/checkBoxArduino"
        android:layout_marginLeft="40dp"
        android:text="@string/txt_arduino_id" />

    <TextView
        android:id="@+id/txt_arduino_id"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignBaseline="@+id/checkBoxArduino"
        android:layout_alignParentTop="true"
        android:layout_toRightOf="@+id/TextView1"
        android:text="@string/dummy"
        android:textColor="@color/blue" />

    <TextView
        android:id="@+id/TextView2"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignBaseline="@+id/checkBoxArduino"
        android:layout_alignParentTop="true"
        android:layout_marginLeft="20dp"
        android:layout_toRightOf="@+id/txt_arduino_id"
        android:text="@string/txt_arduino_description" />

    <TextView
        android:id="@+id/txt_arduino_description"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignBaseline="@+id/checkBoxArduino"
        android:layout_alignTop="@+id/TextView2"
        android:layout_toRightOf="@+id/TextView2"
        android:text="@string/dummy"
        android:textColor="@color/blue" />

</RelativeLayout>
  • рядки 9–15: прапорець;
  • рядки 17–23: текст [Id : ];
  • рядки 25–33: тут буде вказано ідентифікатор Arduino;
  • рядки 35–43: текст [Description : ];
  • рядки 45–53: тут буде вказано опис Arduino;

Цей вигляд використовує тексти (рядки 23, 32, 43), визначені в [res / values / strings.xml]:


    <string name="dummy">XXXXX</string>

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

Вигляд також використовує колір (рядки 33, 53), визначений у [res / values / colors.xml]:


<?xml version="1.0" encoding="utf-8"?>
<resources>

    <color name="red">#FF0000</color>
    <color name="blue">#0000FF</color>
    <color name="wheat">#FFEFD5</color>
    <color name="floral_white">#FFFAF0</color>

</resources>

Менеджер відображення елемента зі списку Arduino

  

Клас [ListArduinosAdapter] — це клас, який викликається класом [ListView] для відображення кожного елемента списку Arduino. Його код такий:


package istia.st.android.vues;

import istia.st.android.R;
...

public class ListArduinosAdapter extends ArrayAdapter<CheckedArduino> {

    // таблиця Arduino
    private List<CheckedArduino> arduinos;
    // контекст виконання
    private Context context;
    // ідентифікатор макета відображення рядка у списку Arduino
    private int layoutResourceId;
    // чи містить рядок прапорець
    private Boolean selectable;

    // конструктор
    public ListArduinosAdapter(Context context, int layoutResourceId, List<CheckedArduino> arduinos, Boolean selectable) {
        // батьківський елемент
        super(context, layoutResourceId, arduinos);
        // інформація зберігається
        this.arduinos = arduinos;
        this.context = context;
        this.layoutResourceId = layoutResourceId;
        this.selectable = selectable;
    }

    @Override
    public View getView(final int position, View convertView, ViewGroup parent) {
...
    }
}
  • рядок 18: конструктор класу приймає чотири параметри: активність, що виконується, ідентифікатор виду, який потрібно відобразити для кожного елемента джерела даних, джерело даних, що наповнює список, та логічне значення, яке вказує, чи слід відображати прапорець, пов’язаний з кожним Arduino;
  • рядки 8–15: ці чотири параметри зберігаються локально;

Рядок 29: метод [getView] відповідає за генерацію подання № [position] у [ListView] та управління його подіями. Його код такий:


@Override
    public View getView(int position, View convertView, ViewGroup parent) {
        // поточний Arduino
        final CheckedArduino arduino = arduinos.get(position);
        // створюється поточний рядок
        View row = ((Activity) context).getLayoutInflater().inflate(layoutResourceId, parent, false);
        // отримуємо посилання на [TextView]
        TextView txtArduinoId = (TextView) row.findViewById(R.id.txt_arduino_id);
        TextView txtArduinoDesc = (TextView) row.findViewById(R.id.txt_arduino_description);
        // заповнюємо рядок
        txtArduinoId.setText(arduino.getId());
        txtArduinoDesc.setText(arduino.getDescription());
        // CheckBox не завжди видно
        CheckBox ck = (CheckBox) row.findViewById(R.id.checkBoxArduino);
        ck.setVisibility(selectable ? View.VISIBLE : View.INVISIBLE);
        if (selectable) {
            // присвоюємо йому значення
            ck.setChecked(arduino.isChecked());
            // обробляємо клік
            ck.setOnCheckedChangeListener(new OnCheckedChangeListener() {

                public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
                    arduino.setChecked(isChecked);
                }
            });
        }
        // виводимо рядок
        return row;
    }
  • рядок 2: перший параметр — це позиція у [ListView] рядка, який потрібно створити. Це також позиція у списку Arduino, що зберігається локально;
  • рядок 4: отримується посилання на Arduino, який буде пов’язаний із створеним рядком;
  • рядок 6: поточний рядок створюється на основі подання [listarduinos_item.xml];
  • рядки 8–9: отримуються посилання на обидва [TextView];
  • рядки 11–12: обидва [TextView] отримують свої значення;
  • рядок 14: отримується посилання на прапорець;
  • рядок 15: його роблять видимим або невидимим, залежно від значення [selectable], переданого спочатку конструктору;
  • рядок 16: якщо прапорець присутній;
  • рядок 18: йому присвоюється значення [isChecked] поточного Arduino;
  • рядки 20–26: обробка кліка на прапорці;
  • рядок 23: значення прапорця зберігається в поточному Arduino;

Обробка списку Arduino

Наразі відображення списку Arduino здійснюється двома методами класу [ConfigFragment]:

  • [clearArduinos]: який відображає порожній список;
  • [showArduinos]: який відображає список, повернутий сервером;

Ці два методи працюють наступним чином:


  // очищаємо список Arduino
  private void clearArduinos() {
    // відображається порожній список
    ListArduinosAdapter adapter = new ListArduinosAdapter(getActivity(), R.layout.listarduinos_item, new ArrayList<CheckedArduino>(), false);
    listArduinos.setAdapter(adapter);
  }

  // відображення списку Arduino
  private void showArduinos(List<CheckedArduino> checkedArduinos) {
    // відображення Arduino
    ListArduinosAdapter adapter = new ListArduinosAdapter(getActivity(), R.layout.listarduinos_item, checkedArduinos, false);
    listArduinos.setAdapter(adapter);
}

Завдання: Внесіть ці зміни та протестуйте нову програму.


Image

5.6.10.5. Сесія

Сесія — це місце, куди ми розміщуємо інформацію, якою обмінюються фрагменти та активність. Усі фрагменти повинні відображати список підключених Arduino. Отже, перша версія сесії матиме такий вигляд:


package client.android.architecture.custom;

import client.android.activity.CheckedArduino;
import client.android.architecture.core.AbstractSession;

import java.util.ArrayList;
import java.util.List;

public class Session extends AbstractSession {
  // дані, які слід обмінювати між самими фрагментами та між фрагментами й активністю
  // елементи, які не можна серіалізувати в jSON, повинні мати анотацію @JsonIgnore
  // не забувайте про геттери та сеттери, необхідні для серіалізації/десеріалізації у форматі jSON

  // список Arduino
  private List<CheckedArduino> checkedArduinos = new ArrayList<>();

  // геттери та сеттери
...
}

Завдання: створіть клас [Session], як зазначено вище.


Створення цієї сесії змушує нас змінити вже написаний код наступним чином:


  // відображення відповіді
  private void consumeArduinosResponse(Response<List<Arduino>> response) {
    // помилка?
    if (response.getStatus() != 0) {
      // відображення
      showAlert(response.getMessages());
      // скасування
      doAnnuler();
      // повернення до інтерфейсу
      return;
    }
    // створюється список [CheckedArduino]
    List<CheckedArduino> checkedArduinos = new ArrayList<>();
    for (Arduino arduino : response.getBody()) {
      checkedArduinos.add(new CheckedArduino(arduino, false));
    }
    // заносимо його в сесію
    session.setCheckedArduinos(checkedArduinos);
    // виводимо їх
    showArduinos(checkedArduinos);
    // скасовуємо очікування
    cancelWaitingTasks();
}
  • рядок 18: список Arduino, створений у попередніх рядках, поміщається в сесію;

5.6.10.6. Управління станом фрагмента

Під час обертання пристрою візуальні компоненти подання відображаються (за замовчуванням) у тому стані, в якому вони перебували під час проектування подання:

  • [ListView] містить елементи, які туди помістив розробник;
  • повідомлення про помилку перебуває у тому стані (видимому чи невидимому), в якому його встановив розробник;

Стани візуальних компонентів під час проектування можуть відповідати або не відповідати вимогам під час відновлення фрагмента. Як це виглядає в даному випадку?

  • [ListView] має відображати список підключених Arduino. Отже, значення [ListView] на етапі проектування не може бути використане;
  • [TextView] з повідомлення про помилку має бути відтворений у тому стані (видимому чи невидимому), який він мав під час збереження. Його значення на етапі проектування може не підходити для цих двох випадків;

Отже, під час збереження стану фрагмента нам потрібно зберегти стан цих двох компонентів:

  • список підключених Arduino;
  • видимість (відображається / приховано) повідомлення про помилку під час введення URL веб-сервісу / jSON;

Оскільки список Arduino присутній у сесії, він буде збережено автоматично. Видимість повідомлення про помилку буде збережено в наступному класі [ConfigFragmentState]:

  

package client.android.fragments.state;

import client.android.architecture.custom.CoreState;

public class ConfigFragmentState extends CoreState {

  // видимість повідомлення про помилку
  private boolean txtMsgErreurUrlServiceRestVisible;

  // гетери та сеттери
...
}

Завдання: створіть попередній клас [ConfigFragmentState].


Для коректного відтворення станів фрагментів необхідно змінити їхні методи [getNumView] та [saveFragment]. Наприклад, метод фрагмента [BlinkFragment] наразі має такий вигляд:


  @Override
  public CoreState saveFragment() {
    // необхідно зберегти фрагмент
    DummyFragmentState state=new DummyFragmentState();
    // ...
    return state;
    // якщонемає чого зберігати, виконати [return new CoreState();] та видалити клас [DummyFragmentState]
  }

  @Override
  protected int getNumView() {
    // необхідно повернути номер фрагмента в таблицю фрагментів, що управляються активністю (див. MainActivity)
    return 0;
}

Якщо нічого не робити, стан, відтворений у рядку 6, буде збережено в елементі 0 (рядок 13) масиву CoreState[] coreStates класу [AbstractSession] (рядок 5 нижче):


public class AbstractSession implements ISession {
  ...

  // стан переглядів
  private CoreState[] coreStates = new CoreState[0];
...

Однак його слід зберегти в елементі, що відповідає номеру фрагмента [BlinkFragment] у таблиці фрагментів, визначених у класі [MainActivity] (рядок 9 нижче):


@EActivity
@OptionsMenu(R.menu.menu_main)
public class MainActivity extends AbstractActivity {

  ...

  @Override
  protected AbstractFragment[] getFragments() {
    return new AbstractFragment[]{new ConfigFragment_(), new BlinkFragment_(), new PinReadFragment_(), new PinWriteFragment_(), new CommandsFragment_()};
  }


Номери фрагментів були визначені в інтерфейсі [IMainActivity]:


public interface IMainActivity extends IDao {

  ...

  // номери переглядів
  int VUE_CONFIG = 0;
  int VUE_BLINK = 1;
  int VUE_PINREAD = 2;
  int VUE_PINWRITE = 3;
  int VUE_COMMANDS = 4;
}

У підсумку стан фрагмента [BlinkFragment] буде оброблятися правильно, якщо записати:


  @Override
  public CoreState saveFragment() {
    // необхідно зберегти фрагмент
    DummyFragmentState state=new DummyFragmentState();
    // ...
    return state;
    // якщонемає чого зберігати, виконати [return new CoreState();] та видалити клас [DummyFragmentState]
  }

  @Override
  protected int getNumView() {
    // необхідно повернути номер фрагмента в таблицю фрагментів, що обробляються активністю (див. MainActivity)
    return IMainActivity.VUE_BLINK;
}
  • рядок 14: у таблицю фрагментів, що обробляються активністю, повертається номер фрагмента [BlinkFragment];

Крім того, батьківський клас [CoreState] для станів фрагментів наразі є таким (див. параграф 5.6.7.2):


package client.android.architecture.custom;

import client.android.architecture.core.MenuItemState;
import client.android.fragments.state.*;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;

@JsonIgnoreProperties(ignoreUnknown = true)
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY)
@JsonSubTypes({
  @JsonSubTypes.Type(value = ConfigFragmentState.class),
  @JsonSubTypes.Type(value = BlinkFragmentState.class),
  @JsonSubTypes.Type(value = PinReadFragmentState.class),
  @JsonSubTypes.Type(value = PinWriteFragmentState.class),
  @JsonSubTypes.Type(value = CommandsFragmentState.class)}
)
public class CoreState {
  // фрагмент відвідано чи ні
  protected boolean hasBeenVisited = false;
  // стан можливого меню фрагмента
  protected MenuItemState[] menuOptionsState;

  // гетери та сеттери
....
}
  • рядки 12–16: клас [DummyFragmentState] відсутній у списку дочірніх класів класу [CoreState]. Однак метод [saveFragment] класу [BlinkFragment] наразі повертає тип [ DummyFragmentState]. Якщо залишити все як є, серіалізація/десеріалізація сесії завершиться невдачею, і сесія не буде відновлена, що призведе до збою програми;

Метод [saveFragment] фрагмента [BlinkFragment] потрібно переписати наступним чином:


  @Override
  public CoreState saveFragment() {
    // фрагмент потрібно зберегти
    BlinkFragmentState state=new BlinkFragmentState();
    // ...
    return state;
    // якщонемає чого зберігати, виконати [return new CoreState();] та видалити клас [DummyFragmentState]
}

Завдання: у кожному з фрагментів змініть метод [getNumView] так, щоб він повертав номер фрагмента, а метод [saveFragment] — екземпляр класу стану фрагмента (як показано вище).


5.6.10.7. Управління життєвим циклом фрагмента

Тут ми розглянемо життєвий цикл фрагмента [ConfigFragment], зокрема чотири методи:

  • [saveFragment]: повинен зберегти стан фрагмента, щоб його можна було відтворити пізніше;
  • [initFragment]: повинен ініціалізувати певні поля фрагмента, якщо це необхідно. Цей метод викликається під час запуску додатка та щоразу, коли відбувається поворот пристрою. Точніше кажучи, він викликається, коли фрагмент стає видимим після однієї з двох попередніх подій;
  • [initView]: повинен ініціалізувати певні компоненти подання за потреби. Цей метод викликається щоразу, коли викликано [initFragment], а також коли подання потрібно перегенерувати, оскільки фрагмент у певний момент вийшов із зони суміжності з відображеним фрагментом. Як і раніше, він викликається, коли фрагмент стає видимим після однієї з цих подій;
  • [updateOnRestore]: який виконується після двох попередніх методів у разі повороту пристрою, а також під час навігації. Його завдання — відновити попередній стан фрагмента;

Ці методи будуть такими:


// адаптер списку Arduino
  private ListArduinosAdapter adapterListArduinos;

...
  // управління життєвим циклом фрагмента -------------------------------------

  @Override
  public CoreState saveFragment() {
    ConfigFragmentState state = new ConfigFragmentState();
    state.setTxtMsgErreurUrlServiceRestVisible(txtMsgErreurUrlServiceRest.getVisibility() == View.VISIBLE);
    return state;
  }

  @Override
  protected void initFragment(CoreState previousState) {
    // адаптер listArduinos
    adapterListArduinos = new ListArduinosAdapter(activity, R.layout.listarduinos_item, session.getCheckedArduinos(), false);

  }

  @Override
  protected void initView(CoreState previousState) {
    // зв'язок між списком перегляду та адаптером
    listArduinos.setAdapter(adapterListArduinos);
    // Перший візит?
    if (previousState == null) {
      // ListView порожній — створено [initFragment]
      // приховане повідомлення про помилку
      txtMsgErreurUrlServiceRest.setVisibility(View.INVISIBLE);
    } else {
      // відновлено видимість повідомлення про помилку
      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() {
    // кнопки
    initButtons();
}
  • рядок 2: адаптер ListView для Arduino. Є глобальною змінною, оскільки використовується в різних методах;
  • рядки 7–12: метод [saveFragment] зберігає у типі [ConfigFragmentState] інформацію про видимість TextView та txtMsgErreurUrlServiceRestVisible (рядок 10);
  • рядки 14–19: метод [initFragment] ініціалізує адаптер із рядка 2 за допомогою списку Arduino, присутніх у сесії (рядок 17). Нагадаємо, що роль [initFragment] полягає в ініціалізації полів фрагмента. Тут цю ініціалізацію потрібно виконувати в будь-якому випадку, незалежно від того, чи це перший візит (previousState==null), чи ні;
  • рядок 17: бачимо, що адаптер пов’язаний із джерелом даних [session.getCheckedArduinos]. Воно не повинно мати значення null. З цієї причини поле [session.checkedArduinos] ініціалізується порожнім списком у сесії:

  // список Arduino
private List<CheckedArduino> checkedArduinos = new ArrayList<>();
  • рядки 21–35: метод [initView] призначений для ініціалізації певних компонентів візуального інтерфейсу, зокрема тих, значення яких не зберігаються під час повороту пристрою;
  • рядок 24: ListView для Arduino пов’язаний з адаптером у рядку 2;
  • рядки 28–32: перше відвідування відрізняється від інших;
  • рядок 29: під час першого відвідування потрібно відобразити порожній [ListView]. Це відбувається тому, що під час першого відвідування адаптер [ListView] був пов’язаний із порожнім списком (рядок 17);
  • рядок 31: повідомлення про помилку приховано;
  • рядки 32–36: випадок, коли це не перший візит;
  • [ListView] вже перебуває у правильному стані з рядка 24. Більше нічого робити не потрібно;
  • рядки 34–35: відновлюємо повідомлення про помилку до того стану, в якому воно перебувало під час останнього збереження фрагмента;
  • рядки 31–36: метод [updateOnRestore] повинен повернути фрагмент у його початковий стан. До методу [updateOnRestore] можна дістатися двома способами:
    • або через поворот пристрою. У цьому випадку всі необхідні ініціалізації вже виконано в [initView];
    • або через перехід з однієї вкладки на вкладку [Config]. Якщо фрагмент [Config] вийшов із сусідності з відображеними фрагментами з моменту його залишення, то метод [initView] був виконаний, і фрагмент уже перебуває у бажаному стані. Якщо фрагмент [Config] не вийшов із сукупності відображених фрагментів з моменту виходу з нього, то його візуальні компоненти не змінили свого стану, і нічого робити не потрібно;

Бачимо, що метод [updateOnRestore] нічого не робить. Іноді так буває, іноді — ні. Різниця полягає в методі [updateOnSubmit]: якщо цей метод виконує дію, яка робить непотрібними певні ініціалізації, виконані в [initView], то ці ініціалізації слід виконувати в методі [updateOnRestore]. Розглянемо приклад перемикача з трьома значеннями: V1, V2, V3. Можливо, у випадку навігації, пов’язаної з дією [SUBMIT], обраним перемикачем завжди має бути той, що має значення V1. У цьому випадку відновлення значення перемикача в методі [initView] є зайвим, оскільки у випадку [SUBMIT] це значення буде замінено на те, що задається методом [updateOnSubmit]. Тому краще перемістити це відновлення в метод [updateOnRestore], щоб уникнути виконання іноді непотрібної операції.

  • рядки 48–52: метод [notifyEndOfUpdates] виконується після всіх попередніх;
  • рядок 51: кнопки повертаються до початкового стану: кнопка [Rafraîchir] відображається, кнопка [Annuler] прихована:

Завдання: додайте наведений вище код у [ConfigFragment], а потім запустіть додаток. Переконайтеся, що під час повороту пристрою вкладка [Config] зберігає свій стан (повідомлення про помилку, список Arduino). Перевірте, чи так само відбувається під час простого переходу з вкладки [config] на вкладку [Commands], а потім на вкладку [Config]. У цьому останньому випадку, якщо у вкладці [IMainActivity] ви зберегли сусідство фрагментів, що дорівнює 1, то перегляд фрагмента [ConfigFragment] знищується під час переходу на вкладку [Commands], а потім відтворюється під час повернення на вкладку [Config]. Під час тестування перегляньте журнали.


5.6.10.8. Вдосконалення коду

Код фрагмента [ConfigFragment] можна вдосконалити. Наприклад, ми написали:


// адаптер списку Arduino
  private ListArduinosAdapter adapterListArduinos;

...

  // відображення списку Arduino
  private void showArduinos(List<CheckedArduino> checkedArduinos) {
    // відображення Arduino
    ListArduinosAdapter adapter = new ListArduinosAdapter(getActivity(), R.layout.listarduinos_item, checkedArduinos, false);
    listArduinos.setAdapter(adapter);
  }

  // очищення списку Arduino
  private void clearArduinos() {
    // відображення порожнього списку
    ListArduinosAdapter adapter = new ListArduinosAdapter(getActivity(), R.layout.listarduinos_item, new ArrayList<CheckedArduino>(), false);
    listArduinos.setAdapter(adapter);
  }
  • бачимо, що в рядках 9 та 16 використовується локальна змінна, не пов’язана з полем у рядку 2, хоча ми саме з цією сутністю і хочемо працювати;

Ми вдосконалюємо код таким чином:


  // адаптер списку Arduino
  private ListArduinosAdapter adapterListArduinos;

  @Click(R.id.btn_Rafraichir)
  protected void doRafraichir() {
  ...
  }

  private void getArduinosInBackground() {
 ...
    // використання
    consumeArduinosResponse(response);
  }

  // відображення відповіді
  private void consumeArduinosResponse(Response<List<Arduino>> response) {
    // помилка?
    if (response.getStatus() != 0) {
      // відображення
      showAlert(response.getMessages());
      // скасування
      doAnnuler();
      // повернення до інтерфейсу
      return;
    }
    // створюється список [CheckedArduino]
    List<CheckedArduino> checkedArduinos = session.getCheckedArduinos();
    checkedArduinos.clear();
    for (Arduino arduino : response.getBody()) {
      checkedArduinos.add(new CheckedArduino(arduino, false));
    }
    // вони відображаються
    adapterListArduinos.notifyDataSetChanged();
    // скасовується очікування
    cancelWaitingTasks();
}
  
  @Override
  protected void initFragment(CoreState previousState) {
    // адаптер listArduinos
    adapterListArduinos = new ListArduinosAdapter(activity, R.layout.listarduinos_item, session.getCheckedArduinos(), false);

  }

  @Override
  protected void initView(CoreState previousState) {
    // зв'язок між списком перегляду та адаптером
    listArduinos.setAdapter(adapterListArduinos);
    ...
}
  • коли виконується метод у рядку 5, життєвий цикл фрагмента вже завершився. Отже:
    • адаптер у рядку 2 було пов’язано з його джерелом даних (рядок 41);
    • [ListView] підключених Arduino було пов’язано з цим адаптером (рядок 48);

Коли ми хочемо змінити відображення [ListView], потрібно зробити дві речі:

  • змінити вміст джерела даних [session.checkedArduinos];
  • повідомити про цю зміну адаптеру за допомогою команди [adapterListArduinos.notifyDataSetChanged()];

Мова йде саме про зміну вмісту джерела даних, а не самого джерела даних. Якщо змінити саме джерело даних, операція [adapterListArduinos.notifyDataSetChanged()] продовжуватиме відображати старе джерело даних. У такому випадку доведеться пов’язати адаптер із новим джерелом даних.

Код виглядає так:

  • рядок 27: отримуємо джерело даних;
  • рядок 28: очищаємо його. З цієї причини ми видалили метод [clearArduinos];
  • рядки 29–31: до цього списку, що став порожнім, додаємо нові елементи;
  • рядок 33: адаптеру дається команда оновити дані. Це призведе до оновлення відображення пов’язаного [ListView];

Завдання: внесіть ці зміни та переконайтеся, що ваш додаток і надалі працює.


5.6.11. Взаємодія між видами

Щоб перевірити взаємодію між вікнами, ми змусимо всі інші вікна відображати список Arduino, отриманий вікном [Config]. Почнемо з вікна [blink.xml]. Раніше воно нічого не відображало, але тепер воно показуватиме список підключених Arduino:

Image

 

Код XML для подання [blink.xml] буде таким:


<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
            android:id="@+id/scrollView1"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content">

  <?xml version="1.0" encoding="utf-8"?>
  <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
                  xmlns:tools="http://schemas.android.com/tools"
                  android:id="@+id/RelativeLayout1"
                  android:layout_width="match_parent"
                  android:layout_height="match_parent">

    <TextView
      android:id="@+id/txt_arduinos"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:layout_alignParentLeft="true"
      android:layout_marginTop="150dp"
      android:text="@string/titre_list_arduinos"
      android:textColor="@color/blue"
      android:textSize="20sp" />

    <ListView
      android:id="@+id/ListViewArduinos"
      android:layout_width="match_parent"
      android:layout_height="200dp"
      android:layout_alignParentLeft="true"
      android:layout_below="@+id/txt_arduinos"
      android:layout_marginTop="30dp"
      android:background="@color/wheat">
    </ListView>

  </RelativeLayout>
</ScrollView>

Цей код було взято безпосередньо з подання [config.xml]. Було змінено лише верхнє поле рядка 19.


Завдання: скопіюйте цей код у перегляд [commands.xml, pinread.xml, pinwrite.xml].


Код фрагмента [BlinkFragment], пов’язаного з поданням [blink.xml], також змінюється:

  

  // візуальні компоненти
  @ViewById(R.id.ListViewArduinos)
  protected ListView listArduinos;

  // адаптер списку Arduino
  private ListArduinosAdapter adapterListArduinos;
...

  // методи, задані батьківським класом -------------------------------------------------------

...
  @Override
  protected void initFragment(CoreState previousState) {
    // адаптер listArduinos
    adapterListArduinos = new ListArduinosAdapter(activity, R.layout.listarduinos_item, session.getCheckedArduinos(), true);

  }

  @Override
  protected void initView(CoreState previousState) {
    // зв’язок між listview та адаптером
    listArduinos.setAdapter(adapterListArduinos);
  }
...
  • рядки 2–3: компонент [ListView] підключених Arduino;
  • рядок 6: адаптер цього [ListView];
  • рядки 12–23: код методів [initFragment] та [initView] — це той самий код, що вже використовувався для фрагмента [ConfigFragment];
  • рядок 15: коли фрагмент потрібно скинути, скидається адаптер із рядка 2 шляхом його прив’язування до списку Arduino, збереженого в сесії. Останній параметр [true] конструктора [ListArduinosAdapter] означає, що поруч із кожним Arduino має відображатися прапорець;
  • рядок 22: коли потрібно скинути відображення фрагмента, пов’язуємо [ListView] підключених Arduino з адаптером у рядку 6;

Завдання: Скопіюйте цей код в інші фрагменти [CommandsFragment, PinReadFragment, PinWriteFragment]. Запустіть додаток і переконайтеся, що на кожній вкладці відображається список підключених Arduino. Також зверніть увагу, що якщо ви встановите прапорці біля Arduino на одній вкладці, а потім перейдете на іншу вкладку, вони залишаться встановленими і на цій вкладці.


Примітка: Пояснення того, чому підмічені Arduino залишаються підміченими, таке. Клас [ListArduinosAdapter] було представлено в розділі 5.6.10.4. Код, пов’язаний із прапорцем, такий:


        // поточний Arduino
        final CheckedArduino arduino = arduinos.get(position);
...
        // CheckBox не завжди видно
        CheckBox ck = (CheckBox) row.findViewById(R.id.checkBoxArduino);
        ck.setVisibility(selectable ? View.VISIBLE : View.INVISIBLE);
        if (selectable) {
            // йому присвоюється значення
            ck.setChecked(arduino.isChecked());
            // обробка кліка
            ck.setOnCheckedChangeListener(new OnCheckedChangeListener() {

                public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
                    arduino.setChecked(isChecked);
                }
            });
}
  • рядки 11–15: якщо у вкладці X встановити прапорець, для Arduino у рядку 2 властивість [checked] змінюється на true (рядок 14);
  • при переході на вкладку Y відображається значення [ListView] для Arduino на цій вкладці. У рядку 9 видно, що якщо властивість [checked] Arduino у рядку 2 змінюється на true, то поле [ck] у рядку 5 буде позначено галочкою;

5.6.12. Шар [DAO]

Примітка: для цієї частини перегляньте реалізацію шару [DAO] у проєкті [exemple-16B] (див. параграф 2.8.3).

Наразі ми вручну сформували список підключених Arduino. Тепер ми будемо запитувати його у веб-сервера / jSON. Для цього ми створимо рівень [DAO]:

  

5.6.12.1. Інтерфейс IDao

Інтерфейс [IDao] шару [DAO] матиме такий вигляд:


package client.android.dao.service;

import client.android.dao.entities.Arduino;
import client.android.dao.entities.Response;
import rx.Observable;

import java.util.List;

public interface IDao {
  // URL веб-сервісу
  void setUrlServiceWebJson(String url);

  // користувач
  void setUser(String user, String mdp);

  // тайм-аут клієнта
  void setTimeout(int timeout);

  // базова автентифікація
  void setBasicAuthentification(boolean isBasicAuthentificationNeeded);

  // режим налагодження
  void setDebugMode(boolean isDebugEnabled);

  // Час очікування клієнта перед надсиланням запиту (у мілісекундах)
  void setDelay(int delay);

  // специфічний ----------------------------------------
  // список Arduino
  Observable<Response<List<Arduino>>> getArduinos();
}
  • рядки 11–26: ці рядки вже присутні в інтерфейсі [IDao] зразкового проєкту [client-android-skel];
  • рядок 30: метод [getArduinos] дозволяє отримати список підключених Arduino у вигляді об’єкта спостереження типу Observable<[Response<List<Arduino>>>];

Нагадаємо, що [Response<T>] — це тип усіх відповідей, що надсилаються сервером у вигляді рядка jSON:


package client.android.dao.entities;

import java.util.List;

public class Response<T> {

    // ----------------- властивості
    // статус операції
    private int status;
    // можливі повідомлення про помилки
    private List<String> messages;
    // тіло відповіді
    private T body;

    // конструктори
    public Response() {

    }

    public Response(int status, List<String> messages, T body) {
        this.status = status;
        this.messages = messages;
        this.body = body;
    }

    // методи отримання та встановлення
...
}

5.6.12.2. Інтерфейс [WebClient]

  

Інтерфейс [WebClient] — це інтерфейс, реалізацію якого забезпечує бібліотека AA. Цей інтерфейс матиме такий вигляд:


package client.android.dao.service;

import client.android.dao.entities.Arduino;
import client.android.dao.entities.Response;
import org.androidannotations.rest.spring.annotations.Get;
import org.androidannotations.rest.spring.annotations.Path;
import org.androidannotations.rest.spring.annotations.Rest;
import org.androidannotations.rest.spring.api.RestClientRootUrl;
import org.androidannotations.rest.spring.api.RestClientSupport;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.client.RestTemplate;

import java.util.List;

@Rest(converters = {MappingJackson2HttpMessageConverter.class})
public interface WebClient extends RestClientRootUrl, RestClientSupport {

  // RestTemplate
  void setRestTemplate(RestTemplate restTemplate);

  // специфічне --------------------------------------
  // список Arduino
  @Get("/arduinos")
  Response<List<Arduino>> getArduinos();
}
  • рядки 15–19: ці рядки є базовими в інтерфейсі [WebClient] зразкового проєкту [client-android-skel];
  • рядок 23: URL сервера, що дозволяє отримати список Arduino за допомогою операції GET. Нагадаємо, що цей URL вимірюється відносно кореневого URL [RestClientRootUrl] у рядку 16;
  • рядок 24: сервер повертає рядок jSON типу [Response<List<Arduino>>]. Цей рядок jSON автоматично десеріалізується у тип [Response<List<Arduino>>] за допомогою конвертера jSON [MappingJackson2HttpMessageConverter] із рядка 15;

5.6.12.3. Клас [Dao]

Клас [Dao] реалізує інтерфейс [IDao] наступним чином:


package client.android.dao.service;

import android.util.Log;
import client.android.dao.entities.Arduino;
import client.android.dao.entities.Response;
import org.androidannotations.annotations.AfterInject;
import org.androidannotations.annotations.Bean;
import org.androidannotations.annotations.EBean;
import org.androidannotations.rest.spring.annotations.RestService;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.client.RestTemplate;
import rx.Observable;

import java.util.ArrayList;
import java.util.List;

@EBean(scope = EBean.Scope.Singleton)
public class Dao extends AbstractDao implements IDao {

  // клієнт веб-сервісу
  @RestService
  protected WebClient webClient;
  // безпека
  @Bean
  protected MyAuthInterceptor authInterceptor;
  // RestTemplate
  private RestTemplate restTemplate;
  // фабрика RestTemplate
  private SimpleClientHttpRequestFactory factory;

  @AfterInject
  public void afterInject() {
    // журнал
    Log.d(className, "afterInject");
    // утворюється restTemplate
    factory = new SimpleClientHttpRequestFactory();
    restTemplate = new RestTemplate(factory);
    // встановлюємо конвертер jSON
    restTemplate.getMessageConverters().add(new MappingJackson2HttpMessageConverter());
    // встановлюємо restTemplate веб-клієнта
    webClient.setRestTemplate(restTemplate);
  }

  @Override
  public void setUrlServiceWebJson(String url) {
    // встановлюємо URL веб-сервісу
    webClient.setRootUrl(url);
  }

  @Override
  public void setUser(String user, String mdp) {
    // реєструємо користувача в перехоплювачі
    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.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));
    }
    // інтерцептор автентифікації?
    if (isBasicAuthentificationNeeded) {
      // додано перехоплювач автентифікації
      List<ClientHttpRequestInterceptor> interceptors = new ArrayList<ClientHttpRequestInterceptor>();
      interceptors.add(authInterceptor);
      restTemplate.setInterceptors(interceptors);
    }
  }

  // приватні методи -------------------------------------------------
  private void log(String message) {
    if (isDebugEnabled) {
      Log.d(className, message);
    }
  }

  // специфічна реалізація IDao -----------------------------------------------

  @Override
  public Observable<Response<List<Arduino>>> getArduinos() {
    // виконання веб-клієнтом
    return getResponse(new IRequest<Response<List<Arduino>>>() {
      @Override
      public Response<List<Arduino>> getResponse() {
        return webClient.getArduinos();
      }
    });
  }
}
  • рядки 19–87: ці рядки є базовими у класі [Dao] проекту [client-android-skel];
  • рядки 91–100: реалізація методу [getArduinos];
  • рядок 94: викликається метод [getResponse] батьківського класу. Єдиним параметром цього методу є екземпляр інтерфейсу [IRequest<T>];
  • рядки 95–99: єдиним методом інтерфейсу [IRequest<T>] є метод [T getResponse()];
  • рядок 94: тип T класу [IRequest<T>] має бути типом T результату Observable<T> методу з рядка 92, отже, тут це тип [Response<List<Arduino>>];
  • рядок 97: метод [IRequest.getResponse()] делегує роботу методу [webClient.getArduinos()], який ми вже розглядали. [webClient], визначений у рядку 24, інстанціюється бібліотекою AA і є екземпляром інтерфейсу [WebClient], який ми вже розглядали;

5.6.13. Активність [MainActivity]

  

Ми вже розглядали операцію [MainActivity] у розділі 5.6.8. Вона розширює клас [AbstractActivity] і, відповідно, реалізує інтерфейс [IMainActivity], який, у свою чергу, розширює інтерфейс [IDao]. Кожного разу, коли до інтерфейсу [IDao] додається метод, його потрібно реалізувати в класі [MainActivity]. Метод [IDao.getArduinos], доданий до інтерфейсу [IDao], буде реалізовано в класі [MainActivity] наступним чином:


...
@EActivity
@OptionsMenu(R.menu.menu_main)
public class MainActivity extends AbstractActivity {

  // шар [DAO]
  @Bean(Dao.class)
  protected IDao dao;
  // сесія
  private Session session;

...

  // реалізація IDao -----------------------------------------
  @Override
  public Observable<Response<List<Arduino>>> getArduinos() {
    return dao.getArduinos();
  }
}
  • рядки 15–18: метод [getArduinos] реалізовано шляхом делегування роботи класу [Dao], який щойно було представлено і на який є посилання в рядку 8;

5.6.14. Повторний розгляд фрагмента [ConfigFragment]

У класі [ConfigFragment] код, що виконується при натисканні кнопки [Rafraîchir], наразі виглядає так:


  @Click(R.id.btn_Rafraichir)
  protected void doRafraichir() {
    ...
    // запит списку Arduino у фоновому режимі
    getArduinosInBackground();
  }

  private void getArduinosInBackground() {
    // створено фіктивний список Arduino
    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));
    }
    // імітація відповіді сервера
    Response<List<Arduino>> response = new Response<>();
    response.setBody(arduinos);
    // обробка відповіді
    consumeArduinosResponse(response);
  }

  // виведення відповіді
  private void consumeArduinosResponse(Response<List<Arduino>> response) {
    ...
}

Нам потрібно переписати рядки 10–16, які жорстко генерували відповідь типу [Response<List<Arduino>>]. Тепер нам потрібно запитувати цей список у шарі [DAO] через активність. Код стає таким:


  @Click(R.id.btn_Rafraichir)
  protected void doRafraichir() {
    // перевіряємо введені дані
    if (!pageValid()) {
      return;
    }
    // зберігаємо введені дані
    mainActivity.setUrlServiceWebJson(urlServiceRest);
    // готуємося до очікування
    beginWaiting(1);
    // виконується асинхронне завдання
    executeInBackground(mainActivity.getArduinos(), new Action1<Response<List<Arduino>>>() {

      @Override
      public void call(Response<List<Arduino>> response) {
        // обробка відповіді
        consumeArduinosResponse(response);
      }
    });
}
  • рядок 8: кореневий URL веб-сервісу / jSON, введений користувачем, передається на рівень [DAO] через активність. Це буде кореневий елемент URL інтерфейсу [WebClient] (див. параграф 5.6.12.2);
  • рядок 10: батьківський клас отримує повідомлення про те, що буде запущено асинхронне завдання;
  • рядки 12–19: запуск асинхронного завдання, яке поверне список Arduino, підключених до сервера;
  • рядок 12: виклик методу [executeInBackground] батьківського класу. Цей метод очікує два параметри:
    • рядок 12: процес, який потрібно спостерігати. Цей процес тут надається методом [mainActivity.getArduinos()];
    • рядки 12–19: екземпляр інтерфейсу [Action1<T>], де тип T — це тип, наданий процесом, у даному випадку тип [Response<List<Arduino>>];
  • рядки 14–18: метод, що викликається, коли асинхронне завдання повертає результат типу [Response<List<Arduino>>];
  • рядок 17: отриману відповідь передаємо до вже написаного методу [consumeArduinosResponse];

Завдання: Запустіть сервер, як зазначено в розділі 5.4. Підключіть один або кілька Arduino до PC, на якому запущено сервер. Потім запустіть клієнт для Android і переконайтеся, що ви можете отримати список підключених Arduino. Перегляньте журнали.


Image

  • введіть URL, вказаний у [1]. Це одна з адрес IP вашого сервера;
  • натисніть на кнопку [2];
  • ви повинні отримати список підключених Arduino в [3];

Переконайтеся, що цей список також відображається в інших вкладках.

5.7. Завдання


Діючи так само, як щойно було зроблено для вікна [Config], послідовно створіть та протестуйте чотири інші вікна додатка: [Blink], [PinRead], [PinWrite] та [Commands].


Види, які потрібно реалізувати, наведено в розділі 5.5.

Для кожного виду необхідно:

  • намалювати вигляд XML (див. розділ 5.6.9);
  • побудувати відповідний фрагмент (див. параграф 5.6.10);
  • додати метод до інтерфейсу [WebClient] (див. параграф 5.6.12.2);
  • додати метод до інтерфейсу [IDao] (див. параграф 5.6.12.2);
  • додати метод до класу [Dao] (див. параграф 5.6.12.3);
  • додати метод до активності [MainActivity] (див. розділ 5.6.13);
  • написати обробники подій фрагмента (див. розділ 5.6.14);
  • провести тестування та проаналізувати журнали;

Примітка 1: за зразком слід брати проект [Exemple-16B] з курсу (див. параграф 2.8.3).

Примітка 2: URL, до яких слід звертатися, та тип їхніх відповідей були представлені в параграфі 5.4.2.

Примітка 3:

Клас [CommandsFragment] надсилає список, що містить єдину команду, яку мають виконати один або кілька Arduino. Ця команда буде інкапсульована в наступний клас [ArduinoCommand]:


package android.arduinos.dao;

import java.util.Map;

public class ArduinoCommand {

  // дані
  private String id;
  private String ac;
  private Map<String, Object> pa;

  // конструктори
  public ArduinoCommand() {

  }

  public ArduinoCommand(String id, String ac, Map<String, Object> pa) {
    this.id = id;
    this.ac = ac;
    this.pa = pa;
  }

  // гетери та сеттери
...
}

В інтерфейсі [WebClient] метод виконання цього списку з однією командою буде таким:


  // відправлення команд JSON
  @Post("/arduinos/commands/{idArduino}")
Response<List<ArduinoResponse>> sendCommands(@Body List<ArduinoCommand> commands, @Path String idArduino);
  • рядок 2: URL запитується з командою HTTP POST;
  • рядок 3: опубліковане значення повинно мати позначку [@Body];

Примітка 4: рекомендується виконувати цю роботу таким чином:

  • переходити до наступного виду лише після того, як поточний вид було створено та протестовано;
  • управляти станом вікон слід лише після того, як додаток запрацює в нормальних умовах. Потім для кожного вікна прокрутіть пристрій для різних станів вікна та занотуйте втрачену інформацію. Саме її потрібно зберегти, а потім відновити. Потім перевірте навігацію: коли ви залишаєте вкладку і повертаєтеся до неї пізніше, вона має бути в тому стані, в якому ви її залишили;