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. MVVM 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

Pri korišćenju MVVM paterna “kod” aplikacije je razdvojen na tri dela:

  1. “View” — Ova sekcija sadrži klase (Aktivnosti i Fragmenti) koje su zadužene za prikaz interfejsa i prihvatanje akcija korisnik (nakon čega o tome obaveštava “ViewModel”)
  2. “ViewModel” — Ova sekcija sadrži klase koje su zadužene za pristup podacima (Repository) i da obaveste “View” (aktivnost/fragment) ukoliko dolazi do promena.
  3. “Model” (DataModel) — Ova sekcija sadrži klase zadužene za pristup raznim vrstama podataka (baza, webservice…) i da abstrahuje takve izvore podataka kroz jedan API.

mvvm diagram

Svaka komponenta zavisi samo od komponenti na jednom nivou ispod nje, “View” zavisi samo od “ViewModel”-a, “ViewModel” zavisi samo od “Repository”, dok “Repository” jedini može da zavisi od više drugih klasa.

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

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”.
“ViewModel” deli informacije sa ostatkom aplikacije tako što emituje tokove događaja (streams of events). “ViewModel” nije briga ko koristi podatke koje on emituje i tako da nema potrebu za referencu na View.

Observe pattern

Preporučeni način za komunikaciju izmedju “ViewModel”-a i View-a 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 informacije 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.

“View”

“View” sekcija je zadužena za prikazivanje podataka i prihvatanje korisničkih akcije koje dalje prosledjuje u “ViewModel” (tzv. Pasivni prikaz).

Keep the logic in Activities and Fragments to a minimum

Pored navedenih obaveza (prikaz i hendlovanje korisničkih akcija) View je takodje zadužen za ažuriranje prikazanih podataka. 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).

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” je klasa koja je dizajnirana da preživi konfiguracione promene i odgovorna za upravljanje podacima i pripremu istih za aktivnost ili fragment. To znači da naša aktivnost ili fragment više ne moraju imati odgovornost čuvanja stanja podataka pošto “ViewModel” preuzima tu odgovornost. “ViewModel” takođe može da upravlja komunikacijom izmedju aktivnosti/fragmenta i ostatka aplikacije (npr. poziva klase koje sadrže poslovnu logiku). Takodje se č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.

Don’t let ViewModels know about Android framework classes

Preporuka je da “ViewModel” klase ne koriste android framework klase tj. da nema android.* imports u okviru klase. “ViewModel” je u direktnoj vezi sa fragmentom/aktivnosti ali neće biti uništen ako je njegov vlasnik uništen (promenom konfiguracije npr. rotacije uredjaja), nova instanca vlasnika će se ponovo povezati na postojeći “ViewModel”. Upravo je to jedna od glavnih namena “ViewModel”-a (nakon što prikupi podatke) da sačuva informacije koje su neophodne za View.

Avoid references to Views in ViewModels.

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).

NAPOMENA:
Iako ViewModel može da preživi promenu konfiguracije (rotacija ekrana…) ipak ne živi beskonačno. “ViewModel” ne može da preživi back dugme ili ubijanje aktivnosti od strane opertativnog sistema ili korisnika. Što vodi do zaključka da ViewModel klasa nije zamena za trajno čuvanje podataka (baza…) ili onSaveInstanceState().
Treba napomenti da je trenutno u alpha verziji SavedStateHandle tzv. “Saved State module for ViewModel” sa kojim možemo da prebacimo kod iz onSaveInstanceStata() metede u ViewModel. Pročitate više o ovome u članku “ViewModels: Persistence, onSaveInstanceState(), Restoring UI State and Loaders”.

“Data repository”

Aplikacija može da ima više izvora podataka:

  • Remote: network or the Cloud
  • Local: database or file
  • In-memory cache

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”. Repository nije neka specijalna arhitektonska komponenta već obična klasa koja ima pristup svim izvorima podataka.

Data repository as the single-point entry to your data

Ovaj sloj je vezan za izvore podataka u aplikaciji, i potpuno je nesvestan sloja prezentacije. Repository ima namenu da ViewModel-u uprosti problem pristupa podacima tako što mu obezbedjuje jednostavan API za to.

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.

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)
  • Fragment Navigation Drawer (meni sa strane)
  • ViewPager (prebacivanje slajdovanjem izmedju fragmenata)

Komunikacija izmedju fragmenata se izvodi preko Aktivnosti, a za to se koristi “listener patern”

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

NAPOMENA:
Fragment nije aktivnost pa samim tim 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, već mora da koristi drugi objekat koji ima pristup context-u. Iz tog razloga u sklopu onCreateView() metode je prosledjen parametar LayoutInflater koji ima pristup kontekstu, te stoga ako nam je potreban context nega koristimo inflater.getContext().

Ž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:
Uvek treba da pozovete ntklasu kada implementirate metode životnog ciklusa fragmenta. Npr. pri override metode onStart() u telu moramo da pozovemo super.onStart();

Kreiranje fragmenta

a) Postupno kreiranje od nule

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).

NAPOMENA:
Fragmenti nemaju metod findViewById().

Preko RootView-a

Ovaj način podrazumeva da prvo definišemo tzv. “rootView”, pa tek onda preko njega da koristimo findViewById():

getView()

Medjutim kada smo van metoda onCreate() i onCreateView() root view-a dobijamo sa metodom getView() (metoda getView() je dostupna čim ekstendujemo našu klasu sa klasom Fragment). Metoda getView() može da se pozove samo nakon kreiranja view-a, te je ne možemo koristiti unutar onCreate() ili onCreateView() metode, već samo u okviru onViewCreated() metode.

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”.

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.

b) Kreiranje od pripremljnog boilerplate “Fragment(Blank)”

Ovaj pristup podrazumeva kreiranje fragmenta na osnovu androidovog template-a pod nazivom Fragment(Blank) u sklopu koga dolazi dosta boilerplate koda.

Factory statička metoda newInstance()

Ova metoda 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:

Aktivnost

Custom Listener

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”. A ceo boilerplate kod možete da pogledate ovde.

×

Korišćenje fragmenta u aktivnostima

Fragmente u aktivnost možemo da dodamo na dva načina:

  1. Statično ubacivanje fragmenta direktno u layout aktivnosti
  2. Programirano ubacivanje fragmenta u postojeću ViewGroup-u

Napomena:
Aktvnost koja sadrži fragment mora da ekstenduje ili FragmentActivity ili njenu potklasu AppCompatActivity

Statično ubacivanje fragmenta direktno u layout aktivnosti

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

Sa fragmentom ubačenim na ovaj način se manipuliše kao sa bilo kojim view-om.

Programirano ubacivanje fragmenta u aktivnost

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

a) 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.