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:

Ovu metodu ćemo da pozovemo u trenutku kada se klikne na element “itemView” (predstavljen kao parametar konstruktorske metode). Iz tog razloga poziv metode će biti smešten u okviru onClick() metode:

Sigurno ste primetili da android studio prijavljuje grešku da ne može da nadje promenjivu listener pošto je naša klasa statična. Da bi smo napravili ovu promenjivu dostupnom, prosledićemo je kao parametar konstruktorskoj funkciji Custom ViewHolder klase. Ubacivanje listenera kao parametra klase je najlakše postići ako se u AndroidStudiu označi listener koji je problem i sa ALT + ENTER pozove pomoćni meni, gde se izabere opcija “Create parameter listener”, nakon čega će Android Studio odraditi sve sam.
Pa bi cela ViewHolder klasa sada ovako izgledala

NAPOMENA:
Pri svakom kliku na neki element liste je potrebno da znamo na koji element je kliknuto, to se jednostavno dobija koristeći metodu getAdapterPosition()

Definisanje objekta koji osluškuje i sadržaja callback metode

U okviru Aktivnosti se nadje objekat koji implementira interfejs a to je u našem slučaju instanca samog adaptera “mAdapter”. Zatim taj objekat poziva setter metodu setOnItemClickListener() da bi kroz prosledjeni parametar definisao objekat koji osluškuje event. U našem slučaju će to da bude anonimni objekat koji implementira interfejs “on the fly”. Akciju koja treba da se izvede nakon okidanja dogadjaja ćemo definisati tako što “pregazimo” (override) njegovu callback metodu onItemClick:

Pogledajte ceo kod Aktivnosti ovde.

Ovo je jednostavniji način i sve se definiše u okviru adapter klase. Prvo je potrebno da naša ViewHolder klasa implementira View.OnClickListener:

A nakon toga će AndroidStudio prijaviti grešku i zahtevati da se implementira i njegova callback metoda onClick(). U okviru ove metode se definiše akcija koja se dešava kada se okine dogadjaj tj. kada se klikne na neki član liste.

Da bi se definisao objekat koji osluškuje potrebno je da ga prosledimo kao parametar setOnClickListener() metodi u okviru ViewHolder konstruktora. U našem slučaju to je sam ViewHolder pa ćemo da prosledimo “this”:

Pa bi ceo ViewHolder ovako izgledao:

×

×

×

×

×

Method Description
notifyItemChanged(int pos) Notify that item at position has changed.
notifyItemInserted(int pos) Notify that item reflected at position has been newly inserted.
notifyItemRemoved(int pos) Notify that items previously located at position has been removed from the data set.
notifyDataSetChanged() Notify that the dataset has changed. Use only as last resort.


Custom ArrayAdapter u Androidu

Uvod

custom-adapter-row

U osnovnoj verziji adaptera se koristi samo jedan String podatak koji se smešta u neki od predefinisanih android layouta sa jednim TextView-om.
Custom adapter se razlikuje od “običnog” po tome što je kod njega jedan element liste predstavljen sa više podataka te je potrebno definisati “složeniji layout” koji bi prihvatio i prikazao te podatke. Takav customLayout row liste može da sadrži više subView-ova i widgeta: sliku, tekstualne podatke (rasporedjene na svojim specifičnim pozicijama)…

Postupak kreiranja Custom ArrayAdapter-a

a) Custom model (prihvata više podataka za jedan član liste)

U ovome primeru jedan član liste sadrži dva podatka, stoga je potrebno napraviti klasu koja će da bude “modla” za kreiranje objekata koji čuvaju podatke jednog člana nekog AdapterView-a:

User.java

Sada u okviru Aktivnosti (Fragmenta) možemo na sledeći način da definišemo inicijalni niz podataka kreirajući nove objekte:

Aktivnost

ili da na osnovu njega generišemo ArrayList User objekata:

User.java

Za generisanje podataka iz JSON-a koristimo sledeći kod koji konvertuje JSON u ArrayList User objekata:

Aktivnost

b) Kreiranje AdapterView-a

Potrebno je u sklopu layout-a Aktivnosti (fragmenta) definisati mesto gde će biti smeštena lista podataka. Ovaj deo je isti kao kod običnog adaptera, kreira se jedan ListView koji će da prihvati po jedan custom row svakog člana.

activity_main.xml

c) Kreiranje custom layout-a za jedan elementa liste

Sada je potrebno napraviti custom layout, koji će se koristiti da prikaže više podataka jednog elementa 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.

item_user.xml

d) Kreiranje Custom Adapter klase

U nastavku aplikacije je potrebno da definišemo Adapter klasu na osnovu koje će biti kreirana nova instanca adaptera u sklopu Aktivnosti (Fragmenta). U ovoj klasi treba da bude smešten ceo proces konvertovanja Java objekta u View popunjen podacima.
Pravimo custom adapter tako što kreiramo novu klasu koja ekstenduje ArrayAdapter klasu. Ekstendovanjm klase dobijamo metodu getView() koju možemo da “pregazimo” (override) tako da za svaki element liste uzme podatke iz modela i da sa njima popuni prethodno definisani Custom layout elementa.

NEekonomični ArrayAdapter

Ovo je primer ArrayAdaptera koji troši veliku količinu resursa pri skrolovanju ekrana, što se naručito oseti kod velikih lista podataka.

Sve mane ovog postupka kao i sam poboljšani postupak koji rešava te manjkovasti je prikazan u narednom primeru.

Poboljšani ArrayAdapter (sa primenjenim ViewHolder pattern-om)

Prva stvar na koju trebamo da obratimo pažnju je ta da se u NEekonomičnom primeru pri svakom pozivanju metode getView() kreira novi “rowView”.

Ukoliko naša lista ima veći broj elemenata koji ne mogu da stanu na jedan ekran, pri svakom novom skrolovanju se generišu novi View-i (svaki View je 1-2kB), što stalno povećava opterećenje memorije.

convertView

Upravo zbog ovog problema Android prosledjuje metodi getVIew() parametar “convertView”. Ovaj parametar se koristi da u njega “skladištimo” View (npr. naš jedan rowView), koji ćemo kasnije koristiti iznova i iznova. Da bi smo sprečili “inflating uvek istog XML-a” (koja je skupa operacija) za svaki novi element liste, jednom napravljen View ćemo sačuvati u okviru convertView promenjive.

U prethodnom kodu proveravamo da li već postoji neki sačuvan View u promenjivoj, a ukoliko ga nema, mi ćemo ga kreirati (samo prvi put) kao naš rowView, jer će nam tako definisan convertView biti uvek dostupan kao parametar getView() metode.

ViewHolder pattern

Sledeći problem koji se javlja je učestalo pozivanje metode findViewById() za svaku podstavku (subView) convertView-a. Često pozivanje metoda findViewById() opterećuje sistem i smanjuje performance aplikacije (naručito ako ima mnogo podataka).

Za rešavanje ovoga problema nam u pomoć nam priskače tzv. “ViewHolder pattern” koji se oslanja na to da već koristimo convertView, i da možemo jednostavno da targetiramo sve njegove subView-ove (samo jednom) i tako izbegnemo stalno pozivanje metode findViewById(). Stoga kreiramo jednu unutrašnju statičnu klasu koja se često naziva ViewHolder.

Članove klase možemo da povežemo sa odgovrajućim View-om tako što ćemo pozivati findViewById(), ali uz napomenu da ćemo to uraditi samo jednom i to u trenutku kada prvi put definišemo convertView-a:

U prethodnom primeru smo koristili i metodu setTag() da bi smo sačuvali ViewHolder objekat ako smo ga već jednom napravili i onda smo ga kasnije pozvali metodu getTag() i tako dobili nazad sačuvani objekat.
Korišćenje ViewHolder pattern-a ubrzava populaciju ListView-a, te sa njim dobijamo glatko i brzo učitavanje stavki. Njegova implementacija omogućava da se izbegne korišćenje “skupog” metoda findViewById() u okviru adapter-a. Ceo primer custom adapter-a:

×

×

×

×

×

Method Description
notifyItemChanged(int pos) Notify that item at position has changed.
notifyItemInserted(int pos) Notify that item reflected at position has been newly inserted.
notifyItemRemoved(int pos) Notify that items previously located at position has been removed from the data set.
notifyDataSetChanged() Notify that the dataset has changed. Use only as last resort.


ArrayAdapter (osnovni android adapter)

Uvod

adapter

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.
ViewGroup: ListView, RecyclerView, GridView, Spinner, Gallery čiji sadržaj se popunjava korišćenjem adaptera se nazivaju zajedničkim imenom “AdapterView”.

Korišćenje adaptera kao medijatora izmedju “model-a” (podataka) i View-a, predstavlja varijaciju MVC patern-a pod nazivom MVA (ModelViewAdapter). To znači da Model i View nikada ne komuniciraju međusobno, već komuniciraju preko Adapter-a (koji je zapravo samo “event-driven Controller”). Prednost ovog patern-a je da View ne mora da zna ništa o modelu, te se na taj način postiže bolje razdvajanje odgovornosti.

adapter hijerarhija

Postoji više različitih adaptera, i njihov izbor zavisi od AdapterView-a za koji se koristi. Najjednostavniji adapter je “ArrayAdapter”, koji uzima podatke u vidu “ArrayList” i sa njima popunjava sadržaj View elementa koji su deo kontejner-a (“ListView”, GridView, Spinner). Za “RecyclerView” se koristi njegov specijalizovani “RecyclerView.adapter”, dok se za “ViewPager” koristi “PagerAdapter”.

Postupak kreiranja ArrayAdapter-a

Osnovni defaultni Android Adapter predstavlja “ArrayAdapter” koji koristi neki od default-nih layout-a koji su ugradjeni u Android.

a) Definisanje podataka koji treba da se prikažu

Prvo su nam potrebni podaci koji bi se smeštali u neki View, ArrayAdapter ima različite konstruktore, te stoga može da primi različite tipove podataka:


ili

Desni klik na res > values zatim se izabere New > Values resource file nakon čega se izabere ime fajla npr. “imena_ljudi_array”

Ovome se kasnije možemo pristupiti:

NAPOMENA:
Kada su podaci statična lista onda nije neophodno koristiti adaptere, možemo povezati podatke (resourse string-array) i View jednostavanije i bez korišćenja adaptera samo koristeći View XML atribut: android:entries.

b) Kreiranje AdapterView-a koji treba da prihvati podatke

Već je pomenuto da je AdapterView ustvari neki ViewGroup (ListView, RecyclerView…) čiji se sadržaj popunjava podacima uz pomoć adaptera, te je potrebno definisati neki AdapterView u okviru XML-a.

Primer

NAPOMENA:
Ako ekstendujemo aktivnost sa ListActivity (ili kod fragmenata sa ListFragment), i tada ne moramo da definišemo nikakav layout, jer u tom slučaju aktivnost (fragment) već sadrži podrazumevano kao root view jedan ListView koji može da primi podatke. Pored toga ListActivity i ListFragment nam takođe dozvoljavaju da “pregazimo” (override) metodu onListItemClick() i tako definišemo akciju koja će da se izvrši klikom na neki od članova liste. Pogledajte objedinjen kod iz primera ovde.

c) Kreiranje instance adaptera

Postoji više “ArrayAdapter” konstruktora koji mogu da prihvate različite tipove podataka, mada generalizovani konstruktor bi mogao ovako da izgleda:

Legenda:

simple

×

simple

  • Context
  • Neki predefinisani layout koji dolazi sa androidom (npr. “R.layout.simple_list_item_1” pogledaj sliku) ili neki custom layout. Listu predefinisanih layout-a možete da pogledate ovde.
  • Podaci koji se ubacuju da (lista, niz….)
Primer

ArrayAdapter zahteva da se tip elementa niza deklariše kao View (u ovom primeru kao String).

NAPOMENA:
ArrayAdapter po default-u u pozadini za svaku stavku niza poziva metodu “toString()” i tako generiše TextView u koji stavlja sadržaj člana niza. Ako želimo komplikovaniji layout koji ima više elemenata (npr. ImageView), onda je potrebno da napravimo custom ArrayAdapter što će biti objašnjeno u nekom drugom članku.

Pored ovog načina za kreiranje adaptera gde se koristi konstruktor metoda, za kreiranje adaptera možemo da koristimo i njegovu
metodu createFromResource(). Pogledajte sve metode ArrayAdapter-a i RecyclerView.adapter-a ovde.

d) Povezivanje adapter instance sa AdapterView-om

Povezivanje adaptera sa odgovarajućim View-om se vrši metodom setAdapter() (setListAdapter() kod “ListActivity”).

Sa ovim smo omogućili da adapter pokupljene podatke iz nekog resursa i ubaci kao String u svaki element AdapterView-a koji je stilizovan sa izabranim predefinisanim android layoutom (u primeru: “android.R.layout.simple_list_item_1”)

f) Definisanje click listener-a

Da bi se aktivirala akcija nakon klika na neki od elemenata liste je potrebno implementirati interfejs AdapterView.OnItemClickListener i override-ovati onItemClick() callback metodu. Metoda prihvata četri parametra od kojih prvi predstavlja parent view (u ovom primeru ListView).

Pogledajte objedinjen kod iz primera ovde.

×

Lista predefinisanih layout-a u sklopu androida.
  • activity_list_item
  • browser_link_context_header
  • expandable_list_content
  • list_content
  • preference_category
  • select_dialog_item
  • select_dialog_multichoice
  • select_dialog_singlechoice
  • simple_dropdown_item_1line
  • simple_expandable_list_item_1
  • simple_expandable_list_item_2
  • simple_gallery_item
  • simple_list_item_1
  • simple_list_item_2
  • simple_list_item_activated_1
  • simple_list_item_activated_2
  • simple_list_item_checked
  • simple_list_item_multiple_choice
  • simple_list_item_single_choice
  • simple_selectable_list_item
  • simple_spinner_dropdown_item
  • simple_spinner_item
  • test_list_item
  • two_line_list_item

Ovim layoutima se pristupa sa R.layout.nazivLayouta a njihov XML može da se pogleda ovde.

×


Konvertovanje XML resursa u View objekat (“Layout Inflating”)

Uvod

“Layout inflation” je termin koji označava postupak sa kojim se XML resurs parsira i konvertuje u View objekat.
Aktivnost ima svoju metodu setContentView() sa kojom inflate-uje svoj root view:

Primer

Ali konvertovanje drugih layout-a u View objekt (čime omogućavamo da elementi layout-a budu dostupni u kodu aktivnosti) možemo da uradimo na sledeće načine:

  1. Koristići inflate() metodu inflater objekta
  2. Koristeći statičnu metodu inflate() View klase

Metoda LayoutInflater.inflate()

Ovaj postupak se sastoji iz dva koraka:

  1. Pravljenje Inflater objekat
  2. Pozivanje inflate() metode

Pravljenje Inflater objekta

U aktivnosti

a) u okviru onCreate()

Ako smo u aktivnosti u sklopu onCreate() metode možemo da pristupimo ovom objektu jednostavno jer je dat kao parametrar metode onCreate():

b) van onCreate() metode

Medjutim ako smo van onCreate() metode moramo mu pristipiti na drugačiji način koristeći getLayoutInflater() metodu:

Van aktivnosti

a) Preko aktivnosti

Pa čak ako smo i van aktivnosti možemo mu pristupiti pozivajući aktivnost:

b) Preko context-a

Koristeći metodu context.getSystemService(Class)

Primer

ili na drugi način koristeći from() metod:

Pozivanje inflate() metode

Pozivanjem “inflate()” metode se vrši konverzija:

Legenda:
  • resource int: ID resursa
  • root ViewGroup: Opciona vrednost koja predstavlja neki View group koji će da bude roditelj ovom view-u (ako je treći parametar “attachToRoot” setovan na true). Ova vrednost može pri pozivu funkcije da bude null!
  • attachToRoot boolean: Kroz ovaj parametar se definiše šta će biti vraćeno iz metode. Ako je attachToRoot postavljen na true, onda je layout file navedena u prvom parametru inflate-ovana i priključena na ViewGroup definisan u drugom parametru, pa tada metod vraća ovaj kombinovani prikaz, sa ViewGroup kao root. Kada je attachToRoot false, layout file iz prvog parametra se inflate i vraća kao prikaz.
Primer

U ovome primeru treći parametar je “false” pa naš view nije ubačen u parent view (ovde container). Ako želimo da view bude ubačen u neki parent view potrebno je da stavimo “true” ili da to naknadno uradimo sa dodatnim kodom koristeć metodu addView().

Primer

NAPOMENA:
Postoji verzija i sa dva parametra (treći parametar je uvek true)

Statična metoda inflate() View klase

Za istu namenu može da se koristi i STATIČNA metoda View objekta inflate(). Ona u pozadini takodje koristi wrap-ovani inflater objekat čiji kod izgleda ovako:

Primer

Ova statićna metoda se najčešće koristi kod slučaja kada se definiše custom VIew:

Što bi bilo poptuno jednako sledećem kodu uradjenom preko inflator objekta:

Jedna od razlika je što u ovoj statičnoj metodi nemamo opciju sa tri parametra (to znači da je treći parametar uvek true)


Fragment u okviru androida

Uvod

fragment

Fragment je modularni deo aktivnosti, koji ima svoj životni ciklus, prima sopstvene ulazne događaje, možete ga dodati ili ukloniti dok se aktivnost pokreće. Fragment objedinje View i logiku tako da se može jednostavno višekratno koristiti unutar jedne ili više različitih aktivnosti. U aktivnostima može biti više od jednog fragmenta tako da svaki fragment može da predstavlja neki View unutar jedne aktivnosti.
Prednost arhitekture koja koristi fragmenate je što nam fragmenti omogućavaju ponovnu upotrebu koda, sa jednostavnim postupkom pravljenja različitih prikaza za tablete (landscape) i mobilne uredjaje.
Komunikacija izmedju dva fragmenata je prilično komplikovana i može izvesti na dva načina:

Fragmenat vs. Aktivnost

U aplikacijama koje koriste fragmente deo zaduženja aktivnosti se delegira u fragmente, pa bi prema toj podeli zaduženja koja ostaju u aktivnosti bi bila sledeća:

  • Da sadrži navigaciju do drugih aktivnosti putem intent-a ili navigacijske komponente (“NavigationDrawer”,“ViewPager”…)
  • Da skriva i prikazuje fragmenate (pomoću menadžera fragmenata)
  • Da prima podatake iz drugih aktivnosti (intent)
  • Da kumunicira sa fragmentima i posreduje komunikaciji između njih

Dok fragmenti preuzimaju obavezu da:

  • Prikazuju odgovarajući sadržaj
  • Event handling
  • Pokretanje network request-a
  • Preuzimanje i čuvanje podataka

Životni ciklus fragmenta direktno je pod uticajem životnog ciklusa aktivnosti. Kada je aktivnost pauzirana, onda su i svi su fragmenti u njoj pauzirani, a kada je aktivnost uništena, onda su i svi njeni fragmenti uništeni. Međutim, dok je aktivnost “živa”, možemo manipulisati sa svakim fragmentom nezavisno. Detaljan prikaz lifecycle fragmenta i aktivnosti možete pogledati ovde.

fragment lifecycle

NAPOMENA:
Kada implementirate neku metodu životnog ciklusa fragmenta uvek treba da pozovete nadklasu (npr. super.onStart();):

Primer

Kreiranje fragmenta

Extendovanje fragment klase

Kreiranje fragmenta se sastoji iz kreiranja odgovarajuće klase zadužene za logiku i pridodajući joj odgovarajući layout. Klasa zadužena za logiku mora da extenduje neku od sledećih klasa:

  • Fragment je glavna klasa dok su ostale njegove podklase (pogledajte kako izgleda boilerplate kod koji generiše android studio).
  • DialogFragment – Fragment koji prikazuje prozor za dijalog, koji lebdi na vrhu prozora njegove aktivnosti. Ovo se obično koristi za prikazivanje dijaloga upozorenja, dijaloga za potvrdu ili za traženje informacija od korisnika u okviru bez potrebe za prebacivanjem na drugu aktivnost, dozvoljavajući korisniku da se vrati na prethodni fragment.
  • PreferenceFragmentCompat se koristi za kreiranje settings liste za našu aplikaciju odkle korisnici imaju mogućnost da promene funkcionalnost i ponašanje aplikacije. (pročitajte više u dokumentaciji).
  • ListFragment se koristi za prikazivanje liste nekih podataka i ima već implementiran event listener na clik nekog člana iz liste, tako da je potrebno samo da definišemo metod onListItemClick() (primer).

Povezivanje fragmenta sa njegovim layout-om

Da biste obezbedili layout za fragment, morate da implementirate “onCreateView()” metodu, koju Android poziva kada dođe vreme da fragment iscrta svoj layout. Implementacija ovog metoda mora da vrati prikaz koji je root layout vašeg fragmenta. Povezivanje fragment klase i njenog view-a se vrši korišćenjenm inflate() metode u sklopu lifecycle metode onCreateViewa:

Pogledajte više o postupku ubacivanja view-a iz xml-a u klasu “Layout inflation” u članku “Konvertovanje XML resursa u View objekat”.

Pored ovog načina možemo da kreiramo fragment na osnovu androidovog template-a pod nazivom Fragment(Blank) u čijem sklopu dolazi dosta pripremljeno boilerplate koda. Više o ovome pogledajte ovde.

Targetiranje elemenata u okviru layout-a

Da bi u okviru fragmenta mogli da koristimo metodu findViewById() potrebno je da prvo targetiramo njegov layout. To možemo uraditi na dva načina u zavisnosti gde nam u kodu treba:

  1. U okviru metoda onCreate() i onCreateView() prvo je potrebno da inflate-ujemo layout, nakon čega možemo da koristimo metodu findViewById():

    Fragment za razliku od aktivnosti nije podklasa klase Context, što znači da nema pristup globalnim informacijama o okruženju aplikacije. To znači da fragment ne može da koristi this za dobijanje context-a. Iz tog razloga u sklopu onCreateView() metode je kao parametar prosledjen objekat LayoutInflater koji ima pristup kontekstu, te stoga ako nam je potreban context možemo njega da iskoristimo inflater.getContext().

  2. U okviru metode onViewCreated() targetiramo fragmentov layout sa metodom getView(). Ova metoda je dostupna kada ekstendujemo našu klasu sa klasom Fragment i može da se pozove samo nakon kreiranja view-a, te je stoga ne možemo koristiti unutar onCreate() ili onCreateView() metode.

Embendovanje fragmenta u aktivnostima

Aktvnost koja sadrži fragment mora da ekstenduje ili FragmentActivity ili njenu potklasu AppCompatActivity. Fragmente u aktivnost možemo da dodamo na dva načina:

a) Statično ubacivanje fragmenta direktno u layout aktivnosti

Statično ubacivanje fragmenta u aktivnost podrazumeva da se fragment ubaci u layout aktivnosti kao view.

Fragment ubačen na ovaj način mora da ima definisan id u okviru XML-a, jer se preko njega targetira u okviru aktivnosti koristeći metodu findFragmentById():

Kada imamo referencu na fragment onda možemo da pozivamo njegove public metode ili properties-e.

b) Programirano ubacivanje fragmenta u aktivnost

Za programirano ubacivanje fragmenta u aktivnost je postupak sledeći:

1.) Kreiranje fragment kontejnera u XML-u

Ako vaša aktivnost dozvoljava da se fragmenti uklone i zamene, trebalo bi da dodate početni fragment (tzv. fragmentKontejner) u “onCreate()”. Taj kontejner se koristi da u njega možemo kasnije da ubacimo neki drugi fragment.

2.) Kreiranje fragmentManager-a

Fragment manager se instancira pozivajući metodu getSupportFragmentManager()

Fragment manager

Fragment manager je objekat koji zadužen za rad sa fragmentima za navigaciju izmedju njih kao i referenciranje fragmenta u okviru aktivnosti:

Metod Opis
addOnBackStackChangedListener Dodaje listener kada dodje do promene back stack-a (više o ovome pogledajte ovde)
beginTransaction() Kreira novu transakciju.
findFragmentById(int id) Nalazi fragment koji je inflated-ovan direktno u XML layout aktivnosti.
findFragmentByTag(String tag) Nalazi fragment preko tag-a
popBackStack() Uklanja fragment sa backstack-a.
executePendingTransactions() Forsira izvršenje transakcije.
3.) Kreiranje instance fragmenta

Kreiranje instance fragmenta je standardno korišćenjem new operatora:

Ukoliko želimo da prosledimo i neke podatke pri instanciranju onda oni moraju biti definisani u okviru konstruktorske metode:

Pa ih onda prosledjujemo kao parametar:

Preporuka je da se koristi tzv. newInstance pristup, kada se u okviru fragmenta definiše factory metoda koja služi za instaciranje fragmenta:

Kasnije u fragmentu okviru metode onCreate() možemo da zatražimo podatke generisane pri instanciranju fragmenta upravo preko naziva argumenta:

Drugi argument u get-eru je defaultna vrednost u slučaju da ne nadje argument.

4.) Izvršavanje neke od transakcija sa fragmentima (add, remove, replace)

Potoji API za rad sa fragmentima u aktivnosti (add, remove, or replace a fragment) i zove se FragmentTransaction. Instancu FragmentTransaction dobijamo koristeći fragmentManager.

Jedna od metoda koju može da izvrši FragmentTransaction je i add(). Ova metoda služi za ubacivanje našeg fragmenta u sklop nekog ViewGroup-a (kontejner). Taj ViewGroup se definiše kroz prvi parametar, dok se kroz drugi parametar definiše fragment koji dodajemo, a kroz treći parametar definišemo TAG koji će da obeleži dodati fragment (na osnovu taga kasnije možemo da ga targetiramo sa fragmentManager.findFragmentByTag(“TAGFRAGMENTA”);).

Da bi se klikom na “back” dugme vratili na prethodno stanje (tj. da ne bi smo zatvorili aktivnost što je podrazumevano) potrebno je da dadamo naš fragment na stack tzv. “backStack” sa metodom addToBackStack().

Po definisanju svih naredbi (može da ih bude više od jedne) je potrebno potvrditi akciju (commit-ovati), nakon čega će one biti i stvarno izvršene:

Akcije koje smo commit-ovali se ne izvršavaju odmah već ih stavljaju na čekanje za izvršavanje na glavnoj niti (main thread). Akcije će se izvršiti tek kada nit bude spremna. Pogledajte primere transakcija ovde.

Targetiranje fragmenta iz aktivnosti

Za targetiranje fragmenta u okviru aktivnost koji je ubačen na ovaj način se koristi metoda findFragmentByTag() kojoj se prosledjuje parametar TAG (definisan kroz treći parametar metode replace()).

Navigacija izmedju fragmenata

Navigacija izmedju fragmenata može biti definisana u sklopu aktivnosti koristeći samo FragmentManager, medjutim ništa nas ne sprečava da koristimo neki od sledećih pristupa:

  • TabLayout (tabovi, pogledajte više u članku)
  • Fragment Navigation Drawer (meni sa strane pogledajte više u članaku)
  • ViewPager (prebacivanje slajdovanjem izmedju fragmenata, pogledajte više u članku)

×

Kreiranje od pripremljnog boilerplate “Fragment(Blank)”

Factory statička metoda newInstance() se koristi pri instanciranju fragmenta u okviru aktivnosi i omogućava jednostavno prosledjivanje parametara pri instanciranju fragmenta:

Sada u okviru fragmenta možemo da koristimo prosledjene parametre koristeći promenjive mParam1 i mParam2 a instanciranje Fragmenta u aktivnosti se vrši sa sledećim kodom:

NAPOMENA:
Ako je fragment podklasa ListFragment-a, kod njega se po default-u vraća ListView u metodi onCreateView(), tako da ne mora da se implementira deo vezan za povezivanje logike sa layout-om osim ako ne koristimo custom layout.

U sklopu boilerplate koda dolazi deo vezan za CustomListener koji se koriste za prosledjivanje podataka iz fragmenta u aktvinost (drugi fragment).

Više o custom listener-ima pogledaj te u članku “Kreiranje custom listenera”.

×

Primer – ListFragment

×

Primer

U ovome primeru je prikazano definisanje početnog fragmenta u aktivnost. U prazan tag kontejnera se dodajemo novi view naš fragment.

Primer

U ovome primeru pozivajući metodu showFragmentA dolazi do zamene fragmenta

×

full fragment lifecycle

×

×

Back Stack

Stack je tip memorije u koji se smeštaju elementi jedan na drugi, tako da poslednji dodati element je na vrhu (analogija sa gomilom tanjira). Elementi se uklanjaju sa stacka obrnutim redosledom, tako što se poslednje dodati element uklanja prvi. U okviru Androida postoji stack u koji se smeštaju sve aktivnosti prema redosledu pozivanja. Pritiskanjem “back” dugmenta poslednja aktivnost sa stacka se briše. Medjutim u slučaju korišćenja fragmenta to nije defaultno ponašanje pa kada korisnik pritisne back sa stacka ne ukloni poslednje dodati fragment već cela aktivnost. To nije očekivano ponašanje te je potrebno je da tu funkcionalnost programer “ručno” doda.

backstack

addToBackStack

Da bi se i fragmenti ubacili na backStack potrebno je da se dodaju koristeći addToBackStack() metodu. Ovu metodu je potrebno dodati pre svakog commit-a. Tekst koji se prosledjuje je opcioni i koristi se ako kasnije želimo da prepoznamo tu transakciju, medjutim sasvim je ok da se prosledi i null kao parametar.

FragmentManager.OnBackStackChangedListener

Ako aplikacija sa promenom fragmenata treba da ažurira i druge elemente korisničkog interfejsa (npr. actionBar) to znači da bi trebalo se reaguje nakon promene backStack-a. U tom slučaju je potrebno da se iskoristi interfejs addOnBackStackChangedListener i da se definiše callback metoda onBackStackChanged() koja treba da izvrši akciju nakon okidanja dogadjaja

Primer kada aktivnost implementira interfejs

Primer kada se callback metoda onBackStackChanged() definie “on the fly”:


Kreiranje custom listenera u Androidu (listener pattern)

Uvod

listener

“Listener pattern” je jedan od najčešće korišćenih paterna koji se koriste pri razvoju aplikacijja. Princip rada se zasniva na tome da u jednoj klasi definišemo dogadjaj i trenutak kada se započinje izvršavanje dogadjaja (“okida dogadjaj”), dok u drugoj klasi definišemo objekat (tzv. listener objekat) koji osluškuje taj dogadjaj i reaguje na njega.

Postoji priličan broj već ugradjenih listener-a u sklopu samog androida, ali pored njih možemo da kreiramo i sopstvene custom listener-e i tako omogućimo da definišemo callback metode za događaje koju su trigerovani i iz drugih delova našeg koda. Custom listeneri se koriste u sledećim slučajevima:

Primer standardnog listenera ugradjenog u operativni sistem

Ovaj patern se često korisi i u sklopu operativnog sistema, a najbolji primer ugradjenog interfejasa u android core je OnClickListener koji je zadužen za “klik” dogadjaj. Ovaj listener je definisan u sklopu View.java klase.

U sklopu View.java klase je definisana setter metoda pod nazivom setOnClickListener(). Pozivanjem ove setter metode u nekoj drugoj klasi se definiše listener objekat koji osluškuje taj dogadjaj, a kroz njega i callback metoda koja reaguje na dogadjaj. U ovom primeru objekat “btnNekiButton” je naslednik View.java klase a samim tim nasledio je sve interfejse uključujući i pomenuti, pa može jednostavno da pozove njegovu setter metodu:

Ili ako prikažemo poznati kod sa “on the fly” kreiranim objektom:

OBJAŠNJENJE:

Instanciranje “on the fly” anonimnog objekta koji implementira interfejs

Anonimna klasa je klasa koja nema ime, stoga kada bi hteli da instanciramo objekat od ovakve klase to bi izgledalo ovako:

Nama je potrebna specifična anonimna klasa, ona koja implementira odredjeni interfejs, stoga iako ne moramo da definišemo ime klase moramo da definišemo koji interfejs da implementiramo. Ako je interfejs definisan u svome fajlu to se postiže ubacivanjem naziva interfejsa ispred zagrada:

Takodje moramo da implementiramo metode interfejsa:

Postupak

Interfejs može da se definiše samostalno u odvojenom fajlu u komunikaciji izmedju dve klase se iz praktičnih razloga definiše u jednoj klasi. U ovim primerima ćemo koristi termine “PrvaKlasa” (mesto gde se definiše interfejs tj. dogadjaj) i “DrugaKlasa” (mesto gde se registruje objekat koji osluškuje i definiše callbackMetoda tj. akcija koja treba da se izvrši nakon “okidanja” dogadjaja).

a) Interfejs (prva klasa):

Kao i kod već pripremljenih listenera u okviru android operativnog sistema i ovde je potrebno prvo kreirati interfejs. Ovaj deo posla je u prethodnom primeru napisan u sklopu androida, dok ćemo u ovome slučaju mi to da uradimo. Interfejs obezbedjuje da se svaki objekat koji implementira interfejs ima callback metodu:

b) Setter metoda (prva klasa)

Pored interfejsa je potrebna setter metoda, čijim se pozivanjem definiše koji objekat je listener (tj. objekat koji osluškuje dogadjaj). Ova metoda nam omogućava da taj objekat definišemo bilo gde, jer je dovoljno da ga prosledimo kao parametar kada pozovemo tu metodu (pogledaj više o tome ovde).

c) Okidanje dogadjaja = pozivanje callback metode (prva klasa)

Pozivanje callback metode se može smatrati kao okidač dogadjaja, stoga negde u okviru klase pozovite callback metodu i na taj način će biti “okinut” prekidač i startovan dogadjaj:

Mada treba izbeći exception:

Celokupni kod iz prve klase možete pregledati ovde.

d) Definisanje objekta koji osluškuje i callback metoda

Svi dosadašnji delovi se kreiraju u jednoj klasi koja je napravilia dogadjaj dok se ovaj deo koda nalazi u nekoj drugoj klasi i zadužen je da u toj drugoj klasi definiše listener objekat koji osluškuje dogadjaj. Definisanjem listener objekta praktično definišemo i callback metodu koja reaguje na dogadjaj. Postoji više načina da se to izvede:

d1) Definisanje listener objekta i callback metode kroz setter metodu

Definisanje callbackMetode može da se izvrši na dva načina u zavisnosti šta je listener.

d1.a) Listener = anonimni objekat

U ovome slučaju definisanje callback metode se vrši pozivanjem setter metode i prosledjivanjem “anonimnog” objekta koji implementira interfejs a samim tim i callback metodu. Da bi mogli da pozovemo metodu iz druge klase potrebno je da je pozovemo preko objekta prve klase ili objekta koji implementira interfejs (više o “objekatKojiImplementiraInterfejs” pogledajte ovde).

Objekat koji prosledjujemo kroz parametar kreiramo kao instancu anonimne klase koja implementira interfejs (kako se “on the fly” kreira objekat od anonimne klase pogledajte ovde).

d1.b) Listener = Cela druga klasa

U ovome slučaju cela klasa implemetira interfejs, pa je potrebno da se cela klasa registruje kao osluškivač tako što se kroz setter metodu prosledi klasa koristeći ključnu reč “this” a zatim override callbackMetoda:

d2) Definisanje listener objekta i callback metode kroz konstruktor

Ako je listener izrazito bitan za samu klasu onda se setter metoda zameni konstruktorskom metodom same klase:

U drugoj klasi kreiramo objekat na osnovu konstruktorske metode PrveKlase, tako što kroz parametar prosledjujemo “on the fly” kreiran anonimni objekat koji implementira listener:

d3) Definisanje listener objekta i callback metode kroz lifecycle metodu

Ovaj pristup se koristi kod komunikacije izmedju fragmenta i aktivnosti.

Fragment

Postupak u okviru fragmenta je sličan postupku (kodu) iz PrvaKlase, tako da se u okviru fragmenta nalaze sva tri prethodno opisana koraka: kreiranje interfejsa, setter metode i okidanje dogadjaja.

Aktivnost

Kod Aktivnosti se postupak delimično razlikuje od DrugeKlase jer mora da se u sklopu metode onFragmentAttach() proveri da li aktivnost implementira interfejs. Tek kada smo sigurni da aktivnost implementira interfejs onda se definiše da aktivnost postane listener objekat. Overajdovanjem callbackMetode se definiše akcija nakon okidanja dogadjaja:

Takodje postoji i drugi način kada se u okviru samog fragmenta proverava da li aktivnost implementira interfejs ili ne. Tada se ceo kod provere izvršava u fragmentu u okviru metode onAtach() (pogledajte ceo kod ovde).

NAPOMENA:
Ukoliko treba da se registruje više od jednog listenera onda je potrebno prilagoditi kod da radi sa nizom listener-a:

Pogledajte ceo novi kod ovde.

Komunikacija Fragment – Aktivnost

U ovom primeru će biti objašnjena interakcija izmedju fragmenta i aktivnosti. U okviru fragmenta kreiranja interfejs i vrši “okidanje” custom dogadjaja, dok se u aktivnosti defiše telo callback-a tj. reakcija na izvršenje tog custom dogadjaja iz fragmenta.

MyListFragment

Aktivnost:

Komunikacija Fragment – Fragment

Komunikacija izmedju njih se može ostvariti na dva načina:

Pošto je u ovome članku tema listener patern, u narednom primeru ćemo prikazati kako se radi komunikacija izmedju dva fragmenta koristeći listenere iako je korišćenje deljenog “ViewModel-a” jednostavniji pristup.
Princip rada je sledeći: poruka iz PrvogFragmenta se šalje u Aktivnost, nakon čega Aktivnost kroz callbackMetodu šalje poruku DrugomFragmentu.

PrviFragment

Aktivnost

DrugiFragment

Na sličan način se rešava komunikacija izmedju Dialoga i Aktivnosti.

Komunikacija Adapter – Aktivnost

U ovome primeru se standardno definiše interfejs, a zatim i setter metoda, kao i okidanje dogadjaja u okviru adapter klase. Iako se u primerima na netu često može naći da se okidanje dogadjaja vrši u okviru “onBindViewHolder() metode, preporuka je da se okidanje dogadjaja vrši u okviru ViewHolder klase tj. u okviru njenog konstruktora.

Adapter

Nakon definisanja interfejsa, potrebno je u fragmentu ili aktivnosti koja koristi adapter registrovati listener i definisati callback metodu:

Fragment ili Aktivnost

Ovde ćemo definisati anonimni listener objekat “on the fly” i u njemu definisati akciju na okidanje dogadjaja:

Ovo je moglo da se uradi i na drugi način: kada Aktivnost implementira interfejs, nekon čega je dovoljno da se overajduje callback metoda za definisanje akcije nakon okidanja dogadjadja.

×

Instanciranje anonimnog objekta koji implementira interfejs “on the fly”:

Anonimna klasa je klasa koja nema ime, stoga kada bi hteli da instanciramo objekat od ovakve klase to bi izgledalo ovako:

Nama je potrebna specifična anonimna klasa, ona koja implementira odredjeni interfejs, stoga iako ne moramo da definišemo ime klase moramo da definišemo koji interfejs da implementiramo. Ako je interfejs definisan u svome fajlu to se postiže ubacivanjem naziva interfejsa ispred zagrada:

Takodje moramo da implementiramo metode interfejsa:

Komunikacje izmedju dve klase:

Kada je interefejs potreban za komunikaciju samo izmedju dve klase onda se on obično definiše i kreira u okviru tzv. prve klase te je potrebno pozvati interfejs preko prve klase:

Pored ovoga je potrebno da implementiramo sve abstraktne metode ovog interfejsa:

U ovom primeru smo koristeći “anonimnu klasu” kreirali “anonimniListenerObjekat” (tj. objekat koji osluškuje), ali “listenerObjekat” ne mora da bude anoniman može da se sačuva u nekoj promenjivoj i da se koristi više puta:

×

Instanciranje anonimnog objekta koji implementira interfejs “on the fly”:

Anonimna klasa je klasa koja nema ime, stoga kada bi hteli da instanciramo objekat od ovakve klase to bi izgledalo ovako:

Nama je potrebna specifična anonimna klasa, ona koja implementira odredjeni interfejs, stoga iako ne moramo da definišemo ime klase moramo da definišemo koji interfejs da implementiramo. Ako je interfejs definisan u svome fajlu to se postiže ubacivanjem naziva interfejsa ispred zagrada:

Takodje moramo da implementiramo metode interfejsa:

×

×

×

Ako je “objekatKojiImplementiraInterfejs” naslednik View klase onda može da bude ubačen u View druge klase kao “custom view”.

1) Ubačen statički (direktno u .xml)

Ako je “objekatKojiImplementiraInterfejs” ubačen statički (kao custom view u layout) onda ga targetiramo kao običian view koristeći metodu indViewById():

2) Ubačen programirano
2.a) Kreiranjem objekta PrveKlase

“listenerObjekat” je ustvari anonimni objekat kreiran “on the fly”:

2.b) DrugaKlasa implementira interfejs

U slučaju da cela klasa implementra interfejs onda se koristi:

×

Prva klasa

Druga klasa

×

Kod ovog pristupa u okviru Aktivnosti ne definišemo koji objekat će biti listeneru, već to radimo u sklopu samog fragmenta kroz metodu onAttach(). Tako da listener objekat postaje bilo koja aktivnost na koju se nakači fragment a da pri tom implementira interfejs:

U aktivnosti koja implementira interfejs se samo override callbackMetoda da bi definisali akciju po okidanju dogadjaja: