Rad sa SQLite bazom u Androidu uz pomoć Room bibiloteke

Uvod

Room je bibloteka preporučena od strane Google-a kao jedna od komponenti tzv. “Android Architecture Components” pristupa. Room je wrapper oko SQLite baze i predstavlja sloj apstrakcije koji olakšava rad sa bazom podataka. Room bibilioteka preuzima na sebe većinu obaveza tako da sada lakše kreiramo tabele i upravljamo podacima.
Room ima tzv. compile-time checks tj. kontrolu koda pri kompajliranju i ukoliko postoji greška ona će se pokazati pri samom kompajliranju. Room nas na taj način spašava iritantnih sitnih grešaka koje nastaju u radu sa SQL bazom (nedostatak tačka zareza ili razmaka…) a koje izazivaju RunTimeException.

Da bi radili sa ovom bibliotekom potrebno je da se definišu dependencies:

room and MVVM diagram

Room biblioteka se sastoji iz tri glavne komponente:

Entity (tabela)

Entity je klasa koja predstavlja tabelu baze podataka a obeležava se sa @Entity. U nastavku ove anotacije (u okviru zagrada) možemo definisati naziv tabele koji može biti drugačiji od samog naziva klase. Ukoliko ne definišemo ime tabele na ovaj način, Room će generisati tabelu sa istim imenom kao što je naziv klase.

Svaki entitet mora imati konstruktor! Konstruktor se najčešće definiše tako da mu se parametri podudaraju sa poljima (na osnovu tipa i imena). Medjutim konstruktor ne mora da primi sva polja kao parametre, polje koje ne želimo da stavimo u konstruktor moramo da obeležimo sa anotacijom @Ignore u suprotnom će Android studio prikazati grešku. Takodje u okviru konstruktora ne moramo da stavimo polje koje se autogeneriše (tj. autoGenerate = true), ali za to polje mora da postoji public setter (mi ga nećemo koristi ali neophodan je Room biblioteci).

@PrimaryKey

U radu sa Room biblioteko jedna kolona mora da ima definisan tzv. PrimaryKey. Obeležavanje kolone koja će predstavljati “Primary key” se vrši sa dodavanjem anotacije @PrimaryKey kod tog polja. Kad definišemo da je neko polje PrimaryKey onda možemo da koristimo njegovu metodu autoGenerate() ili jednostavnije samo u zagradama da dodamo (autoGenerate = true).

Primer

U ovome primeru prvo polje mId se automatski generiše, pa ga nećemo ubaciti u konstruktor, ali je ipak neophodno da se obezbedi njegov setter jer je potreban Room biblioteci (za ostala polja to nisu neophodni setteri).

NAPOMENA:
Konstruktor eventualno može biti i bez ijednog argumenata ali tada moraju postojati definisani setteri za sva polja.

@ColumnInfo

Room po default-u generiše tabelu sa nazivom kolone istio kao i polje klase, medjutim ukoliko želimo da naziv kolone ima drugačiji naziv od naziva polja koje predstavlja, onda moramo koristiti @ColumnInfo anotaciju i u zagradi definisti drugačije ime kolone.

@Embedded

Sa ovom notacijom je moguće ugneziditi jednu tabelu unutar druge (one-one reacija).

Sada možemo praviti upite (query) u ovoj tabeli a User objekat ima sve kolone: id, firstName, street, state, city, i post_code.

NAPOMENA:
Možemo dodati prefiks svim nazivima kolona ubačene (embendded) tabel ako to definišemo u zagradi sa prefiks:

@ForeignKey

Sa ovom anotacijom povezujemo dva entiteta (tabele) povezujeći njihove kolone (kolona iz child entitea sa vrednosti kolone iz parent entiteta). Ovo je praktično dodatna anotacija u okviru @Entity anotacije child entiteta:

Primer

Prvo definišemo parent entitet (tabelu) Course.

Prethodna tabela može biti povezana sa više studenata, pa ćemo za definisanje child tabele Student koristiti anotaciju @ForeignKey:

U ovome primeru smo povezali vrednosti id kolone User tabele sa vrednosti userId Repo tabele. Značenje dodeljenih vrednosti za onDelete/onUpdate su sledeće:

  • int CASCADE – Akcija „CASCADE“ propagira operaciju brisanja ili ažuriranja nadređenog ključa na svaki zavisni podređeni ključ.
  • int NO_ACTION – Podrazumevano ponašanje kada se roditeljski ključ modifikuje ili izbriše iz baze podataka, i ne preduzimaju se neke druge posebne radnje.
  • int RESTRICT – Akcija RESTRICT znači da je aplikaciji zabranjeno brisanje (za onDelete ()) ili menjanje (za onUpdate ()) nadređenog ključa kada postoji jedan ili više podređenih ključeva koji su na njega preslikani.
  • int SET_DEFAULT – Akcije “SET DEFAULT” slične su SET_NULL, osim što je svaki od podređenih stupaca ključa postavljen tako da sadrži podrazumevanu vrednost stupaca umesto NULL.

NAPOMENA:
Poznato je da kreiranje ovakve konekcije ne mora da vodi do relacije izmedju tih tabela, već samo pomaže da se jasno definiše šta će se desiti sa “child entitetom” kada se neki član “parent entiteta” obriše (onDelete) ili ažurira (onUpdate).

@Relations

Sa ovom anotacijom je omogućeno povezivanje dve tabele bez korišćenja @Foreign.

Primer

U prethodnom primeru je prikazano povezivanje dve tabele uz pomoć anotacije @ForeignKey, isti zahtev možemo rešiti bez Foreign kluča koristeći drugu notaciju pod imenom @Relation. Za to je neophodno kreirati novu klasu sa kojom možemo napraviti instancu koja sadrži i parent entity instancu i listu child entity instanci:

Kasnije se za kreira DAO na sledeći način:

Više o ovome u narednoj sekciji.

Data Access Object – DAO

DAO (data access object) kao što mu i samo ime kaže je: objekat za pristup podacima. DAO mora biti ili interfejs ili abstraktna klasa, tj. njegove metode nemaju telo jer će Room generisati sav nepohodan kod u zavisnosti od anotacije (@Insert, @Delete, @Query…). Upravo na taj način se smanjuje količina kod-a koju je neophodno da kreira programer.
Takodje velika prednost ovog objekta je mogućnost da validira SQL naredbe u toku kompajliranja (eng. “at compile-time”) i na taj način nam pravovremeno ukaže za greške što nije bilo moguće u radu sa SQLiteOpenHelper klasom.

@Dao

Sa ovom anotacijom se Room-u daje do znanja da je dati interfejs ili abstraktna klasa ustvari DAO. Generalno se za svaki entitet pravi njegov DAO, pa bi za naš entitet iz primera “BuyItem” Dao ovako izgledao:

@Insert

Sa ovom anotacijom se obeležava metoda koja je zadužena za unos podataka u bazu.

Ovo je sasvim dovoljno da zameni celu metodu insertItemToDB() koju smo koristili u okviru primera iz SQLiteOpenHelper klase:

@Delete

Sa ovom anotacijom se obeležava metoda koja je zadužena za brisanje podataka iz baze.

Ovo je sasvim dovoljno da zameni celu metodu removeItemFromDB() koju smo koristili u okviru primera iz SQLiteOpenHelper klase:

@Query

Sa ovom anotacijom se obeležava metoda koja je zadužena za dobijanje podataka iz baze u zavisnosti od definisnog uslova (query-ja):

Pri pisanju ovog queryja možete primetiti da Android studio ukazuje na greške ib nudi rešenja i promenjive. A ova dva reda su sasvim dovoljno da zamene celu metodu getAllItemsFromDB() koju smo koristili u okviru primera iz SQLiteOpenHelper klase.

Prosledjivanje parametra u query

Često je potrebno proslediti neki parametar sa kojim se filtira query pa to izgleda kao u sledećem primeru:

Primer

Čak možemo proslediti više parametara kao u sledećem primeru:

Primer

Takodje možemo proslediti kao parametari i neku kolekciju (može da vraća LiveData objekat):

NAPOMENA:
Kada prosleđujete podatke kroz slojeve arhitekture aplikacije (iz baze podataka Room, kroz Repository klasu, dalje zatim kroz ViewModel klasu sve do korisničkog interfejsa tj. View klase (aktivnost ili fragmenta), ti podaci moraju biti LiveData u svim slojevima, ili drugim rečima svi podaci koje Room šalje iz DAO kroz neki query u Repository, a zatim iz Repository u VievModel, moraju biti LiveData. Objašnjenje ovoga leži u tome da nigde u aplikaciji nemamo potrebu da setujemo to radi Room za nas, tako da nam ne treba nigde MutableLiveData objekat (koji za razliku od LiveData objekta ima public settere) metode).

Primer

Ovako izgleda jedan tipičan Dao interfejs:

DataBase

Klasa koja predstavalja Room bazu podataka mora da bude abstract i da ekstenduje klasu RoomDatabase:

Pored ovoga je potrebno da obeležimo ovu klasu da bi Room znao o kojoj klasi je reč, a to se postiže kroz anotaciju @Database. U nastavku ove anotacije (u zagradi) definišemo koje sve entitete (tabele) ova baza sadrži, kao i trenutnou verziju baze:

Kreiranje baze

Pri kreiranju baza je “pametno” da koristimo singleton patern. Baza se kreira uz pomoć Room metode pod nazivom databaseBuilder(). Ova metoda prihvata parametre: context, “klasu koja definiše bazu” i naziv baze. Da bi se baza kreirala i inicijalizovala potrebno je da pozovemo metodu build():

Ako želimo da sprečimo probleme pri migraciji baza onda je dobro da pozovemo metodu fallbackToDestructiveMigration().

NAPOMENA:
Ako radi debuging-a želite da pristupite bazi (koristeći DeviceFileExplore) i da je pregledate koristeći DB Browser for SQLite ili neku sličnu aplikaciju, potrebno je da pri kreiranju baze pozovete i metodu setJournalMode(JournalMode.TRUNCATE), u protivnom će baza koju pregledate će biti prazna.

Pored ovaga potrebno je kreirati abstraktnu metodu koja će da vraća odgovarajući Dao objekat:

Primer

Ovako izgleda jedna klasa u celosti:

Repository

Iako ova klasa ne pripada direktno Room biblioteci već je hijerarhijski iznad nje, ipak u ovome članku će biti obradjena zbog toga što Room biblioteka ne dozvoljava izvršenje operacija nad bazom iz glavnog threed-a! Zbog ovoga razloga sve metode vezane za CRUD operacije moraju da se izvršavaju na background thread-u, a jedini izuzetak je metoda koja vraća LiveData objekat, jer o njoj Room vodi računa i automatski je poziva iz background thread-a.

Jedan od načina da se neka metoda izvrši na background thread-u je da ona extenduje AsyncTask klasu, medjutim od verzije androida 11 (API 30) je klasa AsyncTask ukinuta, te je neophodno koristiti drugi pristup (pogledajte ovde kako bi izgledala naša Repository klasa ako bi koristili AsyncTask metode). Pošto moramo da izbegnemo korišćenje AsyncTask, drugi način za izvršenje je korišćenje Executor objekta.

Exexutor servis se kreira u Database klasi uz definisanje neohodnog broja thread-ova:

Sada u okviru naše repository klase možemo iskoristi ExecutorService i izvršiti DAO metodu asihorono sa backround thread-a.

Primer

Ceo projekat iz primera možete naći na GIthub-u pod naziom “sqliteWithRoomLib”.

×

Primer

×

×

×

×

fallbackToDestructiveMigration()

Ova metoda je zadužena da migraciju baze kada menjamo verziju baze i ima sličnu ulogu kao OnUpgrade() metoda iz klase SQLiteOpenHelper.

Ova metoda omogućava Room biblioteci da destruktivno ponovo kreira tabele baze podataka ako nisu pronađene migracije koje bi migrirale stare šeme baze podataka na najnoviju verziju šeme. Više o migraciji sa Room biblotekom pročitajte ovde.

×

Primary key

Primary key je kolona koja predstavlja jedinstveni indetifiaktor reda u tabeli baze podataka. Svaka tabela mora da ima bar jedan Primary key koji ne sme biti NULL. Primary key se definiše pri kreiranju tabele na sledeći način:

Druga sintaksa za definisanje Primary key-a ovako izgleda:


LiveData & MVVM

LiveData klase

LiveData

LiveData je apstraktna klasa tzv. observable data holder zadužen da čuva informacije o podacima i da obavesti sve zainteresovane posmatrače ako dodje do promena.

LiveData is actually just an Abstract Class. So it can’t be used as itself.

mediator_live_data_diagram

Najvažnija karakteristika LiveData je ta što je svestan životnog ciklusa drugih komponenti aplikacije (aktivnosti, fragmenti…). Upravo zbog te karakteristike LiveData prosledjuje podatke samo observers (posmatračima) koji su u aktivnom stanju (ako je životni ciklus u STARTED ili RESUMED stanju), dok neaktivni (destroyed) posmatrači iako registrovani nisu obavešteni vrednostima koje čuva LiveData. Kada koristimo LiveData ne treba brinemo kada se završava (destroy) životni ciklus aktivnosti/fragmenta jer se automatski odjavjljuju čim komponenta završi svoj životni ciklus.

MutableLiveData

Kod LiveData klase su setter metode privatne i ona se ne može se koristi ako je potrebno negde promeniti podatke tj. setovati ih. Iz tog razloga postoji njena podklasa MutableLiveData kod koje je setter metoda public setValue() (za background thread se koristi postValue() metoda).

MediatorLiveData klasa

LiveData subclass which may observe other LiveData objects and react on OnChanged events from them.

Ovo je još specifičnija metoda i ima dodatnu mogućnost da može pratiti rezultate iz različitih LiveData i spojiti ih u jedan MediatorLiveData.
Ako pretpostavim da imamo dva LiveData objekta koji emituju neku vrednost ali iz dva različita izvora (npr. vrednost nekog dobra sa dve različite berze) tada mi želimo da osluškujemo promene iz oba izvora.

mediator_live_data_diagram

  • addSource(LiveData source, Observer onChanged)
    Metoda omogućuje da se izabere koji LiveData objekat se osluškuje, i šta će da radi callback metoda onChanged kada dodje do promene.
    MediatorLiveData ima dva parametra, prvo je LiveData koju želite da primeti MediatorLiveData, a drugi je povratni poziv koji će se pokrenuti kada se promene podaci u LiveData (prosleđeni u prvom parametru)

  • removeSource(LiveData toRemove)
    Metoda omogućava da se prestane sa slušanjem promena LiveData. Kao u sledećem primeru kada se nakon 10 promena podatka više ne prate promene:

Observe u View-u

Deo koda u okviru View-a je isti kao kod LiveData, pa je posmatranje MediatorLiveData može da izgleda ovako:

View – ViewModel

U ovome primeru je prikazan postupak neophodan za komunikacija izmedju View-a (aktivnost) i njegovog ViewModel-a:

  1. Implementacija biblioteke u projekat



    Takodje u okviru gradle staviti da google() bude u sekciji repository:

  2. MutableLiveData u ViewModel klasi

    • Kreiranje ViewModel klase

      ekstendujući ViewModel (kada nam nije potreban Context) ili AndroidViewModel klase (kada nam je potreban Context):

      Ovu klasu koristimo ako nam nije potreban Context:

      AndroidVievModel se koristi ako je potreban context. Za dobijanje context-a u okviru klase se korist metoda getApplication () ili se kroz konstruktor klase prosledi Aplication.

    • Kreiranje MutableLiveData objekta

    • Kreiranje getter-a za MutableLiveData objekat

      Iako je sam objekat “mHasSearchResults” ima promenjive vrednosti pa je predstavljen kao MutableLiveData, mi sa getter-om uvek vraćamo taj isti objekat te se iz tog razloga ovde koristi LiveData objekat a ne MutableLiveData:

    • Emitovanje promena

      Sledeća stvar koju je potrebno uraditi u okviru ViewModel klase je emitovanje promena svim prijavljenim posmatračima. To se izvršava korišćenjem MutableLiveData metoda: setValue() (sa glavnog thread-a) ili postValue() (sa background thread-a):

      Pozivanje metode setValue(T) i prethodnom primeru rezultuje da se kod posmatrača (obično neki View) pozivaju metode onChanged() sa vrednostima koja je poslatim kroz parametar setValue() metode (u ovom slučaju “true”).

      NAPOMENA:
      setValue() se ne može pozivati sa background thread-a, u tom slučaju je potrebno koristi postValue()

  3. Praćenje promena MutableLiveData u View klasi

    • Referenciranje na ViewModel u View-u:

    • Kreiranje reference na LiveData objekat

      Sada kada imamo referencu na ViewModel možemo da pristupimo i njegovim metodama, što ćemo da iskoristimo i da pozovemo getter metodu sa kojom ćemo dobiti referencu i na LiveData objekat:

    • Observe (osluškivanje promena)

      Kada dobijemo referencu na LiveData objekat možemo da pozovemo njegovu metodu observe(). Ova metoda prihvata dva parametra:

      • “LifecycleOwner” – definišemo na koji View treba LiveData da pazi kada je u pitanju LifeCycle
      • “Observer” – kroz ovaj parametar kreirajući novi anonimni posmatrač (Observer) praktično definišemo šta će posmatrač da uradi nakon promene:

Repository – ViewModel – View

mvvm diagram

U MVVM arhitekturi svaki nivo ima direktni pristup samo jednom nivou ispod, dok ga ne interesuju oni iznad njega. Ovakva arhitektura nam omogućava da promenimo layer iznad (npr.View) bez ikakvog menjanja nivoa ispod tj. izvora informacija (npr.ViewModel).

Cela komunikacija izmedju ovih arhitektonskih layer-a ustvari je jedan zatvoreni krug. Početak komunijacije je najčešće sama interakcija korisnika sa interfejsom (View), koji prosledjuje informacije “na dole” sve do baze podataka (kroz setValue()). Promena u bazi je okidač za emitovanje promena u Repository klasi. Te promene osluškuje ViewModel, pa ih zatim koristi da bi emitovao svoje informacije View-u, koji ih zatim koristi za ažuriranje sadržaja. Ovaj slučaj je praktično produženi slučaj komunikacije izmedju View-a i ViewModel-a iz prethodne sekcije.

Repository

Repositor je klasa koja omogućava pristup podacima ostalim delovima programa, a ona jedino ima pristup različitim izvorima informacija (web ili kao u ovome slučaju baza podataka). Za dobijanje informoacija iz baze podataka repository pristupa klasi zaduženoj za direktan pristup bazi (u ovome primeru je “AppDBHelper” klasa)

Ne možemo pozivati ovu handler-ovu metodu i iz drugih delova koda jer ostatak programa ima pristup samo repository-jy, onda je potrebno napraviti metodu koja apstrahuje handler:

Kao što smo do sada videli u radu sa LiveData, da bi se emitovala promena neke promenjive potrebne su tri stvari:

  1. Kreiranje MutableLiveData objekata (ovde sve započinje pa se kreira novi objekat)
  2. Getter tog MutableLiveData objekata
  3. Setovanje nove vrednosti promenjive sa setValue() metodom (u ovome primeru je ta nova vrednost lista koju smo dobili direktno iz Db-a.
    Drugu i treću stavku ćemo definisati zajedno u okviru jedne metode:

ViewModel

U ViewModel klasi je takodje prva stavka kreiranje MutableLiveData objekata. Medjutim ovde postoji mala razlika u odnosu na prethodni primer (komunikacija samo izmedju View-a iViewModel-a), jer se ne kreira novi MutableLiveData objekat, već taj objekat ViewModel dobija iz Repository-ja preko getter-a:

Druga stavka je kreiranje gettera za taj LiveData objekat (koji će iskoristi View):

A treća stavka je emitovanje promena (setValue() ili postValue()). Obično se ove promene dešavaju kada se pozove neka od Repository metoda za insert, update ili remove data iz baze.

View

View je zadužen da pokrene ceo lanac dogadja tako što prihvata interakciju krajnjeg korisnika (koji može da izvrši neku promenu koja će da utiče na sadržaj baze podataka). Zatim se taj korisnički zahtev prosledjuje dalje “na dole” (View -> VieModel -> Repository -> DB). Prihvatanje

U okviru ove metode se poziva metoda koja će na kraju da utiče na promenu baze podataka:

Ili

Kada dodje do promena u bazi podataka View-u su potrebne te nove informacije. Iz tog razloga View prati i osluškuje promene LiveDate objekta (čiji počeci sežu do repa) i reaguje na njih ažuriranjem sadržaja:

Ceo kod projekta u ovim primerima možete videti na GitHub-u u okviru projekta “SQLdatabaseWithoutLibrary”.

Fragmenat – Fragment

Komunikacija se odvija preko zajedničkog ViewModela, tako što svi View-i (fragmenti i aktivnost) mogu da mu pristupe, i “osluškući” promene koje emituje ViewModel.

fragment and viewmodel

Kada fragmentA želi da pošalje neku poruku FragmentuB, dovoljno je da ažurira LiveData u ViewModel-u, a ViewModel će da “emituje” te promene u etar, pa će i FragmentB koji osluškuje to biti obavešten o poruci i moći da reagujue odgovarajuće na nju. Ukoliko Aktivnost osluškuje promene kod istog tog LiveData objekta, i ona će biti istovremeno obaveštena pa će i ona moći da adekvatno reaguje.

Prednosti ove komunikacije su sledeće:

  1. Aktivnost ne mora ništa da zna i da radi u vezi konverzacije izmedju dva fragmenta (koristeći listener patern kod vezan za konverzaciju se nalazi u okviru Aktivnosti).
  2. Fragmenti ne moraju da znaju jedni o drugima, ako jedan od fragmenata nestane, drugi nastavi da radi kao i obično.
  3. Svaki fragment ima svoj životni ciklus i na njega ne utiče životni ciklus drugog. Ako jedan fragment zameni drugi, UI nastavlja da radi bez problema.
Primer




Pogledajte ceo primer projekta na GitHub-u u okviru repozitorijuma “FragmetComunication”.

LiveData klasa

LiveData je apstraktna klasa tzv. observable data holder zadužen da čuva informacije o podacima i da obavesti sve zainteresovane posmatrače ako dodje do promena.

LiveData is actually just an Abstract Class. So it can’t be used as itself.

mediator_live_data_diagram

Najvažnija karakteristika LiveData je ta što je svestan životnog ciklusa drugih komponenti aplikacije (aktivnosti, fragmenti…). Upravo zbog te karakteristike LiveData prosledjuje podatke samo observers (posmatračima) koji su u aktivnom stanju (ako je životni ciklus u STARTED ili RESUMED stanju), dok neaktivni (destroyed) posmatrači iako registrovani nisu obavešteni vrednostima koje čuva LiveData. Kada koristimo LiveData ne treba brinemo kada se završava (destroy) životni ciklus aktivnosti/fragmenta jer se automatski odjavjljuju čim komponenta završi svoj životni ciklus.

MutableLiveData klasa

Pošto je LiveData abstraktna klasa ona ne može da se koristi samomstalno iz tog razloga postoji njena podklasa MutableLiveData koja ima public metode setValue() i postValue() za definisanje vrednosti koje posmatrači prate:

  • setValue() se poziva kada ažuriramo vrednost iz MainThread-a
  • postValue() se poziva kada ažuriramo vrednosti iz nekog drugog thread-a.

Tako da svaki View koji prati promene neke vrednosti setovane kros metode setValue()/postValue() može da ažurira UI kada se jedan LiveData objekat promeni.

MediatorLiveData klasa

LiveData subclass which may observe other LiveData objects and react on OnChanged events from them.

Ovo je još specifičnija metoda i ima dodatnu mogućnost da može pratiti rezultate iz različitih LiveData i spojiti ih u jedan MediatorLiveData.
Ako pretpostavim da imamo dva LiveData objekta koji emituju neku vrednost ali iz dva različita izvora (npr. vrednost nekog dobra sa dve različite berze) tada mi želimo da osluškujemo promene iz oba izvora.

mediator_live_data_diagram

  • addSource(LiveData source, Observer onChanged)
    Metoda omogućuje da se izabere koji LiveData objekat se osluškuje, i šta će da radi callback metoda onChanged kada dodje do promene.
    MediatorLiveData ima dva parametra, prvo je LiveData koju želite da primeti MediatorLiveData, a drugi je povratni poziv koji će se pokrenuti kada se promene podaci u LiveData (prosleđeni u prvom parametru)

  • removeSource(LiveData toRemove)
    Metoda omogućava da se prestane sa slušanjem promena LiveData. Kao u sledećem primeru kada se nakon 10 promena podatka više ne prate promene:

Observe u View-u

Deo koda u okviru View-a je isti kao kod LiveData, pa je posmatranje MediatorLiveData može da izgleda ovako:

View – ViewModel

U ovome primeru je prikazan postupak u kome ViewModel emituje promene stanja neke promenjive, dok te stanje te promenjive osluškuje View (u ovome slučaju aktivnost) i zatim reaguje na tu promenu.

  1. Implementacija biblioteke u projekat



    Takodje u okviru gradle staviti da google() bude u sekciji repository:

  2. MutableLiveData u ViewModel klasi

    • Kreiranje ViewModel klase

      ekstendujući ViewModel (kada nam nije potreban Context) ili AndroidViewModel klase (kada nam je potreban Context):

      Ovu klasu koristimo ako nam nije potreban Context:

      AndroidVievModel se koristi ako je potreban context. Za dobijanje context-a u okviru klase se korist metoda getApplication () ili se kroz konstruktor klase prosledi Aplication.

    • Kreiranje MutableLiveData objekta

    • Kreiranje getter-a za MutableLiveData objekat

      Iako je sam objekat “mHasSearchResults” ima promenjive vrednosti pa je predstavljen kao MutableLiveData, mi sa getter-om uvek vraćamo taj isti objekat te se iz tog razloga ovde koristi LiveData objekat a ne MutableLiveData:

    • Promene vrednosti promenjive koje izaziva emitovanje vesti o tome

      Da bi se emitovala vest iz viewModel-a o promeni vrednosti promenjive potrebno je i napraviti tu promenu. To se izvršava korišćenjem MutableLiveData metoda:

      • setValue() (sa glavnog thread-a)
      • postValue() (sa background thread-a)
      Primer

      NAPOMENA:
      setValue() se ne može pozivati sa background thread-a, u tom slučaju je potrebno koristi postValue()

      Nakon izvršene promene vrednosti promenjive, ViewModel emituje “vest” svim prijavljenim posmatračima da je došlo do promena, što zatim rezultuje da se kod posmatrača izvršava metoda onChanged() sa novim vrednostima koje su prosledjene (kroz parametar setValue() metode, u ovom primeru “true”).

      NAPOMENA:
      Metoda LiveData objekta setValue() sa kojom se menja vrednost promenjive, može da se pozove iz bilo kog fajla gde imamo referencu na taj LiveData objekat (u ovome primeru taj objekat je mHasSearchResults). To može biti neki View koji prihvata korisničku akciju i na osnovu nje setuje novu vrednost, ili fajl koji registruje promenu u bazi podataka nakon čega emituje te promene….

  3. Praćenje promena MutableLiveData u View klasi

    • Referenciranje na ViewModel u View-u:

    • Kreiranje reference na LiveData objekat

      Sada kada imamo referencu na ViewModel možemo da pristupimo i njegovim metodama, što ćemo da iskoristimo i da pozovemo getter metodu sa kojom ćemo dobiti referencu i na LiveData objekat:

    • Observe (osluškivanje promena)

      Kada dobijemo referencu na LiveData objekat možemo da pozovemo njegovu metodu observe(). Ova metoda prihvata dva parametra:

      • “LifecycleOwner” – definišemo na koji View treba LiveData da pazi kada je u pitanju LifeCycle
      • “Observer” – kroz ovaj parametar kreirajući novi anonimni posmatrač (Observer) praktično definišemo šta će posmatrač da uradi nakon promene:

      NAPOMENA:
      Sa prethodnim kodom u okviru onChanged() mi reagujemmo samo na promenu vrednosti koju nadgledamo, medjutim nekada je potrebno imati vrednost koju čuva live objekat i pre same promene (npr. setovati početnu vrednost pre nego što dodje do bilo kakve promene…). U takvom slučaju se dobijanje trenutne vrednosti koju čuva LiveData objekat vrši koristeći njegovu metodu getValue() (vraća trenutnu vrednost koju čuva taj objekat):

Repository – ViewModel – View

U MVVM arhitekturi svaki nivo ima direktni pristup samo jednom nivou ispod, dok ga ne interesuju oni iznad njega. Ovakva arhitektura nam omogućava da promenimo layer iznad (npr.View) bez ikakvog menjanja nivoa ispod tj. izvora informacija (npr.ViewModel).

Cela komunikacija izmedju ovih arhitektonskih layer-a ustvari je jedan zatvoreni krug. Početak komunijacije je najčešće sama interakcija korisnika sa interfejsom (View), koji prosledjuje informacije “na dole” sve do baze podataka (kroz setValue()). Promena u bazi je okidač za emitovanje promena u Repository klasi. Te promene osluškuje ViewModel, pa ih zatim koristi da bi emitovao svoje informacije View-u, koji ih zatim koristi za ažuriranje sadržaja. Ovaj slučaj je praktično produženi slučaj komunikacije izmedju View-a i ViewModel-a iz prethodne sekcije.

Repository

Pošto Repositoru jedini ima direktan pristup izvoru informacija (u ovome slučaju je to baza podataka), u njemu kreiramo metodu čiji cilj je da dobije te informoacije preko klasa zaduženih za pristup bazi (u ovome primeru je “AppDBHelper” klasa)

Kao što smo do sada videli u radu sa LiveData u klasi koja emituje informacije potrebne su tri stvari:

  1. Kreiranje MutableLiveData objekata (ovde sve započinje pa se kreira novi objekat)
  2. Getter tog MutableLiveData objekata
  3. Emitovanje informacija sa setValue() metodom (ove informacije se dobijaju iz Db-a prethodnom metodom “getAllItems()”
    Drugu i treću stavku ćemo definisati zajedno u okviru jedne metode:

ViewModel

U ViewModel klasi je takodje prva stavka kreiranje MutableLiveData objekata. Medjutim ovde postoji mala razlika u odnosu na prethodni primer (komunikacija samo izmedju View-a iViewModel-a), jer se ne kreira novi MutableLiveData objekat, već taj objekat ViewModel dobija iz Repository-ja preko getter-a:

Druga stavka je kreiranje gettera za taj LiveData objekat (koji će iskoristi View):

A treća stavka je emitovanje promena (setValue() ili postValue()). Obično se ove promene dešavaju kada se pozove neka od Repository metoda za insert, update ili remove data iz baze.

View

View je zadužen da pokrene ceo lanac dogadja tako što prihvata interakciju krajnjeg korisnika (koji može da izvrši neku promenu koja će da utiče na sadržaj baze podataka). Zatim se taj korisnički zahtev prosledjuje dalje “na dole” (View -> VieModel -> Repository -> DB). Prihvatanje

U okviru ove metode se poziva metoda koja će na kraju da utiče na promenu baze podataka:

Ili

Kada dodje do promena u bazi podataka View-u su potrebne te nove informacije. Iz tog razloga View prati i osluškuje promene LiveDate objekta (čiji počeci sežu do repa) i reaguje na njih ažuriranjem sadržaja:

Ceo kod projekta u ovim primerima možete videti na GitHub-u u okviru projekta “SQLdatabaseWithoutLibrary”.

Fragmenat – Fragment

Komunikacija se odvija preko zajedničkog ViewModela, tako što svi View-i (fragmenti i aktivnost) mogu da mu pristupe, i “osluškući” promene koje emituje ViewModel.

fragment and viewmodel

Kada fragmentA želi da pošalje neku poruku FragmentuB, dovoljno je da ažurira LiveData u ViewModel-u, a ViewModel će da “emituje” te promene u etar, pa će i FragmentB koji osluškuje to biti obavešten o poruci i moći da reagujue odgovarajuće na nju. Ukoliko Aktivnost osluškuje promene kod istog tog LiveData objekta, i ona će biti istovremeno obaveštena pa će i ona moći da adekvatno reaguje.

Prednosti ove komunikacije su sledeće:

  1. Aktivnost ne mora ništa da zna i da radi u vezi konverzacije izmedju dva fragmenta (koristeći listener patern kod vezan za konverzaciju se nalazi u okviru Aktivnosti).
  2. Fragmenti ne moraju da znaju jedni o drugima, ako jedan od fragmenata nestane, drugi nastavi da radi kao i obično.
  3. Svaki fragment ima svoj životni ciklus i na njega ne utiče životni ciklus drugog. Ako jedan fragment zameni drugi, UI nastavlja da radi bez problema.
Primer




Pogledajte ceo primer projekta na GitHub-u u okviru repozitorijuma “FragmetComunication”.


Uvod u MVVM arhitekturu

MVVM pattern

MVVM (Model-View-ViewModel) je patern koji razdvaja aplikaciju na više komponenti tako da svaka komponenta ima svoje specifične odgovornosti. Pri korišćenju MVVM paterna “kod” aplikacije je razdvojen na tri dela: View, ViewModel i Model, ova arhitektura je preporučena od strane Google-a kao jedan od najboljih načina strukture koda Android aplikacija. Osnovne karakteristike ovog pristupa su:

  • Komponente korisničkog interfejsa UI se drže podalje od poslovne logike
  • Poslovna logika se drži podalje od operacija vezanih za bazu podataka
  • Manje briga sa “lifecycle” događajima (jer se koriste komponente koje su svesne životnog ciklusa drugih komponenti aplikacije)

MVVM diagram

Preporučeni način za komunikaciju izmedju dva susedna sloja je tzv. “Observer patern” (najčešće koristeći “LiveData” ili neku drugu biblioteku).

observe_patern

LiveData

LiveData je tzv. observable data holder zadužen da čuva tj. ima referencu na niži nivo o podacima i da obavesti sve zainteresovane posmatrače ako dodje do promena. Najvažnija karakteristika LiveData je ta što je svestan životnog ciklusa drugih komponenti aplikacije (aktivnosti, fragmenti…). Upravo zbog te karakteristike LiveData ažurira samo observers (posmatrače) koji su u aktivnom stanju (ako je životni ciklus u STARTED ili RESUMED stanju). LiveData samo obaveštava aktivne posmatrače o ažuriranjima, dok neaktivni (destroyed) posmatrači iako registrovani nisu obavešteni o promenama. Kada koristimo LiveData ne treba brinemo kada se završava (destroy) životni ciklus aktivnosti/fragmenta jer se automatski odjavjljuju čim komponenta završi svoj životni ciklus. Više o LiveData pročitajte u članku: “LiveData & MVVM”

Kao što se vidi na slici ispod, jedan sloj ima referencu samo na sloj ispod njega, ali ne i obratno (tj. sloj nema pojama o komoponenti iznad), pa tako: “View” zavisi od “ViewModel”-a, a “ViewModel” zavisi od “Model-a”

mvvm diagram

MVVM je sličan MVP pattern-u, stom razlikom što kod “MVP” pattern-a “Presenter” ima referencu na “View” i direktno “govori” View-u koje podatke i promene da prikaže, dok “ViewModel” ne drži referencu na View i ne može da direktno utiče na “View”.

“View”

“View” sekcija (sadrži klase: Aktivnosti i Fragmente) je zadužena za prikaz interfejsa i prihvatanje akcija korisnika. Pošto ova sekcija ima referencu na nivo ispod (ViewModel) ona može da osluškuje promene u ViewModelu, i ako ima promena da pozove neku metodu iz ViewModel-a i preuzme te nove podatke. Zbog ove odgovornosti je potrebno da u okviru View-a postoji deo koda sa kojim se “View” prijavljuje da posmatra dogadjaje koje emituje “ViewModel” (streams of events). Pročitajte više o tome u članku: “LiveData & MVVM”

“Keep the logic in Activities and Fragments to a minimum”

Nakon preuzimanja novih podataka “View” ima obavezu da ažurira prikaz koji vidi krajnji korisnik. Još jedna bitna odlika vezana za “View” kada se koristi MVVM arhitektura je da aktivnosti ili fragmenti više ne moraju imati odgovornost za čuvanje stanja jer je tu obavezu preuzeo “ViewModel”

Napomena:
Conditional statements i loops ne treba da se nalaze u aktivnostima ili fragmentima taj deo treba da se nalazi u ViewModelima ili drugim slojevima aplikacije.

“ViewModel”

“ViewModel” takodje ima dve uloge:

  1. Pošto ima referencu na nivo ispod (Model klase), ViewModel može da osluškuje “vesti” o promenama koje emituje Model.
  2. Kada dodje do promena podataka da (nakon obrade podataka) dalje emituje vesti o tome (ViewModel ne interesuje ko će te informaciji o promenama iskoristi, njemu je samo važno da obaveštava o tome).

    NAPOMENA: “ViewModel” ne drži referencu na View te stoga ne može direktno da utiče na “View”

“Instead of pushing data to the UI, let the UI observe changes to it.”

“ViewModel” je klasa koja je dizajnirana da preživi konfiguracione promene (npr. rotacija ekrana) i sačuva informacije koje su neophodne za View (a to znači da naša aktivnost/fragment više ne moraju imati tu odgovornost). Kada se “View” (tj. fragmentom/aktivnosti) uništi promenom konfiguracije ili rotacije uredjaja, njegov “ViewModel” neće biti uništen a nova instanca View-a će se ponovo povezati na isti “ViewModel”.

NAPOMENA:
Iako ViewModel može da preživi promenu konfiguracije (npr. rotacija ekrana), ipak ne živi beskonačno!
ViewModel ne može da preživi ubijanje aktivnosti od strane opertativnog sistema ili korisnika (npr. “back” dugme). Ako android uništi aplikaciju/aktivnost to će uništiti i ViewModel, a onda samo onSavedInstance() ili baza podataka pružaju mehanizam za čuvanje podataka. Što vodi do zaključka da ViewModel klasa nije zamena za “trajno čuvanje podataka” ili čuvanje podataka koristeći onSaveInstanceState()!

“Avoid references to Views in ViewModels.”

“ViewModel” se takodje često koristi kao komunikacioni sloj između fragmenata u okviru jedne aktivnosti. Svaki Fragment ima pristup “ViewModel-u” preko svoje Aktivnosti što omogućava komunikaciju između Fragmenta bez direktnog medjusobnog kontakta. Oba fragmenta mogu pristupiti VievModel-u putem njihove aktivnosti u kojoj se nalaze. Prvi fragment može ažurirati neke podatke pozivajući metodu iz ViewModel-a, nakon čega će ViewModel te promene emitovati (pomoću LiveData), a ukoliko drugi fragment “posmatra” LiveData on će to opaziti, i na taj način dobiti informaciju iz prvog fragmenta.
Više o ovome u članku: “Komunikacija izmedju fragmenata koristeći ViewModel i LiveData”

NAPOMENA:
Preporuka je da “ViewModel” klase ne koriste android framework klase tj. da nema android.* imports u okviru klase.

“Don’t let ViewModels know about Android framework classes”

Prethodna izjava da “ViewModel ne treba da ima referencu na View” je iz razloga što to može izazvati probleme u slučaju da se View instanca uništi (zbog rotacije…) u trenuku kada ViewModel ima i dalje referencu na nju (npr. kada “ViewModel” traži online podatke sa mreže). Upravo zbog ove preporuke a da bi omogućili View-u pristup podacima se koristi Observer patern. ViewModel emituje dogadjaje vezane za podatke (najčešće koristeći LiveData biblioteku), a View se prijavi da posmatra te promene.

“Instead of pushing data to the UI, let the UI observe changes to it.”

Kada se kreira ViewModel klasa potrebno je da ekstendujemo ili “ViewModel” ili “AndroidViewModel” klasu (koristite “AndroidViewModel” ako vam treba i application context u okviru ViewModel-a).

“Model”

“Model” sekcija sadrži klase zadužene za direktan pristup podacima sa ciljem da abstrahuje i pojednostavi pristup tim podacima. Pošto često podaci mogu dolaziti iz više izvora (baza, webservice…), iz tog razloga je korisno da iz ViewModel-a imamo samo jednu ulaznu tačku ka izvorima podataka, a ta tačka se zove “Repository” čija je uloga da apstrahuje više izvora podataka u jedan API. Repository nije neka specijalna arhitektonska komponenta već obična klasa koja ima pristup svim izvorima podataka. ViewModel praktično ima direktan pristup samo do repository preko tog API-ja i na jednostavan nači kad god mu zatreba može da dobija podatke (ne znajući iz kog izvora stvarno potiču ti podaci: web ili baza podataka i na koji način je došlo do njih).

“Data repository as the single-point entry to your data”

Repository diagram


Popunjavanje ViewPager-a koristeći PagerAdapter

Šta je ViewPager?

ViewPager je Layout Manager koji omogućava korisniku da se kreće kroz stranice podataka pokretima na levo ili desno, dok se pri promeni stranica izvršava i ugradjena animacija. Najčešće su te strane sa podacima su fragmenti, mada mogu da budu i nešto drugo kao npr. slike koje se slajduju…
ViewPager se popunjava podacima koristeći svoj adapter tzv. PagerAdapter. Ukoliko koristimo fragmente za njih se koriste specijalizovani adapteri:

  • FragmentPagerAdapter (kešira fragmente pa se koristi za manji broj fragmenata obično u saradnji sa tab-ovima)
  • FragmentStatePagerAdapter (koristi se kod većeg broja fragmenata)

ViewPager se ubacuje u layout kao kontejner u kome će se prikazivati sve stranice.

TabLayout

ViewPager se najčešće integriše u layout zajedno sa TabLayout-om, koji predstavlja navigaciju.

tabLayout

Pre ubacivanja TabLayouta potrebno je da se doda novi dependecies “design”:
implementation 'com.android.support:design:28.0.0'

Tek nakon ubacivanja dependencies možemo da dodamo novi widget TabLayout sa TabItem-ima:

Korišćenje PagerAdapter-a

PagerAdapter se koristi da popuni ViewPager kontejner sa odgovarajućim stranicama. U ovom članku stranice će predstavljati fragmenti, te je pre svega potrebno kreirati tri različita fragmenta (FragmentA, FragmentB, FragmentC).

a) Kreiranje adapter klase

Kada pravimo naš custom adapter potrebno je da ekstendujemo našu klasu ili sa PagerAdapter klasom ili sa jednom od već pomenutih klasa: FragmentPagerAdapter ili FragmentStatePagerAdapter koje se koriste za rad sa fragmenti-ma.

MojViewPagerAdapter.java

Pošto u ovome primeru imamo mali broj fragmenta, koristićemo klasu koja kešira fragmente (brza ali dobra samo sa malim brojem fragmenata):

Čim ekstendujemo klasu AndroidStudio zahteva da se generiše konstruktor i implementriraju dve metode:

Metoda getItem(int position) vraća odgovarajući fragment u zavisnosti od pozicije (tj. int parametra koji predstavlja poziciju fragmenta):

Metoda getCount() je planirana da vraća ukupan broj stranica koji treba da budu prikazani:

b) Instanciranje adaptera u aktivnosti

Kada smo napravili Adpter klasu potrebno je da u okviru aktivnosti napravimo njegovu instancu. Nova instanca se pravi koristeći konstruktorsku funkciju kojoj se prosledjuje instanca framentManager-a:

Aktivnost

c) Povezivanje adaptera i ViewPager kontejnera

Kada targetiramo ViewPager onda jednostavno možemo da ga povežemo sa adapterom koristeći metodu setAdapter():

Aktivnost

d) Sinhronizovanje ViewPager-a i TabLayout-a

Već sada je ViewPagerAdapter popunio ViewPager sa fragmentima, tako da možemo slajdovati fragmenate jednostavnim “swipe” pokretima levo ili desno. Medjutim iako se uspešno menjaju fragmenti, ne dolazi do promena kod TabLayout-a. Potrebno je povezati swipe dogadjaj sa promenom u Tablayoutu i obrnuto. Za interakciju izmedju ova dva View-a se koriste listeneri i tzv. listener pattern (više o listener pattern-u možete pogledati ovde).

ViewPager listener

ViewPager ima interfejs “ViewPager.OnPageChangeListener” čije se callback metode (onPageScrollStateChanged, onPageScrolled i onPageSelected) pozivaju kada dodje do promene strane slajdovanjem u ViewPager-u. Da bi listener radio svoj posao potrebno je da setujemo osluškivač dogadjaja, što se vrši sa setter metodom addOnPageChangeListener(). Ovu metodu poziva objekat koji implementira ovaj interfejs a to je bilo koji ViewPager.

Objekat koji je zainteresovan da postane osluškivač promene stranica u ViewPager-u je ustvari TabLayout objekat. Za ovu ulogu u androidu postoji specifična klasa koja implementira sve callback metode i tako sinhronizuje TabLayout nakon promene stranice u ViewPager-u pod nazivom “TabLayout.TabLayoutOnPageChangeListener”.
Ova klasa u konstruktoru prihvata TabLayout kao parametar, pa ćemo nju iskoristiti da napravimo “on the fly” kao anonimni objekat :

TabLayout listener

TabLayout ima interfejs “TabLayout.OnTabSelectedListener” čije se callback metode ( onTabSelected(), onTabReselected(), onTabUnselected()) pozivaju kada dodje do promene u selekciji tabova.
Da bi listener patern radio svoj posao potrebno je da setujemo osluškivača ovih dogadjaja, a to se vrši sa setter metodom addOnTabSelectedListener(). Ovu metodu poziva objekat koji implementira ovaj interfejs a to je bilo koji tabLayout.

Objekat koji je zainteresovan da postane osluškivač promene tabova je ustvari ViewPager objekat. Za ovu ulogu u androidu postoji specifična klasa koja implementira sve callback metode i tako sinhronizuje ViewPager nakon promene tabova pod nazivom “TabLayout.OnTabSelectedListener”.
Ova klasa u konstruktoru prihvata ViewPager kao parametar, pa ćemo nju iskoristiti da napravimo “on the fly” kao anonimni objekat :

Pogledajte ceo kod




NAPOMENA:
Za brzu integraciju TabLayout-a i ViewPager-a možemo koristi već predvidjenu “Tabbed Activity” sa ugradjenom navigacijom (Actions bar Tabs with ViewPager).

Viewpager boilerplate

Sa ovom aktivnosti dolazi već odradjen veći deo posla, pa uz par manjih izmena sve može da se pripremi veoma brzo.
Pre svega potrebno je napraviti par fragment layouta (npr. fragment_a.xml, fragment_b.xml, fragment_c.xml) i prilagoditi getItem() metodu:

Deo koji sa sigurnošću možemo da obrišemo je vezan za privremeni placeholder fragment :

Ukoliko nam ne treba FloatingActionButton onda možemo obrisati njegov deo koda:

Ako nam ne treba “options meni” onda možemo da obrišemo deo vezan za to:

Ceo kod “pročišćene” aktivnosti možete da pogledate ovde.

×

Aktivnost


Adapter za RecyclerView

Uvod

RecyclerView

Adapter je most između izvora podataka (Array objekat, List objekat…) i korisničkog interfejsa. Uloga adapter objekta (implementira Adapter interfejs) je da čita podatke iz različitih izvora podataka, i na osnovu njih popunjava View objekte članove nekog ViewGroup-a.
Do sada smo koristili ListView ali preporuka je da se umesto listView-a koristi RecyclerView, a naručito kada god imate kolekcije podataka čiji se elementi menjaju tokom izvršavanja aplikacije kao reakcija na aktivnost korisnika ili mrežnih dogadjaja. RecyclerView je naslednik ListView i GridView-a, i namenjen je da efikasno renderuje adapter-based view.
RecyclerView ima svoj adapter “RecyclerView.adapter” koji implementira ViewHolder patern, integriše “convertView” i ima pripremljene metode koji su zamena za sve što smo uradili kod “CustomArrayAdapter-a” a poboljšava efikasnost prikaza pri skrolovanju liste.

Postupak kreiranja RecyclerView.Adapter-a

a) Podaci za listu

U ovome primeru jedan član liste prihvata više podataka, stoga je potrebno napraviti klasu koja će da bude “modla” za kreiranje objekata koji čuvaju podatke jednog člana nekog AdapterView-a:

Definisali smo da se kroz konstruktor ubacuju podaci za svaki član liste, te stoga možemo da generišemo pocetnu listu podataka u okviru Aktivnosti (Fragmenta):

b) Kreiranje AdapterView-a

Potrebno je u sklopu layout-a Aktivnosti (fragmenta) definisati mesto gde će biti smeštena lista podataka. Ovaj deo je sličan kao kod običnog adaptera, stim što se kreira se umesto ListView koristi RecyclerView.

c) Kreiranje custom layout-a za jedan elementa liste

custom-adapter-row

Sada je potrebno napraviti custom layout, koji će da ih prikaže više podataka nemenjenih za jedan element liste. Ovaj deo nije bio potreban kod “običnog” adaptera ali je sada potreban zbog viška podataka, jer ne možemo da koristimo već predefinisane androidove layout-e koji prihvataju samo jedan podatak. U ovome primeru imamo za svaki row po tri podatka: slika i dva teksta.

NAPOMENA:
Klikom na row element RecyclerView-a se ne dešava očekivani “ripple” efekat. Da bi se takav efekat aktivirao potrebno je root View-u row layout-a dodati atribut: android:background=”?android:attr/selectableItemBackground”

Pogledajte ceo primer koda ovde.

d) Kreiranje Custom Adapter klase

Za kreiranja klase customAdaptera je potrebno da naša klasa ekstenduje RecyclerView.Adapter klasu. Da bi smo definisali koga tipa su podaci u adapter-u, potrebno je pre svega da unutar adaptera napravimo unutašnju statičnu ViewHolder klasu koja ekstenduje RecyclerView.ViewHolder klasu. Kada smo kreirali statičnu ViewHolder potrebnoje da unutar nje definišemo njenu spostvenu konstruktor metodu.

Sada možemo da okviru uglasitih zagrada ubacimo naziv našeg ViewHolder-a i tako definišemo koji tip prihvata adapter.

Tek nakon ovoga možemo da implementiramo sve metode koje zahteva RecyclerView.Adapter klasa, i da definišemo konstruktor koji prihvata ArrayList-u podataka kao parametar. Posle svega klasa bi trebalo ovako da izgleda:

Sada kada smo napravili kostur potrebno je definišemo i detalje.

Definisanje ViewHolder klase

Kao prvo potrebno je da u okviru ViewHolder klase targetiramo sve sub elemente iz example_item.XML layout-a:

Kreiranje instance ViewHolder-a

Prvo je potrebno izvršimo parsiranje row layout-a u View objekat, a to se vrši u okviru onCreateViewHolder() metode, pa se zatim taj objekat prosledi kao parametar konstruktoru nove ViewHolder instance:

Ubacivanje podataka u listu se vrši u sklopu onBindViewHolder() metode:

Definisanje veličine liste

Veličina liste se definiš u okviru getItemCount()

Cela Custom Adapter klasa sada izgleda ovako.

e) Ubacivanje RecyclerView-a u Aktivnost

Neophodne stvari koji definišu RecyclerView su:

  • View koji predstavlja RecylerView
  • RecyclerView.LayoutManager koji se koristi da definiše raspored elemenata u okviru RecyclerView-a (LinearLayoutManager, GridLayoutManager, StaggeredGridLayoutManager)
  • Adapter koji se koristi za popunjavanje RecyclerView-a

NAPOMENA:
RecyclerView se uglavnom definiše tako da ne zavisi od child elemenata nego uglavnom od parent View-a u kome je smešten, te se njegova visina i šira generalno ne menje tokom vremena. Ali taj podatak moramo da naglasimo da android to ne bi svaki put proveravo kada ubacujemo novi ili izbacujemo već postojeći element. Iz tog razloga da bi poboljšali efiasnost RecyclerVie-a je dobro da definišemo naš RecyclerView kao da je fiksne veličine sa setHasFixedSize(true).

Pre svega je potrebno da definišemo polja koja će biti kasnije dostupna drugim delovima koda:

U sklopu onCreate() metode ćemo setovati napravljena polja:

Ceo kod koji se nalazi u Aktivnosti možete da pogledate ovde.

NAPOMENA
Ukoliko dodajemo ili uklanjamo element iz RecyclerView-a potrebno je posle svakog dodavanja obavestiti sistem da je došlo do promene pozivajući metodu notifyItemInserted(position):

Takodje treba i posle svakog uklanjanja postojećeg elelementa obavestiti sistem da je došlo do promene pozivaju’i metodu notifyItemRemoved(position):

Pa bi metode za ubacivanje i izbacivanje elemenata ovako izgledale:

Sve metode koje mogu da se korisite za obaveštavanje sisteme o premenama pogledajte ovde.

f) Definisanje click listener-a

Zajedno sa standardnim ListView-om stiže i onItemClick interfejs, medjutim toga nema kod RecyclerView-a stoga moramo da kreiramo naš interfejs i customClickListener (pogledaj kako se kreiraju customListener-i ovde).

Kreiranje novog interfejsa

Prvo je potrebno da u sklopu našeg Custom Adapter-a definišemo novi interfejs i jednu callback metodu:

Setter metoda

A zatim je potrebno da definišemo polje i setter metodu, čijim pozivanjem u Aktivnosti definišemo objekat koji osluškuje event:

Okidanje dogadjaja

Potrebno je “okinuti” dogadjaj klikom na neki element RecyclerView liste koji je definisan u ViewHolder klasi, a to ćemo uraditi tako što ćemo pozovati callback metodu našeg interfejsa: