Implementácia aplikácie nSoric aAurela: Rozdiel medzi revíziami

Z Kiwiki
Skočit na navigaci Skočit na vyhledávání
d (Zamkol stránku „Implementácia aplikácie nSoric aAurela“ ([Úprava=Povoliť iba správcom] (na neurčito) [Presun=Povoliť iba správcom] (na neurčito)))
 
(3 medziľahlé úpravy od 2 ďalších používateľov nie sú zobrazené)
Riadok 4: Riadok 4:
  
 
=  =
 
=  =
Implementácia používateľského rozhrania a jednotlivých funkcionalít aplikácie vychádza zo špecifikácie požiadaviek a navrhnutej štruktúry aplikácie v kapitole [[#navrh|[navrh]]]. V tejto kapitole sú postupne opísané: implementácia používateľského rozhrania aplikácie, práca s dátami, komunikácia so serverom a samotná vizualizácia dát v aplikácii.
+
Implementácia používateľského rozhrania a jednotlivých funkcionalít aplikácie vychádza zo špecifikácie požiadaviek a navrhnutej štruktúry aplikácie v kapitole [[Návrh aplikácie nSoric aAurela]]. V tejto kapitole sú postupne opísané: implementácia používateľského rozhrania aplikácie, práca s dátami, komunikácia so serverom a samotná vizualizácia dát v aplikácii.
  
 
== Používateľské rozhranie aplikácie ==
 
== Používateľské rozhranie aplikácie ==
  
Jednotlivé obrazovky tvoriace používateľské rozhranie aplikácie, prostredníctvom ktorých používateľ s aplikáciou interaguje, sú implementované podľa navrhnutej štruktúry aplikácie (časť [[#navrh_aktivity|[navrh_aktivity]]]) ako jednotlivé triedy Android aktivít. Tieto Android aktivity rozdeľujú aplikáciu na dve časti: primárnu a sekundárnu.
+
Jednotlivé obrazovky tvoriace používateľské rozhranie aplikácie, prostredníctvom ktorých používateľ s aplikáciou interaguje, sú implementované podľa navrhnutej štruktúry aplikácie ako jednotlivé triedy Android aktivít. Tieto Android aktivity rozdeľujú aplikáciu na dve časti: primárnu a sekundárnu.
  
 
Aktivity primárnej časti poskytujú používateľovi priamy prístup k prehľadu aktuálneho stavu meracieho systému Sensorical. Aktivity sekundárnej časti predstavujú pomocné aktivity, ktoré sú však nevyhnutné pre prístup do primárnej časti, tj. nutnosť výberu adresy servera, ku ktorému sa chce používateľ pripojiť, ako aj samotné prihlásenie do aplikácie. '''Android aktivity primárnej časti'''
 
Aktivity primárnej časti poskytujú používateľovi priamy prístup k prehľadu aktuálneho stavu meracieho systému Sensorical. Aktivity sekundárnej časti predstavujú pomocné aktivity, ktoré sú však nevyhnutné pre prístup do primárnej časti, tj. nutnosť výberu adresy servera, ku ktorému sa chce používateľ pripojiť, ako aj samotné prihlásenie do aplikácie. '''Android aktivity primárnej časti'''
Riadok 79: Riadok 79:
 
* Snackbar má byť zobrazený len po určitú dobu, bez nutnosti explicitného zrušenia používateľom.
 
* Snackbar má byť zobrazený len po určitú dobu, bez nutnosti explicitného zrušenia používateľom.
  
[[File:snackbar.png|207px|Material Design komponenty použité v aplikácii: Snackbar (aktivita Login), AppBar, Card a FAB (aktivita Dashboard)]] [[File:obrazky/card_app_bar_fab.png|207px|Material Design komponenty použité v aplikácii: Snackbar (aktivita Login), AppBar, Card a FAB (aktivita Dashboard)]]
+
[[File:snackbar.png|207px|Material Design komponenty použité v aplikácii: Snackbar (aktivita Login), AppBar, Card a FAB (aktivita Dashboard)]] [[File:card_app_bar_fab.png|207px|Material Design komponenty použité v aplikácii: Snackbar (aktivita Login), AppBar, Card a FAB (aktivita Dashboard)]]
  
 
== Práca s dátami v aplikácii ==
 
== Práca s dátami v aplikácii ==
Riadok 91: Riadok 91:
 
=== Implementácia SQLite databázy ===
 
=== Implementácia SQLite databázy ===
  
SQLite databáza, ktorá slúži na archiváciu stiahnutých dát a ich následnú prezentáciu v aplikácii, bola vytvorená pomocou knižnice Room (viď [[#room|[room]]]), pričom je implementovaná tak, aby presne korešpondovala s dátovým modelom systému Sensorical (). Aplikácia je navrhnutá tak, že umožňuje vytvárať a pracovať s ľubovoľným počtom SQLite databáz, ktoré sú vytvárané na základe webovej adresy servera, ku ktorej je používateľ aktuálne prihlásený.
+
SQLite databáza, ktorá slúži na archiváciu stiahnutých dát a ich následnú prezentáciu v aplikácii, bola vytvorená pomocou knižnice Room, pričom je implementovaná tak, aby presne korešpondovala s dátovým modelom systému Sensorical (). Aplikácia je navrhnutá tak, že umožňuje vytvárať a pracovať s ľubovoľným počtom SQLite databáz, ktoré sú vytvárané na základe webovej adresy servera, ku ktorej je používateľ aktuálne prihlásený.
  
 
Každý server, ku ktorému sa používateľ prihlási, má v aplikácii vytvorenú vlastnú SQLite databázu. Vždy po úspešnom prihlásení k niektorému zo serverov sa aplikácia pokúsi vytvoriť novú databázu s názvom webovej adresy daného servera. Ak už databáza s takýmto názvom existuje, aplikácia ju použije a otvorí.
 
Každý server, ku ktorému sa používateľ prihlási, má v aplikácii vytvorenú vlastnú SQLite databázu. Vždy po úspešnom prihlásení k niektorému zo serverov sa aplikácia pokúsi vytvoriť novú databázu s názvom webovej adresy daného servera. Ak už databáza s takýmto názvom existuje, aplikácia ju použije a otvorí.
  
Pri samotnej implementácii SQLite databázy v aplikácii, boli ako prvé vytvorené jednotlivé triedy reprezentujúce entity databázy ([[#listing:entity|[listing:entity]]]). Každá trieda entity obsahuje premenné, ktoré predstavujú jednotlivé atribúty danej entity. Každá premenná triedy entity je prístupná prostredníctvom metód get a set.
+
Pri samotnej implementácii SQLite databázy v aplikácii, boli ako prvé vytvorené jednotlivé triedy reprezentujúce entity databázy (Listing 4.1). Každá trieda entity obsahuje premenné, ktoré predstavujú jednotlivé atribúty danej entity. Každá premenná triedy entity je prístupná prostredníctvom metód get a set.
  
<source lang="java">    @Entity(tableName = "area")
+
<source lang="java">     
 +
@Entity(tableName = "area")
 
     public class Area {
 
     public class Area {
 
         @PrimaryKey
 
         @PrimaryKey
Riadok 107: Riadok 108:
 
         public String getName() { return name; }
 
         public String getName() { return name; }
 
         public void setName(String name) { this.name = name; }
 
         public void setName(String name) { this.name = name; }
     }</source>
+
     }
Ako ďalšie boli vytvorené potrebné DAO rozhrania ([[#listing:dao|[listing:dao]]] rozhranie AreaDao) a ich metódy pre prístup k dátam databázy. Návratové typy týchto metód boli zvolené na základe očakávaného výsledku SQL dotazu, ktorý daná metóda vykonáva. V prípade DAO metód, ktoré slúžia na výber údajov z SQLite databázy, sú návratové typy zabalené do generickej triedy Maybe, knižnice RxJava, ktorá umožňuje vykonávať prácu týchto metód asynchrónne v paralelných vláknach.
+
</source>
 +
Kód 4.1: Trieda entity area
 +
 
 +
Ako ďalšie boli vytvorené potrebné DAO rozhrania (Kód 4.2) a ich metódy pre prístup k dátam databázy. Návratové typy týchto metód boli zvolené na základe očakávaného výsledku SQL dotazu, ktorý daná metóda vykonáva. V prípade DAO metód, ktoré slúžia na výber údajov z SQLite databázy, sú návratové typy zabalené do generickej triedy Maybe, knižnice RxJava, ktorá umožňuje vykonávať prácu týchto metód asynchrónne v paralelných vláknach.
  
 
DAO metódy, ktoré do SQLite databázy vkladajú záznamy nameraných hodnôt, využívajú v prípade konfliktu hodnôt primárnych kľúčov parameter IGNORE (ignorovať). Parameter IGNORE zabezpečí neprepísanie existujúceho záznamu v SQLite databáze, pretože nameraná hodnota nemôže byť spätne modifikovaná a teda nie je nutné už existujúce záznamy prepisovať (skrátenie času vykonávania operácie).
 
DAO metódy, ktoré do SQLite databázy vkladajú záznamy nameraných hodnôt, využívajú v prípade konfliktu hodnôt primárnych kľúčov parameter IGNORE (ignorovať). Parameter IGNORE zabezpečí neprepísanie existujúceho záznamu v SQLite databáze, pretože nameraná hodnota nemôže byť spätne modifikovaná a teda nie je nutné už existujúce záznamy prepisovať (skrátenie času vykonávania operácie).
Riadok 114: Riadok 118:
 
DAO metódy zabezpečujúce vkladanie ostatných údajov, majú v prípade konfliktu hodnôt primárnych kľúčov nastavený parameter REPLACE (nahradiť). V prípade modifikácie záznamu na strane servera sa síce nemení hodnota jeho primárneho kľúča, ale hodnota niektorého z atribútov je zmenená. Parameter REPLACE v takomto prípade zabezpečí prepísanie záznamu uloženého v SQLite databáze modifikovaným záznamom stiahnutým zo servera.
 
DAO metódy zabezpečujúce vkladanie ostatných údajov, majú v prípade konfliktu hodnôt primárnych kľúčov nastavený parameter REPLACE (nahradiť). V prípade modifikácie záznamu na strane servera sa síce nemení hodnota jeho primárneho kľúča, ale hodnota niektorého z atribútov je zmenená. Parameter REPLACE v takomto prípade zabezpečí prepísanie záznamu uloženého v SQLite databáze modifikovaným záznamom stiahnutým zo servera.
  
<source lang="java">    @Dao
+
<source lang="java">  
 +
   @Dao
 
     public interface AreaDao {
 
     public interface AreaDao {
 
         @Insert(onConflict = OnConflictStrategy.REPLACE)
 
         @Insert(onConflict = OnConflictStrategy.REPLACE)
Riadok 122: Riadok 127:
 
         where group_view.user_id=:userID")
 
         where group_view.user_id=:userID")
 
         Maybe<List<Integer>> getAllAreaIDsforUser(int userID);  
 
         Maybe<List<Integer>> getAllAreaIDsforUser(int userID);  
     }</source>
+
     }
Ako posledná bola implementovaná abstraktná trieda databázy ([[#listing:database|[listing:database]]]), ktorá je potomkom triedy RoomDatabase. Trieda databázy v aplikácii využíva návrhový vzor Singleton, ktorý zabezpečí vytvorenie a dostupnosť jedinej inštancie triedy databázy počas behu aplikácie. Inštanciu triedy databázy je možné vytvoriť (získať) prostredníctvom statickej metódy getInstance().
+
</source>
 +
Kód 4.2: DAO rozhranie umožňujúce prácu s databázovou entitou area
  
<source lang="java">    @Database(entities = {Area.class, Sensor.class}, version = 1)
+
Ako posledná bola implementovaná abstraktná trieda databázy (Kód 4.3), ktorá je potomkom triedy RoomDatabase. Trieda databázy v aplikácii využíva návrhový vzor Singleton, ktorý zabezpečí vytvorenie a dostupnosť jedinej inštancie triedy databázy počas behu aplikácie. Inštanciu triedy databázy je možné vytvoriť (získať) prostredníctvom statickej metódy getInstance().
 +
 
 +
<source lang="java">     
 +
    @Database(entities = {Area.class, Sensor.class}, version = 1)
 
     public abstract class AppDatabase extends RoomDatabase {
 
     public abstract class AppDatabase extends RoomDatabase {
 
         public abstract AreaDao areaDao();
 
         public abstract AreaDao areaDao();
Riadok 142: Riadok 151:
 
           return Room.databaseBuilder(
 
           return Room.databaseBuilder(
 
             context, AppDatabase.class, DATABASE_NAME).build(); }
 
             context, AppDatabase.class, DATABASE_NAME).build(); }
     }</source>
+
     }
 +
 
 +
Kód 4.3: Trieda Room databázy (obsahujúca entity area a sensor)
 +
</source>
 +
 
 +
 
 
V triede databázy sú zadefinované všetky Java triedy jednotlivých entít, z ktorých SQLite databáza pozostáva a taktiež abstraktné metódy všetkých DAO rozhraní, prostredníctvom ktorých môžno vytvoriť inštancie rozhraní v iných triedach. V aplikácii je v triede databázy implementovaná metóda buildDatabase(), ktorá zisťuje názov aktuálneho webového servera (čítanie zo SharedPreferences - ukladanie menších dát formou kľúč - hodnota), ku ktorému je používateľ prihlásený a tento názov použije pre vytvorenie resp. otvorenie databázy s daným názvom, volaním metódy build().
 
V triede databázy sú zadefinované všetky Java triedy jednotlivých entít, z ktorých SQLite databáza pozostáva a taktiež abstraktné metódy všetkých DAO rozhraní, prostredníctvom ktorých môžno vytvoriť inštancie rozhraní v iných triedach. V aplikácii je v triede databázy implementovaná metóda buildDatabase(), ktorá zisťuje názov aktuálneho webového servera (čítanie zo SharedPreferences - ukladanie menších dát formou kľúč - hodnota), ku ktorému je používateľ prihlásený a tento názov použije pre vytvorenie resp. otvorenie databázy s daným názvom, volaním metódy build().
  
Riadok 157: Riadok 171:
 
Referenčný výber pre aktivitu Dashboard je výber zoznamu ID všetkých oblastí na základe ID prihláseného používateľa. Následne sa sekvenčne vykonávajú ďalšie výbery, ktorých parametrami sú vždy výsledky predchádzajúceho výberu. V metóde createDashboardData() sa vytvára dátová štruktúra, ktorá obsahuje vlastnosti potrebné pre zobrazenie údajov v Dashboard aktivite: názvy oblastí, sektorov a senzorov, fyzikálne jednotky senzorov, namerané hodnoty a ďalšie. Naplnená dátová štruktúra je odoslaná triede adaptéra (viď [[#vizualizacia|1.3]]), ktorá tieto dáta vizualizuje.
 
Referenčný výber pre aktivitu Dashboard je výber zoznamu ID všetkých oblastí na základe ID prihláseného používateľa. Následne sa sekvenčne vykonávajú ďalšie výbery, ktorých parametrami sú vždy výsledky predchádzajúceho výberu. V metóde createDashboardData() sa vytvára dátová štruktúra, ktorá obsahuje vlastnosti potrebné pre zobrazenie údajov v Dashboard aktivite: názvy oblastí, sektorov a senzorov, fyzikálne jednotky senzorov, namerané hodnoty a ďalšie. Naplnená dátová štruktúra je odoslaná triede adaptéra (viď [[#vizualizacia|1.3]]), ktorá tieto dáta vizualizuje.
  
Vykonávanie výberov dát z SQLite databázy prostredníctvom DAO metód predstavuje časovo náročnejšie operácie, ktoré musia prebiehať asynchrónne, v paralelnom vlákne. Pre účel vykonávania DAO metód v paralelnom vlákne je v aplikácii využitá RxJava generická trieda Maybe. znázorňuje princíp návrhového vzoru Observer, ktorý je implementovaný knižnicou RxJava a ktorý je v aplikácii využívaný pre vykonávanie dotazov na výber dát z SQLite databázy použitím triedy Maybe. Tento návrhový vzor zabezpečuje sledovanie priebehu práce časovo náročných DAO metód vykonávaných v paralelných vláknach ([[#listing:rx_operators|[listing:rx_operators]]], metóda getAllAreaIDsForUser()). Po dokončení práce týchto metód dôjde k automatickej notifikácii vlákna špecifikovaného operátorom observeOn() ([[#listing:rx_operators|[listing:rx_operators]]] - hlavné vlákno), na ktoré sú publikované výsledky prostredníctvom rozhrania Consumer a jeho metódy accept(). Metóda accept() prijíma prostredníctvom parametra výsledky vykonaného SQL dotazu danej DAO metódy.
+
Vykonávanie výberov dát z SQLite databázy prostredníctvom DAO metód predstavuje časovo náročnejšie operácie, ktoré musia prebiehať asynchrónne, v paralelnom vlákne. Pre účel vykonávania DAO metód v paralelnom vlákne je v aplikácii využitá RxJava generická trieda Maybe. znázorňuje princíp návrhového vzoru Observer, ktorý je implementovaný knižnicou RxJava a ktorý je v aplikácii využívaný pre vykonávanie dotazov na výber dát z SQLite databázy použitím triedy Maybe. Tento návrhový vzor zabezpečuje sledovanie priebehu práce časovo náročných DAO metód vykonávaných v paralelných vláknach (Kód 4.4, metóda getAllAreaIDsForUser()). Po dokončení práce týchto metód dôjde k automatickej notifikácii vlákna špecifikovaného operátorom observeOn() (Kód 4.4 - hlavné vlákno), na ktoré sú publikované výsledky prostredníctvom rozhrania Consumer a jeho metódy accept(). Metóda accept() prijíma prostredníctvom parametra výsledky vykonaného SQL dotazu danej DAO metódy.
  
 
[[File:dao_class_final.png|thumb|none|x188px|alt=Návrhový vzor Observer využitý na výber dát z SQLite databázy|Návrhový vzor Observer využitý na výber dát z SQLite databázy]]
 
[[File:dao_class_final.png|thumb|none|x188px|alt=Návrhový vzor Observer využitý na výber dát z SQLite databázy|Návrhový vzor Observer využitý na výber dát z SQLite databázy]]
  
<source lang="java">    areaDao.getAllAreaIDsForUser(userID)
+
<source lang="java">     
 +
        areaDao.getAllAreaIDsForUser(userID)
 
         .subscribeOn(Schedulers.io())
 
         .subscribeOn(Schedulers.io())
 
             .observeOn(AndroidSchedulers.mainThread())
 
             .observeOn(AndroidSchedulers.mainThread())
Riadok 167: Riadok 182:
 
                 @Override
 
                 @Override
 
                 public void accept(List<Integer> areaIDs) { }  
 
                 public void accept(List<Integer> areaIDs) { }  
             });</source>
+
             });
 +
 
 +
Kód 4.4: Asynchrónne vykonanie metódy getAllAreaIDsForUser() rozhrania AreaDao
 +
</source>
 
=== Komunikácia so vzdialeným serverom ===
 
=== Komunikácia so vzdialeným serverom ===
  
Riadok 182: Riadok 200:
 
[[File:class_diagram.png|thumb|none|x205px|alt=Zjednodušený diagram tried zabezpečujúcich komunikáciu so serverovým API|Zjednodušený diagram tried zabezpečujúcich komunikáciu so serverovým API]]
 
[[File:class_diagram.png|thumb|none|x205px|alt=Zjednodušený diagram tried zabezpečujúcich komunikáciu so serverovým API|Zjednodušený diagram tried zabezpečujúcich komunikáciu so serverovým API]]
  
Proces komunikácie so vzdialeným serverom je časovo náročná operácia, ktorá musí byť vykonávaná asynchrónne, v paralelnom vlákne. V aplikácii je pre tento účel využitá RxJava trieda Completable. znázorňuje návrhový vzor Observer, ktorý je implementovaný knižnicou RxJava a ktorý je v aplikácii využívaný pre komunikáciu so serverovým API použitím triedy Completable. Podstata tohoto návrhového vzoru spočíva v tom, že po ukončení časovo náročnej metódy ([[#listing:completable|[listing:completable]]] riadok 3, metóda run()) dôjde k automatickej notifikácii vlákna špecifikovaného operátorom observeOn(). Notifikáciu špecifikovaného vlákna ([[#listing:completable|[listing:completable]]] - hlavné vlákno) zabezpečuje rozhranie CompletableObserver a jeho metóda onComplete(), ktorá je zavolaná, ak komunikácia so serverom a spracovanie odpovede prebehli v poriadku. Samotná komunikácia so serverom prebieha v tele metódy run() volaním metódy apiDataPresenter.getAreas(), ktorá vyvolá konkrétnu požiadavku.
+
Proces komunikácie so vzdialeným serverom je časovo náročná operácia, ktorá musí byť vykonávaná asynchrónne, v paralelnom vlákne. V aplikácii je pre tento účel využitá RxJava trieda Completable. znázorňuje návrhový vzor Observer, ktorý je implementovaný knižnicou RxJava a ktorý je v aplikácii využívaný pre komunikáciu so serverovým API použitím triedy Completable. Podstata tohoto návrhového vzoru spočíva v tom, že po ukončení časovo náročnej metódy (Kód 4.5 riadok 3, metóda run()) dôjde k automatickej notifikácii vlákna špecifikovaného operátorom observeOn(). Notifikáciu špecifikovaného vlákna (Kód 4.5 - hlavné vlákno) zabezpečuje rozhranie CompletableObserver a jeho metóda onComplete(), ktorá je zavolaná, ak komunikácia so serverom a spracovanie odpovede prebehli v poriadku. Samotná komunikácia so serverom prebieha v tele metódy run() volaním metódy apiDataPresenter.getAreas(), ktorá vyvolá konkrétnu požiadavku.
  
[[File:obrazky/completable_class_final.png|thumb|none|x188px|alt=Návrhový vzor Observer využitý na komunikáciu so serverovým API|Návrhový vzor Observer využitý na komunikáciu so serverovým API]]
+
[[File:completable_class_final.png|thumb|none|x188px|alt=Návrhový vzor Observer využitý na komunikáciu so serverovým API|Návrhový vzor Observer využitý na komunikáciu so serverovým API]]
  
<source lang="java">    Completable.fromRunnable(new Runnable() {
+
<source lang="java">     
 +
          Completable.fromRunnable(new Runnable() {
 
                 @Override
 
                 @Override
 
                 public void run() {
 
                 public void run() {
Riadok 199: Riadok 218:
 
                         public void onComplete() { }
 
                         public void onComplete() { }
 
                         @Override
 
                         @Override
                         public void onError(Throwable e) { } });</source>
+
                         public void onError(Throwable e) { } });
Textový formát odpovede z API je vrátený do triedy ApiDataPresenter, v ktorej sa transformuje na JSON objekt ([[#listing:api_data_presenter|[listing:api_data_presenter]]], metóda parseAreaResponse()). V tejto metóde sa prechádza pole hodnôt s hodnotou kľúča “data”, pričom jednotlivé hodnoty tohto poľa sú opäť polia, ktorých hodnoty sa ukladajú do dátovej štruktúry, ktorá je vrátená späť do miesta volania ([[#listing:completable|[listing:completable]]], riadok 4). Spôsob vytvárania JSON objektu z prijatej odpovede, prechádzanie a ukladanie jeho hodnôt do dátovej štruktúry zobrazuje [[#listing:api_data_presenter|[listing:api_data_presenter]]]. Postupnosť prechodu medzi triedami a formát dát v jednotlivých triedach počas komunikácie s API znázorňuje .
+
 
 +
Kód 4.5: Asynchrónne sťahovanie údajov entity area a ich ukladanie do SQLite databázy
 +
</source>
 +
Textový formát odpovede z API je vrátený do triedy ApiDataPresenter, v ktorej sa transformuje na JSON objekt (Kód 4.6, metóda parseAreaResponse()). V tejto metóde sa prechádza pole hodnôt s hodnotou kľúča “data”, pričom jednotlivé hodnoty tohto poľa sú opäť polia, ktorých hodnoty sa ukladajú do dátovej štruktúry, ktorá je vrátená späť do miesta volania (Kód 4.5, riadok 4). Spôsob vytvárania JSON objektu z prijatej odpovede, prechádzanie a ukladanie jeho hodnôt do dátovej štruktúry zobrazuje Kód 4.6. Postupnosť prechodu medzi triedami a formát dát v jednotlivých triedach počas komunikácie s API znázorňuje .
  
 
<source lang="java">    public ArrayList<Area> getAreas() {  
 
<source lang="java">    public ArrayList<Area> getAreas() {  
Riadok 219: Riadok 241:
 
             }
 
             }
 
         }  
 
         }  
     }</source>
+
     }
 +
Kód 4.6: Spôsob prechádzania JSON odpovede z API, týkajúcej sa entity area
 +
</source>
 +
 
 
[[File:api_call_diagram_final.png|thumb|none|585px|alt=Prenos a formát dát v jednotlivých triedach počas komunikácie s API|Prenos a formát dát v jednotlivých triedach počas komunikácie s API]]
 
[[File:api_call_diagram_final.png|thumb|none|585px|alt=Prenos a formát dát v jednotlivých triedach počas komunikácie s API|Prenos a formát dát v jednotlivých triedach počas komunikácie s API]]
  
Riadok 246: Riadok 271:
 
V aplikácii sú vytvorené dve triedy adaptérov, kde každá z nich sprostredkováva dáta konkrétnej aktivite: DashboardAdapter (Dashboard aktivita), FavouritesAdapter (Favourites aktivita). Triedy adaptérov v aplikácii využívajú návrhový vzor Singleton, pričom vytvorenie (získanie) inštancie triedy oboch adaptérov je možné prostredníctvom statickej metódy getInstance(). Spôsob implementácie triedy adaptéra v aplikácii je demonštrovaný na jednoduchšej triede FavouritesAdapter, ktorá vykresľuje len jeden typ view holder objektu (len karta senzora, vľavo). Trieda DashboardAdapter vykresľuje tri typy view holder objektov, kde každý z nich reprezentuje kartu: oblasti, sektoru, alebo senzora ( vpravo).
 
V aplikácii sú vytvorené dve triedy adaptérov, kde každá z nich sprostredkováva dáta konkrétnej aktivite: DashboardAdapter (Dashboard aktivita), FavouritesAdapter (Favourites aktivita). Triedy adaptérov v aplikácii využívajú návrhový vzor Singleton, pričom vytvorenie (získanie) inštancie triedy oboch adaptérov je možné prostredníctvom statickej metódy getInstance(). Spôsob implementácie triedy adaptéra v aplikácii je demonštrovaný na jednoduchšej triede FavouritesAdapter, ktorá vykresľuje len jeden typ view holder objektu (len karta senzora, vľavo). Trieda DashboardAdapter vykresľuje tri typy view holder objektov, kde každý z nich reprezentuje kartu: oblasti, sektoru, alebo senzora ( vpravo).
  
Vytvorenie inštancie triedy FavouritesAdapter a následné nastavenie tejto inštancie ako adaptér objektu recylerView demonštruje kód [[#listing:adapter|[listing:adapter]]].
+
Vytvorenie inštancie triedy FavouritesAdapter a následné nastavenie tejto inštancie ako adaptér objektu recylerView demonštruje kód Kód 4.7.
  
 
<source lang="java">    RecyclerView recyclerView = findViewById(R.id.recycler_view);
 
<source lang="java">    RecyclerView recyclerView = findViewById(R.id.recycler_view);
Riadok 253: Riadok 278:
 
[[File:favourites.png|x264px|RecyclerView - aktivita Dashboard (vpravo), aktivita Favourites (vľavo)]] [[File:recycler_view_dashboard_edit.png|x264px|RecyclerView - aktivita Dashboard (vpravo), aktivita Favourites (vľavo)]]
 
[[File:favourites.png|x264px|RecyclerView - aktivita Dashboard (vpravo), aktivita Favourites (vľavo)]] [[File:recycler_view_dashboard_edit.png|x264px|RecyclerView - aktivita Dashboard (vpravo), aktivita Favourites (vľavo)]]
  
Nasledujúca ukážka ([[#listing:viewHolder|[listing:viewHolder]]]) zobrazuje implementáciu návrhového vzoru ViewHolder v triede adaptéra (FavouritesAdapter), vytvorením vnútornej triedy DataViewHolder. Trieda DataViewHolder inicializuje jednotlivé View prvky CardView rozloženia karty senzora. Každý vytvorený objekt tejto triedy reprezentuje jednu kartu v RecyclerView.
+
Nasledujúca ukážka (Kód 4.7) zobrazuje implementáciu návrhového vzoru ViewHolder v triede adaptéra (FavouritesAdapter), vytvorením vnútornej triedy DataViewHolder. Trieda DataViewHolder inicializuje jednotlivé View prvky CardView rozloženia karty senzora. Každý vytvorený objekt tejto triedy reprezentuje jednu kartu v RecyclerView.
  
 
<source lang="java">    public static class DataViewHolder extends RecyclerView.ViewHolder {
 
<source lang="java">    public static class DataViewHolder extends RecyclerView.ViewHolder {
Riadok 263: Riadok 288:
 
           itemView.findViewById(R.id.text_view_sensor_name);
 
           itemView.findViewById(R.id.text_view_sensor_name);
 
         }
 
         }
     }</source>
+
     }
Metóda triedy adaptéra - onCreateViewHolder() ([[#listing:onCreate|[listing:onCreate]]]) slúži pre dynamické vytváranie view holder objektov. Táto metóda vytvára len taký počet view holder objektov (kariet), koľko sa aktuálne zmestí na obrazovku. Pri listovaní v RecyclerView sú dynamicky vytvárané ďalšie potrebné objekty, pričom nahrádzajú tie, ktoré už na obrazovke nie sú vidieť (recycling).
+
Kód 4.7: Nastavenie adaptéra objektu triedy RecyclerView
 +
</source>
 +
 
 +
Metóda triedy adaptéra - onCreateViewHolder() (Kód 4.8) slúži pre dynamické vytváranie view holder objektov. Táto metóda vytvára len taký počet view holder objektov (kariet), koľko sa aktuálne zmestí na obrazovku. Pri listovaní v RecyclerView sú dynamicky vytvárané ďalšie potrebné objekty, pričom nahrádzajú tie, ktoré už na obrazovke nie sú vidieť (recycling).
  
 
<source lang="java">    @NonNull
 
<source lang="java">    @NonNull
Riadok 275: Riadok 303:
 
                 v = mInflater.inflate(
 
                 v = mInflater.inflate(
 
                 R.layout.card_view_sensor, viewGroup, false);
 
                 R.layout.card_view_sensor, viewGroup, false);
                 return new FavouritesAdapter.DataViewHolder(v); }</source>
+
                 return new FavouritesAdapter.DataViewHolder(v); }
Samotné pridanie vytvoreného view holder objektu do RecyclerView zabezpečuje metóda - onBindViewHolder() ([[#listing:onBind|[listing:onBind]]]). Každý view holder objekt v RecyclerView má definovanú pozíciu, na ktorej sa nachádza. Hodnotu aktuálnej pozície, na ktorú má byť ďalší view holder objekt pridaný/vykreslený prijíma metóda onBindViewHolder() prostredníctvom parametra. Na základe pozície je možné naviazať konkrétny view holder objekt na dáta.
+
 
 +
Kód 4.8: Časť vnútornej triedy DataViewHolder v triede FavouritesAdapter
 +
</source>
 +
Samotné pridanie vytvoreného view holder objektu do RecyclerView zabezpečuje metóda - onBindViewHolder() (Kód 4.9). Každý view holder objekt v RecyclerView má definovanú pozíciu, na ktorej sa nachádza. Hodnotu aktuálnej pozície, na ktorú má byť ďalší view holder objekt pridaný/vykreslený prijíma metóda onBindViewHolder() prostredníctvom parametra. Na základe pozície je možné naviazať konkrétny view holder objekt na dáta.
  
 
<source lang="java">    @override
 
<source lang="java">    @override
Riadok 283: Riadok 314:
 
         ((DataViewHolder) viewHolder).
 
         ((DataViewHolder) viewHolder).
 
         textViewSensorName.setText(
 
         textViewSensorName.setText(
         filteredList.get(position).getSensorName()); }</source>
+
         filteredList.get(position).getSensorName()); }
 +
 
 +
Kód 4.9: Vytváranie view holder objektu v triede FavouritesAdapter
 +
</source>
 
=== Vizualizácia dát v aktivite GraphView ===
 
=== Vizualizácia dát v aktivite GraphView ===
  
Prezentáciu dát v aktivite GraphView zabezpečujú grafy, vytvárané pomocou knižnice MPAndroidChart. Táto knižnica umožňuje vytvárať rôzne typy grafov: koláčové, stĺpcové, čiarové a iné. V aplikácii je využitý čiarový graf - LineChart. V aplikácii sú v grafe vykresľované dva typy čiar: LimitLine - horizontálna čiara pre zobrazenie priemernej hodnoty a samotná čiara grafu LineChart pre vykreslenie priebehu meraných hodnôt. Hodnoty osi x sú reprezentované časovými značkami jednotlivých meraní, ktoré sú explicitne konvertované na textový formát dátumu, ktorý je v grafe zobrazený ako legenda osi x. Hodnoty osi y sú reprezentované nameranými hodnotami. Jednotlivé body grafu sú vytvárané ako usporiadaná dvojica [časová značka, hodnota] ([[#listing:graph_creation|[listing:graph_creation]]] trieda Entry). Tieto usporiadané dvojice hodnôt sú postupne pridávané do poľa ([[#listing:graph_creation|[listing:graph_creation]]] objekt xyValues), ktoré je po naplnení priradené vytváranému objektu triedy LineDataSet v jej konštruktore. Metódy triedy LineDataSet umožňujú nastaviť požadované parametre čiary grafu (hrúbka, farba a iné). Objekt triedy LineDataSet je nakoniec priradený samotnému objektu grafu inicializovaného prostredníctvom metódy findViewById(). V aplikácii je v grafoch implementovaná možnosť zoomovania a taktiež možnosť zobrazenia hodnoty konkrétneho bodu pri kliknutí na ľubovoľnú hodnotu v grafe ( vpravo).
+
Prezentáciu dát v aktivite GraphView zabezpečujú grafy, vytvárané pomocou knižnice MPAndroidChart. Táto knižnica umožňuje vytvárať rôzne typy grafov: koláčové, stĺpcové, čiarové a iné. V aplikácii je využitý čiarový graf - LineChart. V aplikácii sú v grafe vykresľované dva typy čiar: LimitLine - horizontálna čiara pre zobrazenie priemernej hodnoty a samotná čiara grafu LineChart pre vykreslenie priebehu meraných hodnôt. Hodnoty osi x sú reprezentované časovými značkami jednotlivých meraní, ktoré sú explicitne konvertované na textový formát dátumu, ktorý je v grafe zobrazený ako legenda osi x. Hodnoty osi y sú reprezentované nameranými hodnotami. Jednotlivé body grafu sú vytvárané ako usporiadaná dvojica [časová značka, hodnota] (Kód 4.10 trieda Entry). Tieto usporiadané dvojice hodnôt sú postupne pridávané do poľa (Kód 4.11 objekt xyValues), ktoré je po naplnení priradené vytváranému objektu triedy LineDataSet v jej konštruktore. Metódy triedy LineDataSet umožňujú nastaviť požadované parametre čiary grafu (hrúbka, farba a iné). Objekt triedy LineDataSet je nakoniec priradený samotnému objektu grafu inicializovaného prostredníctvom metódy findViewById(). V aplikácii je v grafoch implementovaná možnosť zoomovania a taktiež možnosť zobrazenia hodnoty konkrétneho bodu pri kliknutí na ľubovoľnú hodnotu v grafe ( vpravo).
  
 
[[File:graph_view.png|151px|Vykresľovanie priebehu nameraných hodnôt v aktivite GraphView]] [[File:graph_view_land.png|264px|Vykresľovanie priebehu nameraných hodnôt v aktivite GraphView]]
 
[[File:graph_view.png|151px|Vykresľovanie priebehu nameraných hodnôt v aktivite GraphView]] [[File:graph_view_land.png|264px|Vykresľovanie priebehu nameraných hodnôt v aktivite GraphView]]
Riadok 301: Riadok 335:
 
     LineData lineData = new LineData(dataSet);
 
     LineData lineData = new LineData(dataSet);
 
     LineChart chart = findViewById(R.id.chart);
 
     LineChart chart = findViewById(R.id.chart);
     chart.setData(lineData);</source>
+
     chart.setData(lineData);
 +
 
 +
Kód 4.10: Pridávanie view holder objektu do RecyclerView a jeho naviazanie na dáta zoznamu filteredList
 +
</source>

Aktuálna revízia z 23:36, 3. september 2020

Implementácia používateľského rozhrania a jednotlivých funkcionalít aplikácie vychádza zo špecifikácie požiadaviek a navrhnutej štruktúry aplikácie v kapitole Návrh aplikácie nSoric aAurela. V tejto kapitole sú postupne opísané: implementácia používateľského rozhrania aplikácie, práca s dátami, komunikácia so serverom a samotná vizualizácia dát v aplikácii.

Používateľské rozhranie aplikácie

Jednotlivé obrazovky tvoriace používateľské rozhranie aplikácie, prostredníctvom ktorých používateľ s aplikáciou interaguje, sú implementované podľa navrhnutej štruktúry aplikácie ako jednotlivé triedy Android aktivít. Tieto Android aktivity rozdeľujú aplikáciu na dve časti: primárnu a sekundárnu.

Aktivity primárnej časti poskytujú používateľovi priamy prístup k prehľadu aktuálneho stavu meracieho systému Sensorical. Aktivity sekundárnej časti predstavujú pomocné aktivity, ktoré sú však nevyhnutné pre prístup do primárnej časti, tj. nutnosť výberu adresy servera, ku ktorému sa chce používateľ pripojiť, ako aj samotné prihlásenie do aplikácie. Android aktivity primárnej časti

  • Dashboard - aktivita, ktorá sprostredkováva prehľad všetkých aktuálne dostupných senzorov a ich údajov, kategorizovaných v rámci oblastí a sektorov
  • Favourites - aktivita, ktorá zobrazuje prehľad vybraných, obľúbených senzorov a ich údaje
  • GraphView - aktivita ktorá zobrazuje detailný prehľad konkrétneho senzora, históriu nameraných hodnôt (grafy a štatistiky)

Android aktivity sekundárnej časti

  • ServerSelect - aktivita pre výber adresy servera
  • Login - aktivita pre prihlásenie do aplikácie
  • About - aktivita zobrazujúca doplňujúce informácie o aplikácií

Android a Material Design

Material Design predstavuje dizajnérsky jazyk vyvinutý spoločnosťou Google v roku 2014, ktorý priniesol množstvo inovácií a zmien do spôsobu tvorby grafických používateľských rozhraní. Filozofia konceptu Material Design vychádza z fyzikálnych vlastností reálnych objektov ako 3D kontúry, odraz svetla, tiene…

Medzi inovácie, ktoré Material Design prináša patria nové komponenty ako napr. karty (Cards), plávajúce tlačidlo (Floating action button), oznámenia (Snackbars), animácie, prechody a iné. Najväčšou zmenou, ktorú Material Design priniesol je tzv. efekt hĺbky resp. používanie osi z. Jednotlivé komponenty sú zobrazované v pomyselných vrstvách v rôznych hladinách (radené nad sebou). Najdôležitejší komponent, s ktorým môže požívateľ aktuálne pracovať je zobrazený ponad ostatné komponenty (vo vyššej hladine osi z). Výhodou používateľských rozhraní, využívajúcich princíp radenia komponentov do vrstiev, je intuitívne prostredie, v ktorom sa používateľ ľahko zorientuje, s ktorými komponentmi môže aktuálne pracovať.

Implementácia Material Design v aplikácii

Pri implementácii používateľského rozhrania aplikácie boli odporúčania Material Design aplikované priamo v jednotlivých grafických rozhraniach Android aktivít a ich komponentoch. Tieto odporúčania (napr. odsadenie, vyvýšenie, farby, tlačidlá a tvary) presne definujú prípady použitia - kedy a ako môže byť daný komponent použitý, ako má vyzerať a ako sa má správať.

Pri tvorbe grafických používateľských rozhraní a taktiež pre exaktný popis parametrov jednotlivých komponentov sú používané abstraktné jednotky - dp (density pixel). Jednotky dp sú prepočítavané k referenčnému rozlíšeniu v pixeloch: 1dp=1 pixel na obrazovke s rozlíšením 160dpi (bodov na palec).

Material Design komponenty využité v aplikácii

Cards

Funkcia: Cards (Karty) zobrazujú obsah a akcie súvisiace s jedným senzorom (). Každá karta senzora zobrazuje: názov senzora, tlačidlo pre akciu pridania/odstránenia do/zo zoznamu obľúbených senzorov, priemernú hodnotu, poslednú nameranú hodnotu a dátum kedy bola nameraná. Na karte senzora je taktiež vykresľovaný trend meraní daného senzora. Trend predstavuje smer rastu resp. poklesu nameraných hodnôt za poslednú hodinu.

Material Design odporúčania:

  • Vyvýšenie karty od 1dp do 8dp, optimálne 2dp.
  • Ak sú v karte použité oddeľovače obsahu, nemali by prechádzať cez celú kartu, ale len cez jej časť.
Karta zobrazujúca údaje jedného senzora
Karta zobrazujúca údaje jedného senzora

Floating Action Button

Funkcia: plávajúce tlačidlo (FAB) je používané na vyhľadávanie v aktivitách prehľadu (Dashboard, Favourites). Tlačidlo je štandardne viditeľné, avšak pri posunutí v zozname smerom dolu je skryté, z dôvodu úplnej viditeľnosti údajov poslednej karty zoznamu. Pri posunutí v zozname smerom hore sa stane tlačidlo znova viditeľné.

Material Design odporúčania:

  • FAB je zobrazené ponad všetok ostatný obsah, štandardne v pravom dolnom rohu.
  • Optimálne rodičovské rozloženie - CoordinatorLayout (umožňuje automatické posunutie FAB smerom hore, napr. v prípade zobrazenia Snackbar oznámenia).
  • Odporúčaná veľkosť je 56x56dp pre zariadenia s rozlíšením nad 460dp, pre zariadenia s nižším rozlíšením 46x46dp.
  • Farba plávajúceho tlačidla je rovnaká ako hlavná farebná schéma aplikácie.
  • Odsadenie od okraja obrazovky 16dp, vyvýšenie ponad ostatný obsah do 6dp.

AppBar

Funkcia: horná nástrojová lišta sprostredkováva informácie a akcie súvisiace s aktuálnou obrazovkou. Zobrazuje názov aktuálnej obrazovky, rýchle akcie (napr. otvorenie zoznamu obľúbených) a kontextové menu.

Material Design odporúčania:

  • Názov obrazovky je umiestnený na ľavej strane nástrojovej lišty.
  • Kontextové akcie sú umiestnené na pravú stranu.
  • Ak je použité kontextové menu, je umiestené úplne na pravej strane.
  • Vyvýšenie nástrojovej lišty je štandardne 4dp.

Snackbar

Funkcia: rýchle oznámenia ukotvené k spodnej časti obrazovky, ktoré informujú o aktuálnom dianí v aplikácii.

Material Design odporúčania:

  • Optimálne rodičovské rozloženie - CoordinatorLayout.
  • Snackbar má byť zobrazený len po určitú dobu, bez nutnosti explicitného zrušenia používateľom.

Material Design komponenty použité v aplikácii: Snackbar (aktivita Login), AppBar, Card a FAB (aktivita Dashboard) Material Design komponenty použité v aplikácii: Snackbar (aktivita Login), AppBar, Card a FAB (aktivita Dashboard)

Práca s dátami v aplikácii

Dáta, s ktorými aplikácia pracuje sú sťahované zo vzdialených serverov a po stiahnutí sú ukladané do SQLite databázy, z ktorej sú sprostredkovávané v aplikácii. Sťahovanie najaktuálnejších meraní prebieha automaticky vždy po spustení resp. prihlásení do aplikácie. V prípade potreby manuálnej aktualizácie nameraných hodnôt, prípadne synchronizácie všetkých dát s aktuálne dostupnými dátami daného servera sú v aplikácii implementované dva rôzne spôsoby manuálnej synchronizácie: aktualizácia len nameraných hodnôt alebo synchronizácia všetkých dát.

Pre zefektívnenie a zrýchlenie sťahovania nameraných hodnôt sa v aplikácii vytvárajú záznamy o dátumoch, za ktoré boli namerané hodnoty úspešne stiahnuté. Vždy pred každým ďalším sťahovaním nových meraní sú tieto záznamy prečítané a stiahnu sa len merania tých dátumov, ktoré nie sú evidované ako stiahnuté.

Keďže aplikácia slúži na monitorovanie aktuálneho stavu a krátkodobejších trendov, maximálny rozsah zobrazenia nameraných hodnôt bol stanovený na jeden týždeň. Aby sa predišlo prílišnému zväčšeniu veľkosti aplikácie dôsledkom sťahovania množstva údajov, sú po každom stiahnutí najnovších nameraných hodnôt vymazané nepotrebné záznamy (merania) staršie ako jeden týždeň.

Implementácia SQLite databázy

SQLite databáza, ktorá slúži na archiváciu stiahnutých dát a ich následnú prezentáciu v aplikácii, bola vytvorená pomocou knižnice Room, pričom je implementovaná tak, aby presne korešpondovala s dátovým modelom systému Sensorical (). Aplikácia je navrhnutá tak, že umožňuje vytvárať a pracovať s ľubovoľným počtom SQLite databáz, ktoré sú vytvárané na základe webovej adresy servera, ku ktorej je používateľ aktuálne prihlásený.

Každý server, ku ktorému sa používateľ prihlási, má v aplikácii vytvorenú vlastnú SQLite databázu. Vždy po úspešnom prihlásení k niektorému zo serverov sa aplikácia pokúsi vytvoriť novú databázu s názvom webovej adresy daného servera. Ak už databáza s takýmto názvom existuje, aplikácia ju použije a otvorí.

Pri samotnej implementácii SQLite databázy v aplikácii, boli ako prvé vytvorené jednotlivé triedy reprezentujúce entity databázy (Listing 4.1). Každá trieda entity obsahuje premenné, ktoré predstavujú jednotlivé atribúty danej entity. Každá premenná triedy entity je prístupná prostredníctvom metód get a set.

    
@Entity(tableName = "area")
    public class Area {
        @PrimaryKey
        private int id;
        private String name;

        public void setId(int id) { this.id = id; }
        public int getId() { return id; }
        public String getName() { return name; }
        public void setName(String name) { this.name = name; }
    }

Kód 4.1: Trieda entity area

Ako ďalšie boli vytvorené potrebné DAO rozhrania (Kód 4.2) a ich metódy pre prístup k dátam databázy. Návratové typy týchto metód boli zvolené na základe očakávaného výsledku SQL dotazu, ktorý daná metóda vykonáva. V prípade DAO metód, ktoré slúžia na výber údajov z SQLite databázy, sú návratové typy zabalené do generickej triedy Maybe, knižnice RxJava, ktorá umožňuje vykonávať prácu týchto metód asynchrónne v paralelných vláknach.

DAO metódy, ktoré do SQLite databázy vkladajú záznamy nameraných hodnôt, využívajú v prípade konfliktu hodnôt primárnych kľúčov parameter IGNORE (ignorovať). Parameter IGNORE zabezpečí neprepísanie existujúceho záznamu v SQLite databáze, pretože nameraná hodnota nemôže byť spätne modifikovaná a teda nie je nutné už existujúce záznamy prepisovať (skrátenie času vykonávania operácie).

DAO metódy zabezpečujúce vkladanie ostatných údajov, majú v prípade konfliktu hodnôt primárnych kľúčov nastavený parameter REPLACE (nahradiť). V prípade modifikácie záznamu na strane servera sa síce nemení hodnota jeho primárneho kľúča, ale hodnota niektorého z atribútov je zmenená. Parameter REPLACE v takomto prípade zabezpečí prepísanie záznamu uloženého v SQLite databáze modifikovaným záznamom stiahnutým zo servera.

    
   @Dao
    public interface AreaDao {
        @Insert(onConflict = OnConflictStrategy.REPLACE)
        void insertAllAreas(List<Area> areas);

        @Query("select group_view.area_id from group_view
        where group_view.user_id=:userID")
        Maybe<List<Integer>> getAllAreaIDsforUser(int userID); 
    }

Kód 4.2: DAO rozhranie umožňujúce prácu s databázovou entitou area

Ako posledná bola implementovaná abstraktná trieda databázy (Kód 4.3), ktorá je potomkom triedy RoomDatabase. Trieda databázy v aplikácii využíva návrhový vzor Singleton, ktorý zabezpečí vytvorenie a dostupnosť jedinej inštancie triedy databázy počas behu aplikácie. Inštanciu triedy databázy je možné vytvoriť (získať) prostredníctvom statickej metódy getInstance().

    
    @Database(entities = {Area.class, Sensor.class}, version = 1)
    public abstract class AppDatabase extends RoomDatabase {
        public abstract AreaDao areaDao();
        private static AppDatabase INSTANCE;
        
        public static AppDatabase getInstance(final Context context){
            synchronized (AppDatabase.class) {
                if (INSTANCE == null){
                  INSTANCE = buildDatabase(context.getApplicationContext()); }
        } return INSTANCE; }
        
        private static AppDatabase buildDatabase(final Context context){
          String DATABASE_NAME = context
            .getSharedPreferences("preferences", Context.MODE_PRIVATE)
            .getString("server_name", "default_name");
          return Room.databaseBuilder(
            context, AppDatabase.class, DATABASE_NAME).build(); }
    }

Kód 4.3: Trieda Room databázy (obsahujúca entity area a sensor)


V triede databázy sú zadefinované všetky Java triedy jednotlivých entít, z ktorých SQLite databáza pozostáva a taktiež abstraktné metódy všetkých DAO rozhraní, prostredníctvom ktorých môžno vytvoriť inštancie rozhraní v iných triedach. V aplikácii je v triede databázy implementovaná metóda buildDatabase(), ktorá zisťuje názov aktuálneho webového servera (čítanie zo SharedPreferences - ukladanie menších dát formou kľúč - hodnota), ku ktorému je používateľ prihlásený a tento názov použije pre vytvorenie resp. otvorenie databázy s daným názvom, volaním metódy build().

Práca s SQLite databázou

Práca s SQLite databázou, konkrétne vykonávanie dotazov na výber údajov pre niektorú z aktivít prehľadu (Dashboard, Favourites, GraphView) je v aplikácii realizované sekvenčne. Najskôr sa vykoná referenčný výber, pričom všetky ostatné dotazy čakajú vo fronte. V momente, kedy sa aktuálny výber dokončí, začne sa vykonávať nasledujúci (čakajúci). Sekvenčný spôsob vykonávania práce je zvolený z dôvodu závislosti vstupných parametrov (podmienok pre vykonanie) čakajúceho dotazu, na výsledku jemu predchádzajúcemu výberu.

Aby aplikácia zobrazila len ten obsah, na ktorý má prihlásený používateľ pridelené práva je využitý jedinečný identifikátor každého používateľa aplikácie - ID. Hodnota ID daného používateľa je využitá práve pri referenčnom výbere, na základe ktorého sú vybrané len tie vstupné parametre, na ktoré má používateľ definované oprávnenia.

demonštruje postupnosť volania DAO metód, ktoré vyberajú z SQLite databázy údaje pre aktivitu Dashboard. Obdobným spôsobom sú riešené výbery pre ostatné dve aktivity prehľadov (Favourites a GraphView).

Volanie DAO metód na výber dát z SQLite databázy pre aktivitu Dashboard
Volanie DAO metód na výber dát z SQLite databázy pre aktivitu Dashboard

Referenčný výber pre aktivitu Dashboard je výber zoznamu ID všetkých oblastí na základe ID prihláseného používateľa. Následne sa sekvenčne vykonávajú ďalšie výbery, ktorých parametrami sú vždy výsledky predchádzajúceho výberu. V metóde createDashboardData() sa vytvára dátová štruktúra, ktorá obsahuje vlastnosti potrebné pre zobrazenie údajov v Dashboard aktivite: názvy oblastí, sektorov a senzorov, fyzikálne jednotky senzorov, namerané hodnoty a ďalšie. Naplnená dátová štruktúra je odoslaná triede adaptéra (viď 1.3), ktorá tieto dáta vizualizuje.

Vykonávanie výberov dát z SQLite databázy prostredníctvom DAO metód predstavuje časovo náročnejšie operácie, ktoré musia prebiehať asynchrónne, v paralelnom vlákne. Pre účel vykonávania DAO metód v paralelnom vlákne je v aplikácii využitá RxJava generická trieda Maybe. znázorňuje princíp návrhového vzoru Observer, ktorý je implementovaný knižnicou RxJava a ktorý je v aplikácii využívaný pre vykonávanie dotazov na výber dát z SQLite databázy použitím triedy Maybe. Tento návrhový vzor zabezpečuje sledovanie priebehu práce časovo náročných DAO metód vykonávaných v paralelných vláknach (Kód 4.4, metóda getAllAreaIDsForUser()). Po dokončení práce týchto metód dôjde k automatickej notifikácii vlákna špecifikovaného operátorom observeOn() (Kód 4.4 - hlavné vlákno), na ktoré sú publikované výsledky prostredníctvom rozhrania Consumer a jeho metódy accept(). Metóda accept() prijíma prostredníctvom parametra výsledky vykonaného SQL dotazu danej DAO metódy.

Návrhový vzor Observer využitý na výber dát z SQLite databázy
Návrhový vzor Observer využitý na výber dát z SQLite databázy
    
         areaDao.getAllAreaIDsForUser(userID)
        .subscribeOn(Schedulers.io())
            .observeOn(AndroidSchedulers.mainThread())
                .subscribe(new Consumer<List<Integer>>() {
                @Override
                 public void accept(List<Integer> areaIDs) { } 
            });

Kód 4.4: Asynchrónne vykonanie metódy getAllAreaIDsForUser() rozhrania AreaDao

Komunikácia so vzdialeným serverom

Pre komunikáciu aplikácie so serverovým API je využitý socketový dátový prenos. Požiadavky/odpovede sú odosielané/prijímané v textovom formáte JSON. Po prijatí odpovede sa prechádza tá časť odpovede, ktorá obsahuje relevantné dáta (hodnota kľúča JSON objektu “data”). Časť odpovede “data” a jej hodnoty sa prechádzajú a ukladajú do dátových štruktúr (triedy jednotlivých entít), ktoré sú po naplnení vložené do SQLite databázy.

Vytvorenie, odoslanie a spracovanie požiadavky

Vytvorenie a odoslanie požiadavky na API a následné spracovanie odpovede zabezpečujú v aplikácii tri triedy (ClientSocket, ApiResponses, ApiDataPresenter), ktorých zjednodušenú štruktúru znázorňuje diagram tried na obrázku 1.6.

  • trieda ClientSocket (odoslanie požiadavky) - konvertuje požiadavku do formátu JSON, otvára spojenie so serverom, odosiela požiadavku, čaká na odpoveď a vracia ju späť metóde triedy ApiResponses, ktorá požiadavku vyvolala.
  • trieda ApiResponses (vytvorenie požiadavky) - vytvára a posiela triede ClientSocket údaje potrebné pre odoslanie požiadavky prostredníctvom parameterov metódy sendRequest(). Trieda ApiResponses zároveň prijíma a vracia späť textovú odpoveď metóde triedy ApiDataPresenter, ktorá danú požiadavku vyvolala.
  • trieda ApiDataPresenter (spracovanie odpovede) - inicializuje proces vytvárania požiadavky volaním príslušnej metódy triedy ApiResponses, transformuje prijatú odpoveď na JSON objekt, ktorého hodnoty ukladá do dátovej štruktúry.
Zjednodušený diagram tried zabezpečujúcich komunikáciu so serverovým API
Zjednodušený diagram tried zabezpečujúcich komunikáciu so serverovým API

Proces komunikácie so vzdialeným serverom je časovo náročná operácia, ktorá musí byť vykonávaná asynchrónne, v paralelnom vlákne. V aplikácii je pre tento účel využitá RxJava trieda Completable. znázorňuje návrhový vzor Observer, ktorý je implementovaný knižnicou RxJava a ktorý je v aplikácii využívaný pre komunikáciu so serverovým API použitím triedy Completable. Podstata tohoto návrhového vzoru spočíva v tom, že po ukončení časovo náročnej metódy (Kód 4.5 riadok 3, metóda run()) dôjde k automatickej notifikácii vlákna špecifikovaného operátorom observeOn(). Notifikáciu špecifikovaného vlákna (Kód 4.5 - hlavné vlákno) zabezpečuje rozhranie CompletableObserver a jeho metóda onComplete(), ktorá je zavolaná, ak komunikácia so serverom a spracovanie odpovede prebehli v poriadku. Samotná komunikácia so serverom prebieha v tele metódy run() volaním metódy apiDataPresenter.getAreas(), ktorá vyvolá konkrétnu požiadavku.

Návrhový vzor Observer využitý na komunikáciu so serverovým API
Návrhový vzor Observer využitý na komunikáciu so serverovým API
    
           Completable.fromRunnable(new Runnable() {
                @Override
                public void run() {
                areaDao.insertAllAreas(apiDataPresenter.getAreas());
                }
            }).subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread()
                    .subscribe(new CompletableObserver() {
                        @Override
                        public void onSubscribe(Disposable d) { }
                        @Override
                        public void onComplete() { }
                        @Override
                        public void onError(Throwable e) { } });

Kód 4.5: Asynchrónne sťahovanie údajov entity area a ich ukladanie do SQLite databázy

Textový formát odpovede z API je vrátený do triedy ApiDataPresenter, v ktorej sa transformuje na JSON objekt (Kód 4.6, metóda parseAreaResponse()). V tejto metóde sa prechádza pole hodnôt s hodnotou kľúča “data”, pričom jednotlivé hodnoty tohto poľa sú opäť polia, ktorých hodnoty sa ukladajú do dátovej štruktúry, ktorá je vrátená späť do miesta volania (Kód 4.5, riadok 4). Spôsob vytvárania JSON objektu z prijatej odpovede, prechádzanie a ukladanie jeho hodnôt do dátovej štruktúry zobrazuje Kód 4.6. Postupnosť prechodu medzi triedami a formát dát v jednotlivých triedach počas komunikácie s API znázorňuje .

    public ArrayList<Area> getAreas() { 
        parseAreaResponse(); 
        return this.areaList; 
    }
    
    private void parseAreaResponse() {
        String serverResponse = apiResponses.getAreaResponse();
        if (!serverResponse.isEmpty()) {
            JSONObject response = new JSONObject(serverResponse);
            JSONArray array = response.getJSONArray("data");
            for (int i = 0; i < array.length(); i++) {
                Area area = new Area();
                area.setId(array.getJSONArray(i).getInt(0));
                area.setName(array.getJSONArray(i).getString(1));
                this.areaList.add(area);
            }
        } 
    }
Kód 4.6: Spôsob prechádzania JSON odpovede z API, týkajúcej sa entity area
Prenos a formát dát v jednotlivých triedach počas komunikácie s API
Prenos a formát dát v jednotlivých triedach počas komunikácie s API

Vizualizácia dát v aplikácii

Prezentáciu dát v aktivitách prehľadu (Dashboard, Favourites) zabezpečujú komponenty - CardView, RecyclerView a Adaptér. V aktivite detailného prehľadu konkrétneho senzora (GraphView) sú dáta vizualizované prostredníctvom grafov - knižnica MPAndroidChart.

Vizualizácia dát v aktivitách Dashboard a Favourites

CardView

CardView je grafická reprezentácia karty (Card 1.1.2), predstavuje rodičovské rozloženie pre všetky v nej obsiahnuté View prvky (textové polia, tlačidlá, obrázky…).

RecyclerView

RecylerView predstavuje model, ktorý umožňuje zobraziť väčšie množstvo údajov vo forme zoznamu. V aplikácii sú ako jednotlivé položky zobrazené v RecyclerView použité karty. Výhodou používania RecyclerView je jeho rýchlosť, pretože sa nevykresľujú (nevytvárajú) naraz všetky karty, ale vytvárajú sa len tie, ktoré sa aktuálne zmestia na obrazovku. Pre naplnenie RecyclerView kartami a ich následné zobrazenie sa využívajú návrhové vzory (triedy) ViewHolder a Adaptér. Jednotlivé karty zobrazené v RecyclerView sú reprezentované objektmi nazývanými view holders. Tieto objekty sú inštanciami tried, ktoré sú potomkami triedy RecyclerView.ViewHolder.

ViewHolder

ViewHolder je návrhový vzor, ktorý zabezpečuje vytvorenie objektu daného CardView rozloženia spolu s jej View prvkami. Takto vytvorený objekt môže byť použitý viacnásobne, bez nutnosti opakovanej inicializácie View prvkov tohto objektu, volaním metódy findViewById(), čo znižuje pamäťovú náročnosť a zároveň výrazne zrýchľuje proces listovania v RecyclerView. Vytvorené view holder objekty reprezentujú karty, ktoré sú zobrazené v RecyclerView. Tento návrhový vzor možno použiť vytvorením vnútornej triedy, potomok triedy RecyclerView.ViewHolder, v triede adaptéra.

Adaptér

Adaptér je návrhový vzor, ktorý funguje ako most medzi používateľským rozhraním a dátami. V Androide predstavuje adaptér triedu, ktorá sprostredkováva dáta RecyclerView. Trieda adaptéra (potomok triedy RecyclerView.Adapter) zabezpečuje správu jednotlivých view holder objektov prostredníctvom preťažených metód rodičovskej triedy: vytvorenie view holder objektov - metóda onCreateViewHolder, pridávanie vytvorených view holder objektov do RecyclerView - metóda onBindViewHolder().

V aplikácii sú vytvorené dve triedy adaptérov, kde každá z nich sprostredkováva dáta konkrétnej aktivite: DashboardAdapter (Dashboard aktivita), FavouritesAdapter (Favourites aktivita). Triedy adaptérov v aplikácii využívajú návrhový vzor Singleton, pričom vytvorenie (získanie) inštancie triedy oboch adaptérov je možné prostredníctvom statickej metódy getInstance(). Spôsob implementácie triedy adaptéra v aplikácii je demonštrovaný na jednoduchšej triede FavouritesAdapter, ktorá vykresľuje len jeden typ view holder objektu (len karta senzora, vľavo). Trieda DashboardAdapter vykresľuje tri typy view holder objektov, kde každý z nich reprezentuje kartu: oblasti, sektoru, alebo senzora ( vpravo).

Vytvorenie inštancie triedy FavouritesAdapter a následné nastavenie tejto inštancie ako adaptér objektu recylerView demonštruje kód Kód 4.7.

    RecyclerView recyclerView = findViewById(R.id.recycler_view);
    FavouritesAdapter adapter = FavouritesAdapter.getInstance();
    recyclerView.setAdapter(adapter);

RecyclerView - aktivita Dashboard (vpravo), aktivita Favourites (vľavo) RecyclerView - aktivita Dashboard (vpravo), aktivita Favourites (vľavo)

Nasledujúca ukážka (Kód 4.7) zobrazuje implementáciu návrhového vzoru ViewHolder v triede adaptéra (FavouritesAdapter), vytvorením vnútornej triedy DataViewHolder. Trieda DataViewHolder inicializuje jednotlivé View prvky CardView rozloženia karty senzora. Každý vytvorený objekt tejto triedy reprezentuje jednu kartu v RecyclerView.

    public static class DataViewHolder extends RecyclerView.ViewHolder {
        private TextView textViewSensorName;
        // iné View prvky CardView rozloženia karty senzora...
        public DataViewHolder(@NonNull final View itemView) {
          super(itemView);
          textViewSensorName = 
          itemView.findViewById(R.id.text_view_sensor_name);
        }
    }
Kód 4.7: Nastavenie adaptéra objektu triedy RecyclerView

Metóda triedy adaptéra - onCreateViewHolder() (Kód 4.8) slúži pre dynamické vytváranie view holder objektov. Táto metóda vytvára len taký počet view holder objektov (kariet), koľko sa aktuálne zmestí na obrazovku. Pri listovaní v RecyclerView sú dynamicky vytvárané ďalšie potrebné objekty, pričom nahrádzajú tie, ktoré už na obrazovke nie sú vidieť (recycling).

    @NonNull
    @override
    public RecyclerView.ViewHolder onCreateViewHolder(
    @NonNull ViewGroup viewGroup, int i) {
                View v;
                LayoutInflater mInflater = 
                LayoutInflater.from(viewGroup.getContext());
                v = mInflater.inflate(
                R.layout.card_view_sensor, viewGroup, false);
                return new FavouritesAdapter.DataViewHolder(v); }

Kód 4.8: Časť vnútornej triedy DataViewHolder v triede FavouritesAdapter

Samotné pridanie vytvoreného view holder objektu do RecyclerView zabezpečuje metóda - onBindViewHolder() (Kód 4.9). Každý view holder objekt v RecyclerView má definovanú pozíciu, na ktorej sa nachádza. Hodnotu aktuálnej pozície, na ktorú má byť ďalší view holder objekt pridaný/vykreslený prijíma metóda onBindViewHolder() prostredníctvom parametra. Na základe pozície je možné naviazať konkrétny view holder objekt na dáta.

    @override
    public void onBindViewHolder(
    @NonNull RecyclerView.ViewHolder viewHolder, int position) {
        ((DataViewHolder) viewHolder).
        textViewSensorName.setText(
        filteredList.get(position).getSensorName()); }

Kód 4.9: Vytváranie view holder objektu v triede FavouritesAdapter

Vizualizácia dát v aktivite GraphView

Prezentáciu dát v aktivite GraphView zabezpečujú grafy, vytvárané pomocou knižnice MPAndroidChart. Táto knižnica umožňuje vytvárať rôzne typy grafov: koláčové, stĺpcové, čiarové a iné. V aplikácii je využitý čiarový graf - LineChart. V aplikácii sú v grafe vykresľované dva typy čiar: LimitLine - horizontálna čiara pre zobrazenie priemernej hodnoty a samotná čiara grafu LineChart pre vykreslenie priebehu meraných hodnôt. Hodnoty osi x sú reprezentované časovými značkami jednotlivých meraní, ktoré sú explicitne konvertované na textový formát dátumu, ktorý je v grafe zobrazený ako legenda osi x. Hodnoty osi y sú reprezentované nameranými hodnotami. Jednotlivé body grafu sú vytvárané ako usporiadaná dvojica [časová značka, hodnota] (Kód 4.10 trieda Entry). Tieto usporiadané dvojice hodnôt sú postupne pridávané do poľa (Kód 4.11 objekt xyValues), ktoré je po naplnení priradené vytváranému objektu triedy LineDataSet v jej konštruktore. Metódy triedy LineDataSet umožňujú nastaviť požadované parametre čiary grafu (hrúbka, farba a iné). Objekt triedy LineDataSet je nakoniec priradený samotnému objektu grafu inicializovaného prostredníctvom metódy findViewById(). V aplikácii je v grafoch implementovaná možnosť zoomovania a taktiež možnosť zobrazenia hodnoty konkrétneho bodu pri kliknutí na ľubovoľnú hodnotu v grafe ( vpravo).

Vykresľovanie priebehu nameraných hodnôt v aktivite GraphView Vykresľovanie priebehu nameraných hodnôt v aktivite GraphView

    ArrayList<Entry> xyValues = new ArrayList<>();
        for (int i = 0; i < values.size; i++) {
            Entry xy = new Entry(values.get(i).getDate().getTime(),
                values.getValue());
            xyValues.add(xy);
        }
    LineDataSet dataSet = new LineDataSet(xyValues, "hodnoty");
    dataSet.setLineWidth(2f);
    // iné nastavenia vlastností dataSetu... 
    LineData lineData = new LineData(dataSet);
    LineChart chart = findViewById(R.id.chart);
    chart.setData(lineData);

Kód 4.10: Pridávanie view holder objektu do RecyclerView a jeho naviazanie na dáta zoznamu filteredList