# The One App ***Osnovni podaci o projektu*** - **predmet**: Razvoj mobilnih aplikacija - **predmetni profesor**: Prof. dr Elmedin Selmanović - **autor**: Sara-Farah Materne - **studijska godina**: 2020/2021 ***O aplikaciji*** ![](https://i.imgur.com/ddNKmBs.png) Aplikacija **The One App** razvijana je kao završni projekat iz predmeta *Razvoj mobilnih aplikacija*. U sklopu projekta bilo je neophodno razviti Android aplikaciju koja će omogućavati prikaz podataka koji se dobiju kroz neki Web API. Web API koji je iskorišten u sklopu ovog projekta je *The One API*. Ovaj API je free-to-use servis koji omogućava dobavljanje, te korištenje podataka vezanih za *The Lord of the Rings* filmove i knjige, te detalje o istima kao što su likovi, citati, statistike o filmovima i sl. Više o API-ju se može pročitati na linku *https://the-one-api.dev/* Osim direktnog prikazivanja podataka pozivanjem *GET* zahtjeva sa servisa, u sklopu ovog projekta omogućeno je keširanje podataka sa ciljem poboljšanja korisničkog iskustva. Ovo je podrazumijevalo implementaciju lokalne *Room* baze u koju korisnik spašava dobijene podatke sa API-ja, ukoliko je obezbijeđena konekcija sa Internetom. Ukoliko korisnik ima poteškoća sa Internet konekcijom, tada se podaci dobavljaju iz lokalne baze. U sklopu *The One App*, podaci koji se keširaju su podaci vezani za citate iz filmova nastalih na osnovu knjiga J.R.R. Tolkiena. Osim mogućnosti keširanja ovih podataka, u sklopu aplikacije, ponovno korištenjem *Room* baze podataka, implementirano je i spašavanje proizvoljnog Movie objekta u lokalnu bazu. Spašavanjem filmova kreira se simulacija liste filmova koje korisnik želi pogledati. Nakon što se otvori lista, iteme je moguće komentarisati (comment), te ocijeniti (rate), pri čemu se ove promjene spašavaju u bazu. Također je podržano i brisanje pojedinačnih itema. Ostali objekti koji se mogu prikazati (Characters, te sve dostupne instance tipa Movies) dobavljaju se direktno sa API-ja, kako bismo imali mogućnost upoređivanja situacija u koje ćemo doći zahvaljujući npr. nestabilnoj ili nepostojećoj Internet konekciji. Ovi objekti, u slučaju nepostojanja konekcije uopšte neće biti prikazani. Filtriranje podataka obezbijeđeno je za objekte tipa Character, te je moguće dobaviti likove po njihovoj rasi. Također, prilikom prikaza liste objekata tipa Movies, klikom na pojedini item, otvara se detaljni prikaz tog objekta u novom fragmentu. Tada je on obogaćen nekim dodatnim podacima koji prethodno nisu bili prikazani. ![](https://i.imgur.com/Kd3Fp63.png) ***Arhitektura aplikacije*** Već pri izradi semestralnog projekta susreli smo se sa poteškoćama sa čuvanjem podataka prilikom promjene stanja u kojoj se aplikacija nalazi (npr. rekreiranje fragmenta prilikom okretanja uređaja). Sada imamo ozbiljniji rad sa podacima - onima sa web servisa, ali i u lokalnoj bazi. Zahvaljujući tome, neophodno je aplikaciju strukturirati kroz složeniju arhitekturu. Podatke je, dakle, potrebno na ispravan način čuvati, ali i omogućiti njihov prikaz te izmjene. Neophodno je, dakle, razdvojiti komponente aplikacije (u našem slučaju, fragmente) od podataka i stanja koje aplikacija koristi, te komponente međusobno. Ovakav princip naziva se **Separation of concerns**. On zahtijeva da se u UI-klasama (aktivnostima i fragmentima) piše samo logika vezana za UI i interakciju sa operativnim sistemom. Još jedan bitan princip jeste da se podaci koji se prikazuju u sklopu UI dobiju iz dosljednog modela. Modeli su komponente zaslužne za obradu i čuvanje podataka u sklopu aplikacije. Oni ne ovise o View objektima i komponentama, stoga na njih ne utječe životni ciklus aplikacije. Iz tog razloga oni su pogodni za konzistentno čuvanje podataka. ![](https://i.imgur.com/dsG3nD4.png) Vidimo da svaki sloj aplikacije ovisi o onom ispod njega. Iz tog razloga, prvo recimo nešto o baznim - najnižim slojevima. ***Web service*** Ne postoji jedinstveno pravilo o tome kako web servisi trebaju funkcionisati, ali najrasprostranjeniji pristup je **REST** (Representational State Transfer). Također, ne postoji precizan konsenzus o tome šta RESTful web servis sadrži. Lako se pretpostavlja da se nedostatak detaljnih specifikacija ogleda u brojnim neslaganjima o tome šta REST ustvari znači, te kako se RESTful servisi kreiraju. Ove ćemo filozofije u našem radu zanemariti, te ćemo se držati premise koja je u srži REST-a, te oko koje uglavnom ne postoje sukobi mišljenja: web servis definiše API kroz kombinaciju URL i HTTP metoda kao što su GET i POST. Ove metode specificiraju tip operacije, dok URL specificira objekat (ili više njih) na koje se metode primjenjuju. Za pristup backendu, koji obezbjeđuje REST API, u sklopu ovog projekta koristi se **Retrofit** biblioteka. Ona je type-safe HTTP klijent za Android i Javu. U našem projektu, kreiran je interfejs LOTRApiService, u sklopu kojeg su definisane GET metode za dobavljanje objekata tipa Character, Movie i Quote. Metode imaju obaveznu anotaciju *@GET*, a budući da je za pristup web servisu potrebna autorizacija u vidu tokena, njega u zahtjevu naglašavamo kroz **@Header** anotaciju. Za Character još omogućavamo filtriranje, te se ova funkcionalnost omogućava na način da dodamo **@Query** prije naziva atributa po kojem se vrši filtriranje. Također, deklaracije ovih metoda sadrže ključnu riječ **suspend**, pa uvedimo teoriju koja stoji iza ovog principa, a to su **Coroutines**. Korutine predstavljaju koncept sličan threadovima (nitima): one izvršavaju jedan dio koda, dok je omogućeno istovremeno izvršavanje ostatka. To jeste, korutine omogućavaju izvršavanje asinhronog dijela koda na isti način na koji bi se izvršavao sinhroni. Na ovaj način izbjegava se potreba za obraćanjem pažnje na komplikovanu sintaksu, sa kakvom smo se susretali u npr. Javi, te u drugim jezicima. Korutine nisu vezane za neku određenu nit. Mogu započeti izvršavanje na jednoj, a nastaviti na drugoj. Ključna riječ **suspend** odnosi se na suspendovanje korutina (zadataka koji se izvršavaju na nekoj niti). Kada dođe do suspendovanja korutine, odgovarajuće komputacije se pauziraju, uklone sa niti, i sačuvaju u memoriju. U međuvremenu, nit se može baviti drugim zadacima: ![](https://i.imgur.com/SwRsLXz.png) Kada se izvršavanje zadatka treba nastaviti, vraća se na nit, ali ne nužno onu na kojoj je izvršavanje započeto. Razlog za uvođenje korutina jeste kako bismo organizovali izvršavanje zadataka koji duže traju, a koji bi u suprotnom mogli blokirati glavni thread, te uzrokovati da aplikacija postane unresponsive. Naredna biblioteka sa kojom se surećemo jeste **Moshi**. Moshi je moderna JSON biblioteka za Android i Javu, te omogućava parsiranje JSON objekata u Java objekte na jednostavan način. Sa druge strane, imamo model. Model se ustvari odnosi na DataModel. ***Room*** Room biblioteka predstavlja sloj apstrakcije nad SQLite-om kako omogućila jednostavan pristup bazi podataka, ali i dalje koristeći snagu SQLite-a. Room obezbjeđuje compile-time verifikaciju SQL upita, jednostavne anotacije kojima se minimizuje količina ponavljajućeg boilerplate koda, te jednostavne migracije baze. Tri su osnovne komponente u Roomu: * Database class - u nju se "smješta" baza, te služi kao glavna pristupna tačka za konekciju sa podacima * Data entities - predstavljaju tabele u sklopu baze podataka * Data access object (DAO) - definišu metode koje možemo koristiti u sklopu aplikacije kako bismo pristupali podacima iz baze, to jeste, sadrži upite kojima možemo dobaviti podatke iz baze, izbrisati ih, editovati i sl. U sklopu baze čuvamo instance DAO-a vezanih za tu bazu. Na ovaj način možemo koristiti DAO objekte u ostatku aplikacije za pristup odgovarajućim podacima. ![](https://i.imgur.com/G33Axwz.png) Sljedeća komponenta arhitekture koju uvodimo jeste Repository. ***Repository*** Iako je moguće izbjeći ovaj korak, tj. omogućiti ViewModelu da direktno uzima podatke i dodjeljuje ih LiveData objektima, na ovaj način organizacija aplikacije biva otežana, te se odstupa od Separation of Concerns principa. Također, scope ViewModel-a je vezan za životni vijek aktivnosti ili fragmenta, pa bi se podaci sa web servisa gubili po završetku životnog ciklusa neke od ovih komponenti. Kako bismo ovo izbjegli, koristimo repozitorije. Repozitoriji rukuju operacijama sa podacima. Obezbjeđuju jednostavan pristup podacima ostatku aplikacije, te posjeduju informacije o tome odakle se podaci prikupljaju te pomoću kojih metoda. Njih možemo pojmiti kao medijatore između različitih izvora podataka kao što su keš, web servisi i lokalne baze. Zatim dolazimo do ViewModela. ***ViewModel*** ViewModel modul služi za čuvanje podataka koji se trebaju prikazati u fragmentu ili aktivnosti za koji je vezan. U sklopu ViewModela možemo vršiti neke jednostavnije kalkulacije i transformacije podataka kako bi ih UI kontroler prikazao. ***ViewModelFactory*** U sklopu *The One App* za više ViewModela bilo je neophodno definisati i ViewModelFactory klasu. Ova klasa zaslužna je za instanciranje ViewModel objekata, a njoj je moguće proslijediti konstruktorske parametre (jer po defaultu ViewModel klase nemaju parametre.) Naposlijetku, dolazimo do aktivnosti i fragmenata. ***Fragment*** Kao što je već rečeno, fragment predstavlja UI komponentu aplikacije, te sadrži minimum koda potrebnog za komunikaciju korisnika sa aplikacijom. ***Aktivnost*** U sklopu ovog projekta, realizovana je samo jedna glavna aktivnost - Main Activity. Ovo nam ne predstavlja iznenađenje, budući da je navigacijska komponenta dizajnirana upravo za takvu strukturu - jedan MainActivity koji nam omogućava da kroz njega vršimo pregled i navigaciju kroz ostatak fragmenata. Kako bismo zaokružili priču o arhitekturi aplikacije i konceptu **Separation of concerns**, prije nego opišemo navigaciju kroz aplikaciju, ukratko opišimo kako je gore navedena struktura implementirana u projektu. Unutar UI direktorija, u odgovarajućim poddirektorijima nalaze se odgovarajući fragmenti, njhovi ViewModeli, te po potrebi ViewModelFactories. ![](https://i.imgur.com/wjbb77Q.png) Krenimo od logic direktorija te opišimo sadržaje njegovih poddirektorija. ***adapter*** Binding adapteri su odgovorni da izvrše odgovarajuće framework pozove da postave vrijednosti. U našem slučaju, u Binding adapter klasi, pomoću anotacije @BindingAdapter i odgovarajuće sintakse omogućavamo prosljeđivanje odgovarajućih listi podataka u njen adapter (naprimjer povezivanje liste objekata tipa Movie u MovieAdapter). Implementirani su binding adapteri za sve tipove podataka koji koriste recycler view - Movie, Quote, Character, te Movie u sklopu watchliste. ***DAO*** Sljedeći direktorij je DAO te kao što je već rečeno, on sadrži interfejse u kojima su navedene metode - Queryji za pristup elementima baze. DAO koje ovaj projekat posjeduje su vezani za Movie, Movie u sklopu watchliste, te Quote. ***database*** AppDatabase je apstraktna klasa za koju navodimo entitete koje će ta baza podataka obuhvatati. Apstraktni parametri su odgovarajući (gore navedeni) DAO objekti, te u sebi sadrži companion objekat. Companion objekti u sklopu klase omogućavaju da se napiše metoda koja će se moći pozvati bez instanciranja klase. Deklarisanjem companion objekta, dakle, možemo pristupiti članovima klase samo pomoću imena klase, bez njenog eksplicitnog instanciranja. ***entity*** U sklopu entity direktorija nalaze se sve user-defined klase objekata koje se koriste u projektu. Za one koji trebaju predstavljati tabelu u sklopu baze iskorištene su odgovarajuće anotacije: * @Parcelable - kako bi se omogućilo custom objektu da ona bude proslijeđena drugoj komponenti * @Entity - da bi se označilo da se radi o entitetu * @PrimaryKey - da se označi da je atribut primarni ključ u svojoj tabeli * @ColumnInfo - da se označi da se radi o jednoj koloni tabele. ***service*** U ovoj klasi definisane su inicijalizacije Retrofit i Moshi objekata, naveden je bazni URL za pribavljanje podataka sa servisa, servis koji koristimo (sa upitima upućenim serveru), te vrijednosti po kojima vršimo filterisanje podataka. ***repository*** Na osnovu obrazloženja Repository komponente navedene na početku rada, možemo pretpostaviti da je ona potrebna za prikaz podataka koje lokalno učitavamo - tj. za objekte tipa Quote, odnosno MovieWatchList. Kada je u pitanju ui direktorij, on sadrži sve fragmente koji se prikazuju u sklopu aplikacije, te njihove popratne klase - adaptere, viewmodele, viewmodelfactory - po potrebi. Teorijski dio je sasvim dovoljno pojasnio upotrebu i svrhu ovih klasa, stoga o njima neće biti dodatnog razmatranja ***Navigacija kroz aplikaciju*** Kao što je već rečeno, ukratko ćemo opisati koncept navigacije kroz aplikaciju. Iako smo se sa ovim konceptima upoznali na prvom projektu, oni spadaju pod specifikacije i ovog, stoga ćemo ponoviti neke osnovne pojmove. Gotovo da ne postoji mobilna aplikacija u kojoj se ne zahtijeva od korisnika da vrši neki niz akcija sa ciljem izvršenja zadatka. U odnosu na navigaciju kroz aplikaciju prvog projekta - CovApp, u ovoj aplikaciji navigacija nije toliko "linearna" - centralni meni je razgranava na tematske fragmente. ![](https://i.imgur.com/BXS5B3X.png) ***Navigation component*** Androidova Navigation component sastavljena je od tri dijela, a oni su: * Navigacijski graf * Navigacijski host * Navigacijski kontroler Pomoću navigacijskog grafa opisujemo koja su to kretanja kroz aplikaciju dostupna korisniku prilikom korištenja aplikacije. Vidimo da je centralni navigacijski dio meni fragment pomoću kojeg se može pristupiti characterlistFragmentu, watchlistFragmentu, quoteListFragmentu, te movieListFragmentu. Postoji još jedan fragment u aplikaciji, a koji nije obuhvaćen u sklopu navigacijskog grafa. To je aboutAppFragment, te se njemu pristupa isključivo pomoću NavigationDrawera. Budući da NavigationDrawer nije bio jedan od zahtjeva za ovaj projekat, nećemo opisivati tačne funkcionalnosti, ali prilažemo odgovarajuću sliku: ![](https://i.imgur.com/5zzLMwp.png) Uklanjanje fragmenata sa back stacka jedna je od mogućnosti koju nam nudi implementacija navigacijske komponente. Po defaultu kada pređemo na idući fragment, prethodni se doda na back stack. Kad pritisnemo dugme back, trenutni se pop-a sa stacka, pa pristupamo prethodnom. Kao što je rečeno u prethodnom radu, ova pojava nekada može biti besmislena - pogotovo u slučaju kada imamo neke ciklički povezane fragmente. U našem slučaju takvi su fragmenti koji se odnose na watchlist i na editovanje elementa watchliste. Pri tome, radi se o fragmentu koji ima mogućnost da briše podatke, pa bi bilo besmisleno omogućiti korisniku da nakon što obriše podatak, vrati se na fragment u kojem ti podaci još uvijek postoje. ***Safe Args plugin*** Safe args plugin za navigacijsku komponentu generiše klase za prenos podataka: * Directions (za fragment odakle se podaci prenose) * Args (za fragment gdje se podaci prenose - kako bi im se moglo pristupiti) * Action (predstavlja akciju prelaska sa jednog fragmenta na drugi) Safe args plugin omogućava type-safety i jednostavnan pristup. ***View Binding i Data Binding*** Kao i u sklopu prethodnog projekta, i u ovom su korišteni View i Data binding. U sklopu prošlog projekta dominirao je View binding, dok je sada slučaj obratan - gotovo svi fragmenti imaju svoj odgovarajući data binding. Korištenje data bindinga često se kombinuje sa konceptom "živih" podataka - LiveData, o čemu će biti riječi kasnije. Podsjetimo se ukratko nekih osobina ovih tipova bindinga. Bindinzi se koriste u svrhu povezivanja layout file-ova sa UI-komponentama (fragmentima i aktivnostima). Najlošiji način pristupanja jeste pomoću id-a nekog view-a. Ovaj način je zastarjeo, te su mane što ne postoje null safety, te za svaki od view-ova iz layouta se mora vršiti zasebna pretraga. View binding i Data binding, nakon što se njihova upotreba dozvoli u sklopu build gradle file-a, automatski generišu svoje odgovarajuće binding klase. Pomoću View bindinga, nakon što napravimo instancu date klase prilikom poziva metode kojom se fragment kreira, svakom view-u iz tog layout-a možemo pristupiti na način kao da je njen atribut/metoda. Prednost ovog pristupa jeste što se brže kompajlira u odnosu na Data binding. U ovom projektu Data binding, unatoč tome što je za njega bilo potrebno uvoditi neke postavke (layout tagove u sklopu layout file-a, postavljanje varijable, itd.), bio je pogodniji za korištenje, budući da je korištenjem Data bindinga moguće pratiti promjene (observe), te vršiti direktne izmjene prikazanog sadržaja (npr. pomoću Binding adaptera). ***Recycler view*** Možemo slobodno reći da veći dio aplikacije sadrži i koristi recycler view. Recycler view je kontejner za dinamičke liste, te on omogućava efikasan prikaz velikog broja podataka. Za proslijeđene podatke i definisani izgled svakog od itema, RecyclerView biblioteka dinamički kreira elemente kada zatrebaju. Kao što i samo ime kaže, RecyclerView reciklira pojedine elemente. Prilikom skrolanja, RecyclerView ne uništava view. Umjesto toga, on ga iskoristi za prikaz novih ekrana, do kojih se došlo skrolanjem. Na ovaj način se poboljšava responsiveness i smanjuje potrošnja energije. ![](https://i.imgur.com/yjPD5Nl.png) Implementacija RecyclerView-a je poprilično šablonska, te je potrebno izvršiti sljedeće korake. Prvo se odredi kako će cjelokupna lista izgledati - to jeste, da li će u pitanju biti grid, prikaz u vidu liste, i sl. Ovo se radi pomoću LayoutManagera. Zatim se dizajnira izgled i ponašanje pojedinog objekta. Na osnovu toga, extenda se ViewHolder klasa. ViewHolder klasa nam obezbjeđuje sve funkcionalnosti za elemente liste - on je wrapper za view, a RecyclerView se pobrine za taj view. Nakon toga, implementira se Adapter - on povezuje podatke sa view-ovima iz ViewHoldera. ViewHolder i Adapter zajedno definišu prikaz podataka. Adapter kreira ViewHolder objekte kada je to potrebno, te kreira podatke za te view-ove. Proces povezivanja view-ova sa njihovim podacima naziva se binding. Sam kod adaptera je poprilično "boilerplate", stoga ćemo samo navesti tri ključne metode koje je potrebno overrideati: * onCreateViewHolder() * onBindViewHolder() * getItemCount() ***LiveData*** Kao što je već rečeno, jedna bitna komponenta ovog projekta jesu "živi" - Live podaci. LiveData predstavlja Observable data holder klasu. Za razliku od običnog observable objekta, LiveData je lifecycle-aware, što znači da poštuje životni ciklus drugih komponenti aplikacije kao što su aktivnosti, fragmenti ili servisi. Ova svijest obezbjeđuje da se pomoću LiveData updateuju samo observeri koji su u aktivnom lifecycle stanju. Prednosti koje dobijamo korištenjem LiveData su: * Obezbjeđivanje da UI odgovara stanju podataka * Nema curenja memorije (observeri se veži za Lifecycle objekte i očiste zauzetu memoriju kad se uništi lifecycle za koji su vezani) * Nema crasheva zbog zaustavljenih aktivnosti (ako je lifecycle observera neaktivan, npr. aktivnost na back stacku, tada ne prima nikakve LiveData evente) * Ne trebamo ručno handleati lifecycle * Podaci su uvijek up-to-date (ako lifecycle postane neaktivan, dobit će najnovije podatke kada ponovo postane aktivan) Ukoliko želimo raditi sa LiveData objektima, prvo kreiramo instancu LiveData da čuva određeni tip podataka. Ovo se najčešće radi u sklopu ViewModel klase. Zatim se kreira Observer objekat koji definiše onChanged() metodu, koja kontroliše šta se desi kada se podaci koje čuva LiveData promijene. Observer objekat se obični kreira u sklopu UI kontrolera - aktivnosti ili fragmenta. Observer objekat se zakači za LiveData objekat pomoću observe() metode. Ona prima LifecycleOwner objekat, čime se Observer objekat veže za LiveData kako bi bio obaviješten o promjenama. U slučaju naše aplikacije, Observere smo koristili na sljedeći način: ``` viewModel.navigateToSelectedProperty.observe(viewLifecycleOwner, Observer { if ( null != it ) { this.findNavController().navigate( MovieListFragmentDirections.actionNavGalleryToMovieDetailFragment(it)) viewModel.displayMovieDetailsComplete() } }) ``` Dakle, u sklopu fragment klase definisali smo observer nad odgovarajućim view modelom koji će pratiti (observe) neke promjene stanja. U ovom slučaju, promjena stanja dešava se na klik, te je indikator da je potrebno izvršiti navigaciju sa prikaza liste na prikaz pojedinog elementa. ***Zaključak*** U poređenju sa aplikacijom rađenom u sklopu prvog projekta, vidimo da se mogućnosti i obim aplikacije znatno povećava uvođenjem standardnih koncepata kao što su lokalno čuvanje podataka, te njihovo dobavljanje sa mreže. Uvođenjem svih komponenti *Separation of concerns* pristupa imamo priliku po prvi put jedanput konstruisati aplikaciju držeći se strogo pravila njene arhitekture. Ova aplikacija je i dobar pokazatelj kako čak i fakultetski projekat može u velikoj mjeri ličiti, te imati funkcionalnosti aplikacija koje su u stvarnoj upotrebi.