napredne tehnike i alati za izradu aplikacija za ... · mvvm (model-view-viewmodel) obrasci....
TRANSCRIPT
SVEUČILIŠTE U ZAGREBU
FAKULTET ELEKTROTEHNIKE I RAČUNARSTVA
DIPLOMSKI RAD br. 1978
Napredne tehnike i alati za izradu aplikacija za
operacijski sustav Android
Sara Brozović
Zagreb, lipanj 2019.
Sadržaj
Uvod ...................................................................................................................................... 1
1. Gradle – sustav za automatizaciju izgradnje Android aplikacija .................................. 2
1.1. Groovy Gradle ....................................................................................................... 3
1.2. Kotlin-DSL ............................................................................................................ 8
1.2.1. Tip-sigurni pristupnici ................................................................................... 8
1.3. Razlike između Groovya i Kotlin-DSLa ............................................................. 11
1.3.1. Naziv Gradle skripti..................................................................................... 11
1.3.2. Osnovna sintaksa ......................................................................................... 11
1.3.3. Primjenjivanje dodataka .............................................................................. 12
1.3.4. Kreiranje zadataka ....................................................................................... 13
1.3.5. Deklaracije ovisnosti ................................................................................... 14
2. Arhitektura u Android projektima ............................................................................... 16
2.1. Model-View-Presenter obrazac ........................................................................... 17
2.2. Model-View-ViewModel obrazac ....................................................................... 22
3. Komunikacija Android aplikacije s poslužiteljem korištenjem Retrofit, okHttp i RxJava
biblioteka ............................................................................................................................. 24
4. Kriptiranje podataka unutar Android aplikacije .......................................................... 26
5. The Great Guide Android aplikacija ........................................................................... 28
5.1. Dijelovi koda iz The Great Guide aplikacije ....................................................... 30
5.1.1. Primjer koda za MVVM obrazac................................................................. 30
5.1.2. Primjer koda za injekciju ovisnosti ............................................................. 34
5.1.3. Primjer koda za komunikaciju s poslužiteljem ............................................ 35
5.1.4. Primjer koda za kriptiranje podataka koji se spremaju na Android uređaj . 40
Zaključak ............................................................................................................................. 52
Literatura ............................................................................................................................. 53
Sažetak ................................................................................................................................. 54
Summary .............................................................................................................................. 55
Privitak ................................................................................................................................ 56
1
Uvod
Razvoj Android aplikacija u današnje vrijeme postaje sve popularniji odabir za područje
kojim će se programeri baviti. Većina to odabere jer smatra da je vrlo lagano napraviti
Android aplikaciju, te da neće imati puno muke oko programiranja. Može se reći da je to u
jednu ruku točno. Nije toliko teško započeti s izradom Android aplikacija i u kratkom roku
se s minimalno znanja može napraviti osnovna aplikacija. Međutim, ako želimo napraviti
kvalitetnu Android aplikaciju, koju će tim ljudi godinama održavati ili ako želimo napraviti
aplikaciju s više od tri ekrana koja ima u pozadini dodatne funkcije koje ne dolaze
standardno, onda to nije tako jednostavan proces.
Svaka kvalitetna Android aplikacija mora imati neku vrstu arhitekture, što ponajprije
uključuje arhitektonske obrasce. Danas se najviše koriste MVP (Model-View-Presenter) i
MVVM (Model-View-ViewModel) obrasci. Aplikacija bi također trebala imati neku vrstu
injekcije ovisnosti kako bi se lakše slale ovisnosti drugim objektima u aplikaciji. Najbolji
primjer za to je radni okvir Dagger.
Svaka aplikacija vrlo vjerojatno mora prikazati sadržaj koji se ne čuva u samoj aplikaciji već
se dohvaća s poslužitelja. Komunikaciju s poslužiteljem moguće je ostvariti pomoću
klijenata kao što su okHttp i Retrofit. A upravljanje s odgovorima od poslužitelja je možda
najbolje napraviti korištenjem biblioteke RXJava. Korištenjem RXJave postiže se vrlo čist i
pregledam kod.
Ako aplikacija sadrži osjetljive podatke koje sprema na uređaj ili ih sadrži u kodu, potrebno
ih je na neki način zaštititi. Zaštitu je moguće ostvariti kriptiranjem. Najčešće se za tu svrhu
koriste SHA, RSA ili AES algoritmi.
2
1. Gradle – sustav za automatizaciju izgradnje
Android aplikacija
Gradle je sustav za automatizaciju izgradnje otvorenog koda koji se temelji na konceptima
Apache Ant i Apache Maven i uvodi jezik temeljen na Groovyu umjesto XML obrasca koji
koristi Apache Maven za deklariranje konfiguracije projekta. Gradle koristi usmjereni
aciklički graf ("DAG") kako bi odredio redoslijed kojim se zadaci mogu izvoditi [1].
Gradle je dizajniran za izgradnju više projekata, koji mogu postati prilično veliki. Podržava
inkrementalne gradnje inteligentnim određivanjem koji su dijelovi stabla izgradnje ažurni;
nijedan zadatak ovisan samo o tim dijelovima ne mora biti ponovno izvršen. Također je
dizajniran tako da bude dovoljno fleksibilan da izgradi gotovo bilo koju vrstu softvera.
Jedna od glavnih karakteristika Gradlea su visoke performanse. Gradle izbjegava nepotreban
rad tako što pokreće samo one zadatke koje je potrebno pokrenuti jer su se njihovi ulazi ili
izlazi promijenili [1].
Druga karakteristika je ta da je temelj Gradlea JVM (Java Virtual Machine). Gradle radi na
JVM-u i potrebno je imati instaliran JDK (Java Development Kit) kako bi ga se koristilo.
Također olakšava pokretanje Gradlea na različitim platformama [1].
Treća karakteristika su konvencije. Gradle čini zajedničke grupe projekta, kao što su Java
projekti, jednostavnim za izgradnju primjenom konvencija. Primjenom odgovarajućih
dodataka (engl. plugins) moguće je završiti s kratkim skriptama za izradu mnogih projekata.
No, ove konvencije ne ograničavaju. Gradle omogućava njihovo poništavanje ili omogućava
dodavanje novih zadataka, te izgradnju mnogih drugih prilagodbi za gradnju temeljene na
konvencijama [1].
Četvrta karakteristika je rastezljivost (engl. extensibility). Moguće je jednostavno proširiti
Gradle kako bi osigurali nove tipove zadataka ili čak izgradili model. Na primjer, za
izgradnju Androida (engl. Android build) dodaju se mnogi novi koncepti izrade, kao što su
okusi (engl. flavors) i vrste izrade (engl. build types) [1].
Peta karakteristika je IDE podrška. Nekoliko glavnih IDE-a omogućuju uvoz Gradle gradnje
(engl. Gradle build) i stupanje u interakciju s njima: Android Studio, IntelliJ IDEA, Eclipse
i NetBeans. Gradle također ima podršku za generiranje datoteka rješenja potrebnih za
učitavanje projekta u Visual Studio [1].
3
Šesta karakteristika je uvid (engl. insight). Skeniranje gradnje (engl. build scans) pruža
opsežne informacije o izvođenju izgradnje koje je moguće koristiti za identificiranje
problema pri izradi. Oni su osobito dobri u tome da pomognu identificirati probleme s
izvedbom gradnje [1].
1.1. Groovy Gradle
Groovy Gradle se u projekte ubacuje pomoću Groovy dodatka (engl. Groovy plugin). On
proširuje dodatak za Javu kako bi dodao podršku za Groovy projekte. Dodatak može raditi
s Groovy kodom, mješovitim Groovy i Java kodom, pa čak i čistim Java kodom. Dodatak
podržava zajedničku kompilaciju, koja omogućuje slobodno miješanje i usklađivanje
Groovy i Java koda, s ovisnostima u oba smjera. Na primjer, klasa Groovy može proširiti
Java klasu koja zauzvrat proširuje klasu Groovy. Time je omogućeno korištenje najboljeg
jezika za posao i, ako je potrebno, prepisati bilo koji razred u drugom jeziku.
Groovy Gradle se u Android projektima nalazi u obliku *.gradle datoteka. U njih se dodaju
ovisnosti (engl. dependencies), ciljne i minimalne API razine, potpisne konfiguracije (engl.
signing configs) i vrste izgradnje (engl. build types).
Groovy datoteke su zapravo Groovy skripte, a sintaksa je slična Javinoj sintaksi. Jedna od
prednosti u Groovy sintaksi je ta što nema potrebe za zagradama kada se pozivaju metode s
barem jednim parametrom (ako su jednoznačne) [2]:
def printAge(String name, int age) {
print("$name is $age years old")
}
def printEmptyLine() {
println()
}
def callClosure(Closure closure) {
closure()
}
printAge "John", 24 // ispisat će "John is 24 years old"
printEmptyLine() // ispisat će praznu liniju
callClosure { println("From closure") } // ispisat će "From
closure"
4
Također, ako je zadnji parametar closure (predstavlja lambda poziv), može se napisati
izvan zagrada:
def callWithParam(String param, Closure<String> closure) {
closure(param)
}
callWithParam("param", { println it }) // ispisat će "param"
callWithParam("param") { println it } // ispisat će "param"
callWithParam "param", { println it } // ispisat će "param"
Također, ako pozovemo Groovy metodu s metodom s imenovanim parametrima (engl.
named parameters), oni se pretvaraju u mapu i prosljeđuju kao prvi argument metode. Ostali
(neimenovani argumenti) se zatim dodaju popisu parametara [2]:
def printPersonInfo(Map<String, Object> person) {
println("${person.name} is ${person.age} years old")
}
def printJobInfo(
Map<String, Object> job,
String employeeName
) {
println("$employeeName works as ${job.title})
}
printPersonInfo name: "John", age: 24
printJobInfo "John", title: "Android developer"
Ovo će ispisati „John is 24 years old“ nakon čega će se ispisati „John works as Android
developer.“ U oba slučaja će rezultat biti isti, bez obzira na redoslijed parametra.
Jedan od važnih značajki (engl. feature) su zatvarači (engl. closures). Zatvarači u Groovyu
su kao lambde na steroidima. Oni su blokovi koda koji se mogu izvršiti, imati parametre i
povratne vrijednosti. Ono što je drugačije je to da je moguće promijeniti delegata zatvaranja
(engl. delegate of a closure). U nastavku je primjer koda:
class WriterOne {
def printText(str) {
println "Printed in One: $str"
}
}
5
class WriterTwo {
def printText(str) {
println "Printed in Two: $str"
}
}
def printClosure = {
printText "I come from a closure"
}
printClosure.delegate = new WriterOne()
printClosure() // ispisat će "Printed in One: I come from a
//closure"
printClosure.delegate = new WriterTwo()
printClosure() // ispisat će "Printed in Two: I come from a
// closure"
Vidljivo je da printClosure poziva metodu printText od delegata koji mu je pružen
(isto vrijedi i za svojstva).
Postoje tri glavne datoteke koje Gradle koristi. Svaki od njih je blok koda koji se izvršava
naspram različitih objekata (engl. execute against various objects).
Build skripte u build.gradle datotekama se izvršavaju naspram Project objekata (ovo sučelje
je glavni API koji se koristi za interakciju s Gradleom iz datoteke izgradnje. Iz Projekta
postoji programski pristup svim značajkama Gradlea), settings skripte u settings.gradle
datotekama se izvršavaju naspram objekta Settings (deklarira konfiguraciju potrebnu za
instanciranje i konfiguriranje hijerarhije instanci Projecta koji će sudjelovati u izgradnji), a
init skripte se koriste za globalnu konfiguraciju (izvršavaju se naspram Gradle instanci).
Gradle build se sastoji od jednog ili više projekata, a projekti se sastoje od zadataka. Uvijek
postoji barem korijenski projekt (engl. root project), koji može sadržavati potprojekte (engl.
subprojects), koji također mogu imati ugniježđene potprojekte. Uobičajena konvencija je da
je uloga korijenskog projekta samo orkestrirati grupne projekte, pružiti uobičajenu
konfiguraciju, puteve klasa itd.
Tipični Android projekt ima sljedeću strukturu:
6
Slika 1 Struktura Android projekta [2]
Settings.gradle (#1) datoteka je datoteka postavki za korijenski projekt, koja se izvršava
naspram Settings instance. Build.gradle (#2) datoteka je konfiguracija izgradnje korijenskog
projekta, gradle.properties (#3) je datoteka postavki aplikacijskog projekta (engl. app
project), dodane u aplikacijski Settings. Unutarnji build.gradle (#4) je konfiguracija
izgradnje aplikacijskog projekta.
U nastavku je prikazan kod iz settings.gradle datoteke:
include ‘:app’
Settings.gradle deklarira konfiguraciju potrebnu za instanciranje i konfiguriranje hijerarhije
instanci projekta koje će sudjelovati u gradnji.
Prije nego što se definira korijenski build.gradle, potrebno je definirati unutarnju
build.gradle jer on definira što sve treba ići u korijenski build.gradle.
Najprije je potrebno definirati koje dodatke (engl. plugins) treba dodati u projekt. Dodaci se
dodaju pomoću apply metode koja ima tri definicije:
void apply(Closure closure)
void apply(Map<String, ?> options)
void apply(Action<? super ObjectConfigurationAction> action)
Treća definicija je najvažnija jer koristi statički API, ali najčešće se koristi druga definicija
pomoću koje je imenovane parametre moguće proslijedi metodi kao mapu. U službenoj
dokumentaciji Gradlea postoji nekoliko imenovanih parametara koje je moguće koristiti.
Prvi od njih je from koja definira skriptu za primjenu, zatim plugin koji definira ID ili
klasu implementacije dodatka za primjenu, te zadnji to koji definira ciljni objekt ili objekte
delegata. Ako treba, na primjer, dodati com.android.application dodatak, koristit
ćemo plugin parametar.
apply(plugin: 'com.android.application')
7
Ranije je navedeno da je moguće izostaviti zagrade ako je poziv nejasan pa će konačni poziv
izgledati ovako:
apply plugin: 'com.android.application'
Kada bi se ovdje stalo i pokrenuo projekt došla bi poruku o grešci koja kaže da Gradle ne
može pronaći datoteku s id-jem 'com.android.application'. Razlog greške je taj da Gradle ne
može samostalno naći Android .jar datoteku, te je potrebno navesti put do datoteke. Ovaj
put je najbolje konfigurirati u korijenskoj build.gradle datoteci:
buildscript {
repositories {
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:2.3.0-
beta2'
}
}
Kod iznad buildscript(Closure) se poziva nad Project instancom, a dani closure
je izveden uz ScriptHandler objekt. repositories(Closure) se poziva nad
ScriptHandler instancom, dok se dani closure izvodi uz RepositoryHandler objekt.
dependencies(Closure) se također izvodi nad ScriptHandler instancom, ali njegov
argument se izvodi uz DependecyHandler objekt. Što znači da se jcenter() poziva unutar
RepositoryHandlera, a classpath(String) se poziva unutar DependecyHandlera.
Nakon što su dodani dodaci u projekt potrebno je dodati konfiguraciju povezanu s
Androidom:
android {
buildToolsVersion "28.0.3"
compileSdkVersion 28
}
Vidljivo je da je Project instanci dodana android metoda koja delegira zatvaranje (engl.
closure) nekom objektu (u ovom slučaju to je AppExtension objekt), te sadrži
buildToolsVersion i compileSdkVersion metode. Na ovaj način dodatak za
Android dobiva svu potrebnu konfiguraciju, uključujući zadanu konfiguraciju, okuse (engl.
flavors) itd.
8
Nakon što je dodana konfiguracija vezana uz Android potrebno je dodati ovisnosti:
dependencies {
implementation 'io.reactivex.rxjava2:rxjava:2.0.4'
testImplementation 'junit:junit:4.12'
}
Iako dependecies metoda delegira zatvaranje DependencyHandleru, on ne sadrži
implementation, testImplementation niti ostale metode koje se inače koriste.
1.2. Kotlin-DSL
Prilikom izrade DSL-a zasnovanog na bilo kojem jeziku opće namjene ne govorimo o
stvaranju potpuno novog programskog jezika s potpuno novom sintaksom. Umjesto toga,
postavlja se poseban način korištenja tog jezika. Tako je sada moguće koristiti sredstva
Kotlin jezika za pisanje Gradle skripti i dodataka umjesto Groovy jezika.
Gradleov Kotlin DSL pruža alternativnu sintaksu tradicionalnom Groovyu s poboljšanim
iskustvom uređivanja u podržanim IDE-ovima, kao Android Studiu, uz vrhunsku asistenciju
sadržaja, refactoringa, dokumentaciju i još mnogo toga [3].
Baš kao i Groovy, Kotlin DSL je implementiran na Gradleovom Java API-ju. Sve što se
može pročitati u Kotlin DSL skripti je Kotlin kod sastavljen i izvršen od strane Gradlea.
Mnogi objekti, funkcije i svojstva koja se koriste u skriptama za gradnju dolaze iz Gradle
API-ja i API-ja primijenjenih dodataka.
Sve Kotlin DSL skripte za izgradnju imaju implicitni uvoz koji se sastoji od zadanih Gradle
API-ja uvoza (engl. import) i Kotlin DSL APIa, kojem pripada sve unutar paketa
org.gradle.kotlin.dsl i org.gradle.kotlin.dsl.plugins.dsl. Preporučeno je izbjegavati unutarnje
Kotlin DSL APIje, jer korištenje unutarnjeg Kotlin DSL API-ja u dodacima i skriptama za
izgradnju, ima potencijal razbiti gradnje kada se promijeni ili Gradle ili dodaci [4].
1.2.1. Tip-sigurni pristupnici
Kotlin DSL trenutno podržava modele tip-sigurnih pristupnika (engl. type-safe model
accessors) za bilo koju od sljedećih vrsta koje su podržane dodacima: konfiguracije ovisnosti
i artefakata (kao što je implementation i runtimeOnly koje je pridonio Java dodatak),
proširenja i konvencije projekta (kao što je sourceSets), elementi u tasks i configurations
kontejnerima i elementi u kontejnerima proširenja projekta [4].
9
Skup dostupnih modela tip-sigurnih pristupnika izračunava se neposredno prije procjene
tijela skripte, odmah nakon bloka plugins{}. Bilo koji elementi modela koji su prisutni nakon
te točke ne rade s dodacima modela tip-sigurnih pristupnika. Na primjer, to uključuje sve
konfiguracije koje se mogu definirati u vlastitoj skripti izrade. Međutim, ovaj pristup znači
da je moguće koristiti tip-sigurne pristupnike za sve elemente modela koje pridonose dodaci
koje su primijenili (engl. applied) nadređeni projekti [4].
U nastavku se nalazi skripta izrade projekta koja pokazuje kako se može pristupiti različitim
konfiguracijama, proširenjima i drugim elementima pomoću tip-sigurnih pristupnika.
plugins {
`java-library`
}
dependencies { // (1)
api("junit:junit:4.12")
implementation("junit:junit:4.12")
testImplementation("junit:junit:4.12")
}
configurations { // (1)
implementation {
resolutionStrategy.failOnVersionConflict()
}
}
sourceSets { // (2)
main { // (3)
java.srcDir("src/core/java")
}
}
java { // (4)
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
tasks {
test { // (5)
testLogging.showExceptions = true
}
10
}
U kodu označenom s (1) vidljivo je da se tip-sigurni pristupnici kogu koristiti za api,
implementation i testImplementation konfiguracije ovisnosti. Pod (2) je
vidljivo da se pristupnik može koristiti za konfiguriranje sourceSets ekstenzije projekta,
a pod (3) se može koristiti za konfiguraciju main skupa izvora. Pod (4) se koristi za
konfiguriranje java izvora za main skup izvora, a pod (5) za konfiguraciju test zadatka.
Tip-sigurni pristupnici nisu dostupni za elemente modela koji su dostupni kroz dodatke
primijenjene pomoću apply(plugin = "id")metode, skripte za izgradnju projekta,
skriptnih dodatka kroz apply(from = "script-plugin.gradle.kts"), te
dodataka primijenjenih pomoću cross-project konfiguracije.
Ako nije moguće naći tip-sigurni pristupnik, onda je potrebno vratiti se na korištenje
normalnog APIja za odgovarajuće tipove. Kako bi se to postiglo, potrebno je znati imena
i/ili tipove konfiguriranih elemenata modela [4].
Sljedeći primjer pokazuje kako se referenciraju i konfiguriraju konfiguracije artefakata bez
tip-sigurnih pristupnika.
apply(plugin = "java-library")
dependencies {
"api"("junit:junit:4.12")
"implementation"("junit:junit:4.12")
"testImplementation"("junit:junit:4.12")
}
configurations {
"implementation" {
resolutionStrategy.failOnVersionConflict()
}
}
Kod izgleda slično kao kod tip-sigurnih pristupnika, osim što su imena konfiguracije u ovom
slučaju string literali. Za imena konfiguracije u deklaracijama ovisnosti i unutar bloka
configurations {} moguće je koristiti string literale.
11
1.3. Razlike između Groovya i Kotlin-DSLa
1.3.1. Naziv Gradle skripti
Jedna od najbitnijih razlika između Groovya i Kotlin DLS-a je imenovanje gradle skripti.
Groovy skripte koriste .gradle ekstenziju, dok Kotlin DSL koristi .gradle.kts ekstenziju.
Kako bi se aktivirao Kotlin DSL, jednostavno treba upotrijebiti nastavak .gradle.kts za
skripte za gradnju umjesto .gradle. To vrijedi i za datoteku s postavkama (potrebno je
promijeniti u settings.gradle.kts) i za inicijalizacijske skripte [5].
1.3.2. Osnovna sintaksa
Groovy stringovi se mogu pisati s jednostrukim 'string' i dvostrukim "string" navodnicima,
dok Kotlin traži da se piše samo s dvostrukim navodnicima "string".
Groovy omogućuje izostavljanje zagrada kod poziva funkcija, dok Kotlin uvijek zahtijeva
zagrade. Također, Groovy omogućuje izostavljanje operatora dodjeljivanja = prilikom
dodjeljivanja svojstava, dok Kotlin uvijek zahtijeva operator dodjele [5].
Kodovi u nastavku su primjer navedenih pravila.
Groovy:
group 'com.acme' (1)(2)
dependencies {
implementation "com.acme:example:1.0" (1)(3)
}
Kotlin DSL:
group = "com.acme" (1)(2)
dependencies {
implementation("com.acme:example:1.0") (1)(3)
}
Pod (1) su vidljiva pravila navodnika, gdje Groovy može imati obje vrste navodnika dok
Kotlin može imati samo dvostruke navodnike. Pod (2) je vidljivo pravilo operatora dodjele,
a pod (3) poziv funkcija.
12
1.3.3. Primjenjivanje dodataka
Postoje dva načina kako primijeniti Gradle dodatke. Prvi je deklarativno, korištenjem
plugins{} bloka, a drugi je imperativno, korištenjem apply (..) funkcija.
1.3.3.1. Primjenjivanje korištenjem plugins bloka
Plugins DSL daju sažet i praktičan način za deklariranje ovisnosti o dodatku. Radi s Gradle
plugin portalom kako bi se osigurao jednostavan pristup jezgrenim dodacima (engl. core
plugins) i dodatcima zajednice (engl. community plugins) [6].
U nastavku je prikazano korištenje Plugins DSLa odnosno plugins bloka pomoću Groovya i
Kotlin DSLa.
Jezgreni dodaci:
Groovy:
plugins {
id 'java'
}
Kotlin DSL:
plugins {
java
}
Dodaci zajednice:
Groovy:
plugins {
id 'com.jfrog.bintray' version '0.4.1'
}
Kotlin DSL:
plugins {
id("com.jfrog.bintray") version "0.4.1"
}
13
1.3.3.2 Primjenjivanje korištenjem apply funkcije
S uvođenjem plugins DSLa, korisnici bi trebali imati malo razloga za korištenjem apply
funkcije. Potrebno ju je koristiti u slučaju da autor ne može koristiti plugins DSL zbog
ograničenja u načinu na koji build trenutno radi [6].
U nastavku je primjer korištenja apply funkcije za Groovy i Kotlin DSL.
Groovy:
apply plugin: 'java'
apply plugin: 'jacoco'
apply plugin: 'org.springframework.boot'
Kotlin DSL:
apply(plugin = "java")
apply(plugin = "jacoco")
apply(plugin = "org.springframework.boot")
Gradle preporučuje korištenje plugins{} bloka umjesto apply funkcije, pogotovo u Kotlin
DSLu. plugins{} blok omogućuje Kotlin DSLu da pruža tip-sigurne pristupnike za
proširenja, konfiguracije i druge značajke koje pridonose primijenjeni dodaci, što IDE-ima
olakšava otkrivanje detalja modela dodataka i olakšava njihovo konfiguriranje.
1.3.4. Kreiranje zadataka
Kreiranje zadataka može se izvršiti pomoću top-level funkcije task (…).
Groovy:
task greeting {
doLast { println 'Hello, World!' }
}
Kotlin DSL:
task("greeting") {
doLast { println("Hello, World!") }
}
Registriranje ili kreiranje zadataka također se može obaviti u tasks kontejneru, odnosno
pomoću register (…) i create (…) funkcija.
Groovy:
tasks.register('greeting') {
14
doLast { println("Hello, World!") }
}
tasks.create('greeting') {
doLast { println("Hello, World!") }
}
Kotlin DSL:
tasks.register("greeting") {
doLast { println("Hello, World!") }
}
tasks.create("greeting") {
doLast { println("Hello, World!") }
}
Gornji primjeri stvaraju zadatke bez konkretnog tipa. Ako je potrebno stvoriti zadatak
određenog tipa, to je također moguće napraviti pomoću register (…) i create (…) funkcija.
U nastavku je primjer zadataka tipa Zip.
Groovy:
tasks.register('docZip', Zip) {
archiveName = 'doc.zip'
from 'doc'
}
tasks.create(name: 'docZip', type: Zip) {
archiveName = 'doc.zip'
from 'doc'
}
Kotlin DSL:
tasks.register<Zip>("docZip") {
archiveName = "doc.zip"
from("doc")
}
tasks.create<Zip>("docZip") {
archiveName = "doc.zip"
from("doc")
}
1.3.5. Deklaracije ovisnosti
Deklariranje ovisnosti je vrlo slično u Groovyu i Kotlin DSLu.
15
Groovy:
plugins {
id 'java'
}
dependencies {
implementation 'com.example:lib:1.1'
runtimeOnly 'com.example:runtime:1.0'
testImplementation('com.example:test-support:1.3') {
exclude(module: 'junit')
}
testRuntimeOnly 'com.example:test-junit-jupiter-
runtime:1.3'
}
Kotlin DSL:
plugins {
java
}
dependencies {
implementation("com.example:lib:1.1")
runtimeOnly("com.example:runtime:1.0")
testImplementation("com.example:test-support:1.3") {
exclude(module = "junit")
}
testRuntimeOnly("com.example:test-junit-jupiter-
runtime:1.3")
}
16
2. Arhitektura u Android projektima
Jasno definirana arhitektura je vrlo važna, pogotovo u projektima u kojima se tim sastoji od
nekoliko ili više programera, puno se stvari mijenja, nove značajke se razvijaju. Potrebno je
zadržati skalabilnost, red u kodu i na kraju osigurati dobru kvalitetu proizvoda.
Jednostavno rečeno, uloga arhitekture je da svi programeri u timu znaju kako bi trebali
razvijati aplikaciju. Zamislimo situaciju kada malo programera radi na istom projektu, svaki
od njih ima drugačiji način implementacije koda, koristi različite konvencije imenovanja,
različite obrasce i tako dalje. Vjerojatno na početku neće biti jako bolno, ali kako projekt
raste, tako raste i nered i nedosljednost u njemu, sve do trenutka kada više ništa nije moguće
promijeniti bez posljedica. Kako bi se to izbjeglo svaki projekt bi trebao imati skup pravila
razvoja. Važno je da sve značajke u aplikaciji budu napisane na dosljedan način [7].
Arhitektura je skup pravila koja se odnose na određeni projekt. Ta pravila proizlaze iz
sporazuma između programera i definiraju kako razviti i održavati aplikaciju.
Postoje mnogi aspekti koji bi trebali biti pokriveni arhitekturom, a jedan od najvažnijih
aspekata je arhitektonski obrazac (engl. architectural pattern). Odabir pravog obrasca koji
povezuje korisničko sučelje s poslovnom logikom i modelom podataka ključni je dio
arhitekture. To će u osnovi utjecati na izgled koda. Treba izbjegavati miješanje različitih
obrazaca unutar jednog projekta, odnosno treba se odlučiti za jedan i njega koristiti na
svakom zaslonu/značajki [7].
Neki od obrazaca su MVP (Model-View-Presenter) i MVVM (Model-View-ViewModel),
koji će kasnije biti detaljno obrađeni.
Jedan od važnih aspekata je i injekcija ovisnosti (engl. dependency injection skraćenica DI).
Izbor i postavljanje DI okvira bi trebali biti u okviru definicije arhitekture. Postoji nekoliko
biblioteka koje nude spremna rješenja, kao na primjer Dagger. S injekcijom ovisnosti nije
potrebno toliko brinuti o stvarima kao što su npr. stvaranje objekta, predaja referenci i
implementacija jednostrukog obrasca (engl. singleton pattern) [7].
U nastavku je primjer koda bez DI-a i s DI-jem.
class CitiesActivity {
private lateinit var viewModel: CitiesViewModel
constructor(viewModel: CitiesViewModel) {
this.viewModel = viewModel;
17
}
}
class CitiesActivity {
@Inject lateinit var viewModel: CitiesViewModel
}
@Inject anotacija se koristi za injekciju objekta.
Modularizaciju bi također trebalo uzeti u obzir prilikom definiranja arhitekture projekta.
Razdvajanje projekta u nekoliko modula trebalo bi donijeti dobit. Moguće je imati apstraktne
slojeve koji se fizički nalaze u različitim modulima, svaki modul ima svoju konfiguraciju
izgradnje, te je tako moguće prilagoditi ovisnosti za svaku od njih. Na primjer, UI modul ne
mora vidjeti database ili network module [7].
Slika 2 Prikaz monolitnog i modulariziranog projekta [7]
Iz slike 2 je vidljivo da su u monolitnoj arhitekturi sve značajke unutar jedne cjeline, te
promjena jedne značajke može rezultirati lančanom reakcijom koja će dovesti do primjene
ostalih značajki u projektu. Kod modularizirane arhitekture vidimo da je svaka značajka
cjelina za sebe, pa samim time promjena kod jedne značajke neće imati nikakav utjecaj na
ostale značajke.
2.1. Model-View-Presenter obrazac
Model-View-Presenter je jedan od najčešće korištenih arhitektonskih obrazaca u Android
projektima. Jednostavan je za razumjeti i implementirati.
18
Slika 3 Model-View-Presenter
Ovaj arhitektonski obrazac se sastoji od 3 dijela: Modela, Presentera i Viewa.
Model je sučelje odgovorno za upravljanje podacima. Odgovornosti Modela uključuju
upotrebu API-ja, spremanje podataka u predmemoriju, upravljanje bazama podataka itd.
Model također može biti sučelje koje komunicira s drugim modulima zaduženim za te
odgovornosti. Na primjer, ako koristite obrazac spremišta (engl. repository pattern), Model
bi mogao biti Repository. Ako koristite čistu arhitekturu (engl. clean architecture), umjesto
toga, Model može biti Interactor. Interactor se sastoji od klasa koje bi trebale stupiti u
interakciju s podacima. Podatke je moguće dobiti ili iz API-ja ili iz baze podataka ili iz
lokalnog App Cache-a, a ideja je da će Presenter pozvati metodu iz Interactora npr. metodu
pod nazivom getData() i dobiti podatke iz odgovora (engl. response) [8].
Presenter je posrednik između Modela i Viewa. Sva prezentacijska logika pripada njemu.
Presenter je odgovoran za upit modela i ažuriranje prikaza, reagirajući na korisničke
interakcije koje ažuriraju model.
View je odgovoran samo za prikazivanje podataka na način koji odluči Presenter. View se
može implementirati putem aktivnosti (engl. Activities), fragmenata (engl. Fragments), bilo
kojeg Android widgeta ili bilo čega što može obavljati operacije kao što je prikazivanje
ProgressBara, ažuriranje TextView-a, popunjavanje RecyclerView-a i tako dalje [8].
Jedan od najvećih problema Androida je što Viewove (aktivnosti, fragmenti,…) nije lako
napraviti jedinično testiranje (engl. unit testing) zbog složenosti okvira (engl. framework).
Kako bi se riješio ovaj problem, trebate implementirati obrazac pasivnog prikaza (engl.
Passive View pattern). Implementacija ovog obrasca smanjuje ponašanje Viewa na apsolutni
minimum pomoću kontrolera, u ovom slučaju, Presentera. Ovakva implementacija
dramatično poboljšava mogućnost testiranja [8].
Kako bi prethodni princip bio stvarno učinkovit (poboljšanje testiranja), Presenter ne smije
ovisi o klasama Androida. Presenter treba napisati samo pomoću Java ovisnosti, jer se time
Presenter izdvaja od implementacijskih detalja, odnosno Android okvira. U Presenteru se,
19
zbog toga, ne smije koristiti kontekst. U slučaju da je potreban kontekst za pristup
zajedničkim postavkama (engl. shared preferences) ili resursima, potrebno je pristupiti
resursima u Viewu i postavkama u modelu, te dobiveni rezultat proslijediti Presenteru.
Kada se implementira nova značajka, dobro je na početku napisati ugovor. Ugovor opisuje
komunikaciju između Viewa i Presentera, pomaže pri lakšem osmišljavanju interakcije.
Ugovor se može sastojati od jednog ili dva sučelja. Jedno sučelje predstavlja komunikaciju
prema Viewu, a drugo sučelje predstavlja komunikaciju prema Presenteru. Sučelje prema
Viewu je obavezno, a u nekim implementacijama se sučelje prema Presenteru izostavlja.
Poanta oba sučelja je da se iz naziva metoda razumije kakve se komunikacija odvija prema
Viewu i Presenteru.
Kod View ugovora, Presenter mora ovisiti o sučelju View, a ne izravno o aktivnosti, jer se
na taj način odvaja (engl. decouple) Presenter od implementacije Viewa poštujući princip
inverzije ovisnosti (engl. dependency inversion principle) iz načela SOLID (Single
Responsibility Principle, Open-Closed Principle, Liskov Substitution Principle, Interface
Segregation principle i Dependency Inversion Principle).
Što se tiče Presenter ugovora, postoji dvije teorije. Jedna kaže da bi ga trebalo imati, jer se
na taj način odvaja konkretni View on konkretnog Presentera. A druga smatra da ga ne bi
trebalo imati jer se apstrahira nešto što je već apstrakcija (Viewa) i nije potrebno pisati
sučelje. Štoviše, vjerojatno se nikada neće napisati alternativni Presenter, pa bi to bio gubitak
vremena i linija koda.
Jedna od stvari na koje treba obratiti pozornost u MVP obrascu je ta da Presenter ima jedan-
na-jedan vezu s Viewom. Pojavljuje se kada i View i uništava se zajedno s Viewom, te može
kontrolirati samo jedan View odjednom.
U nastavku se nalazi primjer jednostavne značajke gdje korisnik upisuje svoje korisničko
ime i lozinku, koji se pritiskom na gumb Send šalju na poslužitelj. Značajka je napravljena
korištenjem MVP obrasca, DI-a korištenjem Daggera. Napisana je u jeziku Kotlin.
View ugovor:
interface IdentificationView{
fun showMessage()
}
20
View:
class IdentificationActivity : BaseActivity(),
IdentificationView {
@Inject lateinit var presenter: IdentificationPresenter
override fun providePresenter(): BasePresenter? =
presenter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_identification)
sendButton.setOnClickListener {
presenter.sendData(
usernameEditText.text.toString(),
passwordEditText.text.toString()
)
}
}
override fun showMessage(){
Toast.makeText(
this,
"Identification successfull!",
Toast.LENGTH_LONG
)
}
}
Presenter:
class IdentificationPresenter @Inject constructor(
private val view: IdentificationView,
private val identificationInteractor:
Interactors.IdentificationInteractor
) : BasePresenter(view) {
fun sendData(username: String, password: String) {
identificationInteractor
.sendData(User(username, password))
.subscribeOn(ioScheduler)
.observeOn(callbackScheduler)
.subscribe(
21
object:ErrorHandlingCompletableObserver(){
override fun onComplate(){
view.showMessage()
}
}
)
}
}
Model → Interactor:
data class User (
@SerializedName(“username”) val username: String,
@SerializedName(“password”) val password: String
)
interface Interactors{
interface IdentificationInteractor {
fun sendData(user: User): Completable
}
}
class IdentificationInteractor @Inject constructor(
private val apiService: ApiService
) : Interactors: IdentificationInteractor {
override fun sendData (user: User): Completable{
return apiService.identify(user)
}
}
Aktivnost IdentificationActivity sadrži referencu na Presenter koju je dobio
preko DI-a. Vidljivo je da se u Viewu ne nalazi nikakva logika. U onCreate() metodu se
samo postavlja onClickListener koji poziva metodu iz Presentera. U Presenteru se
nalazi sva potrebna logira za slanje podataka na poslužitelj.
Model je u ovom slučaju Interactor. Implementacija IdentificationInteractora ima referencu
na ApiService koji predstavlja klasu u kojoj se definira sam zahtjev (HTTP metodu i put).
22
2.2. Model-View-ViewModel obrazac
MVVM obrazac je trenutno vjerojatno najbolji odabir za arhitektonski obrazac. Sastoji se
od Viewa (informira ViewModel o korisnikovim akcijama), ViewModela (izlaže tokove
podataka relevantnih za View) i Modela (apstrahira izvor podataka, ViewModel radi s
DataModelom za dobivanje i spremanje podataka).
Na prvi pogled, MVVM se čini vrlo sličnim MVP obrascu, jer oboje rade veliki posao u
apstrahiranju stanja i ponašanja Viewa. Presentation Model apstrahira View neovisan od
određene platforme korisničkog sučelja, dok je obrazac MVVM kreiran kako bi se
pojednostavilo programiranje korisničkih sučelja vođeno događajima [9].
U obrazacu MVP Presenter izravno govori Viewu što prikazati, a kod MVVM-a ViewModel
izlaže tokove događaja na koje se Viewovi mogu povezati. Ovako, ViewModel više ne mora
držati referencu na View, za razliku od Presentera. To također znači da su sva sučelja koja
MVP uzorak zahtijeva, sada redundantna.
Viewovi također obavještavaju ViewModel o različitim aktivnostima. Prema tome, MVVM
obrazac podržava dvosmjerno vezanje podataka između Viewa i ViewModela, te postoji
odnos "više naprema jedan" između Viewa i ViewModela. View ima referencu na
ViewModel, ali ViewModel nema informacija o Viewu.
Slika 4 MVVM struktura [9]
Događajni dio koji zahtijeva MVVM obavlja se pomoću RxJava Observables. Konkretnije
koriste se LiveData i MutableLiveData. LiveData je observable klasa nositelja podataka. Za
razliku od uobičajenog observable-a, LiveData je svjestan životnog ciklusa (engl. lifecycle-
aware), što znači da poštuje životni ciklus drugih komponenti aplikacije, kao što su
aktivnosti, fragmenti ili usluge. Ova svijest osigurava da LiveData samo ažurira promatrače
komponenti aplikacije koji su u stanju aktivnog životnog ciklusa. LiveData nema javno
dostupne metode za ažuriranje pohranjenih podataka, te je zbog toga potreban
MutableLiveData [9].
23
Klasa MutableLiveData izlaže metode setValue (T) i postValue (T) javno i potrebno ih je
koristiti ako treba urediti vrijednost pohranjenu u MutableLiveData objektu. Obično se
MutableLiveData koristi u ViewModelu, a zatim ViewModel izlaže samo nepromjenjive
MutableLiveData objekte promatračima [12].
Što se tiče DataModela, DataModel izlaže podatke koji se lako mogu konzumirati kroz tok
događaja - RxJava's Observables. Sastavlja podatke iz više izvora, kao što je mrežni sloj
(engl. network layer), baza podataka ili zajedničke postavke i izlaže podatke onima kojima
je potrebno. DataModeli imaju cijelu poslovnu logiku.
Snažan naglasak na načelo jedinstvene odgovornosti dovodi do stvaranja DataModela za
svaku značajku aplikacije. Na primjer, imamo ArticleDataModel koji sastavlja izlaz iz API
usluge i sloja baze podataka. Ovaj DataModel obrađuje poslovnu logiku osiguravajući da se
najnovije vijesti iz baze podataka dohvaćaju primjenom filtra za dob [9].
ViewModel je model za View aplikacije: apstrakcija Viewa. ViewModel dohvaća potrebne
podatke iz DataModela, primjenjuje logiku korisničkog sučelja i zatim izlaže relevantne
podatke koji će View konzumirati. Slično kao i DataModel, ViewModel izlaže podatke
preko Observables.
Dvoje bitne stvari vrijede za ViewModel.
Prva od njih je da ViewModel treba otkriti stanja za View, a ne samo događaje. Na primjer,
ako je potrebno prikazati ime i adresu e-pošte korisnika, umjesto da za to stvaraju dva toka,
stvara se objekt DisplayableUser koji sadrži dva polja. Tok će se emitirati svaki put kada se
promijeni ime za prikaz ili e-pošta. Na taj način se osigurava da View uvijek prikazuje
trenutno stanje korisnika.
Druga stvar je da svaka radnja korisnika treba proći kroz ViewModel i da se svaka moguća
logika Viewa preseli u ViewModel.
View predstavlja korisničko sučelje u aplikaciji. On može biti aktivnost, fragment ili bilo
koji prilagođeni Android pogled.
Primjer koda za MVVM obrazac se nalazi u poglavlju 5. The Great Guide Android
aplikacija, u potpoglavlju 5.1.1. Primjer koda za MVVM obrazac.
24
3. Komunikacija Android aplikacije s poslužiteljem
korištenjem Retrofit, okHttp i RxJava biblioteka
Kada se govori o tome kako ostvariti komunikacija klijenta, u ovom slučaju Android
aplikacije s poslužiteljem, obično se misli na to kako definirati REST API poziv prema
poslužitelju, kako izvesti takav poziv, te kako obraditi odgovor koji je došao od poslužitelja.
Postoje različiti načini kako se takva komunikacija može ostvariti. Ovdje će se obraditi način
koji koristi Retrofit, okHttp i RxJava biblioteke, zato što ovaj pristup omogućava vrlo
pregledanu komunikaciju, clean code, te mogućnost dodavanja novih REST API poziva uz
minimalne promjene.
OkHTTP je projekt otvorenog koda dizajniran da bude učinkovit HTTP klijent. Podržava
SPDY protokol. SPDY je osnova za HTTP 2.0 i omogućuje višestrukim multipleksiranjem
više HTTP zahtjeva preko jedne utičnice (engl. socket). OkHttp podržava suvremene TLS
značajke (TLS 1.3, ALPN, pinning certifikat) [14].
Retrofit je REST Client biblioteka otvorenog koda koja se koristi u Androidu i Javi za
stvaranje HTTP zahtjeva i za obradu HTTP odgovora iz REST API-ja. Stvorio ga je Square.
Retrofit je moguće koristiti za primanje struktura podataka osim JSON-a, na primjer Jackson
i Moshi [11].
REST Client je u našem slučaju biblioteka Retrofit koja se koristi na strani klijenta (Android)
za izradu HTTP zahtjeva za REST API i za obradu odgovora. REST API definira skup
funkcija pomoću kojih programeri mogu izvesti zahtjeve i primiti odgovore putem HTTP
protokola kao što su GET i POST [10].
RESTful API je sučelje aplikacijskog programa (engl. application program interface,
skraćeno API) koje koristi HTTP zahtjeve za GET, PUT, POST i DELETE metode [10].
Za korištenje Retrofita u Android aplikaciji potrebno je imati 3 vrste klasa. Jedna od klasa
bi bilo sučelje koje definira HTTP operacije (u poglavlju 5., potpoglavlje 5.1.3 to sučelje se
naziva ApiService). Svaka metoda unutar sučelja predstavlja jedan mogući API poziv. Mora
imati HTTP anotaciju kao npr. GET ili POST, kako bi se odredila vrsta zahtjeva i relativni
URL. Povratna vrijednost omata odgovor u objekt s tipom očekivanog rezultata. Parametri
upita (engl. query parameters) također se mogu dodati metodi. Primjer takvog koda se nalazi
u nastavku:
25
@GET(“group/{id}/users”)
fun groupList(@Path(“id”) int groupId, @Query(“sort”) String
sort): Single<List<User>>
Za podešavanje URL-a koriste se zamjenski blokovi i parametre upita. Zamjenski blok
dodaje se relativnom URL-u s vitičastim zagradama {}. Uz pomoć anotacija @Path na
parametru metode, vrijednost tog parametra vezana je za određeni zamjenski blok.
Druga klasa bi trebala biti neka Retrofit klasa kao npr. ApiModule, koja inicijalizira sve što
je potrebno kako bi Retrofit funkcionirao.
Treća klasa je klasa s modelom zahtjeva ili odgovora. U Kotlinu je to data class koja u
konstruktoru prima sve potrebne parametre koji bi se trebali prikazati u JSON objektu.
Kako bi pretvorili data class u JSON objekt potrebni su nam Retrofit pretvarači (engl.
Retrofit converters). Retrofit pretvarači su poput sporazuma između Android klijenta i
poslužitelja o formatu u kojem će podaci biti zastupljeni. Obje strane se trebaju složiti oko
toga koji će se format koristiti za komunikaciju za prijenos podataka. To može biti JSON, a
može biti ili Moshi ili Jackson ili neki drugi podržani pretvarač. Ako će se koristiti JSON
onda je potrebno odabrati Gson pretvarač.
RxJava je biblioteka za sastavljanje asinkronih programa i programa temeljenih na
događajima koji koriste observable sekvence za JVM (Java Virtual Machine). RxJava je
zapravo JVM implementacija Reactive Extensions [13].
RxJava radi kao obrazac promatrač (engl. Observer pattern), te sadrži tri bitne komponente:
Observable, Subscriber i Observer.
Kod pretplaćivanja na događaj, u ovom slučaju odgovor poslužitelja, koristi se nekoliko
funkcija. Prva od njih je subscribe() s kojim se pretplaćuje na određeni proces.
Općenito se koristi IO dretva (engl. IO thread) pomoću Schedulers.io(). Druga
funkcija je observeOn() koja pokazuje gdje se žele konzumirati podaci koji dolaze iz
observable. Najčešće je ta metoda postavljena da se izvodi na glavnu dretvu koristeći
AndroidSchedulers.mainThread().
Primjer koda za komunikaciju s poslužiteljem se nalazi u poglavlju 5. The Great Guide
Android aplikacija, u potpoglavlju 5.1.3. Primjer koda za komunikaciju s poslužiteljem.
26
4. Kriptiranje podataka unutar Android aplikacije
Ako je potrebno sačuvati neke povjerljive podatke, a nije ih moguće spremiti ili dohvatiti s
poslužitelja, onda ih je potrebno spremiti na uređaj. Android sklopovlje nema nikakvu zaštitu
podatka koji se nalaze na njemu. To znači da ako netko ima pristup samom uređaju može se
spojiti na uređaj i pročitati sve što se nalazi na uređaju u čistom tekstu (engl. plaintext). Zbog
toga je potrebno sve podatke prije spremanja šifrirati.
Šifriranje je proces kodiranja svih korisničkih podataka na Android uređaju pomoću
simetričnih ključeva za šifriranje. Nakon što je uređaj šifriran, svi podaci koje je stvorio
korisnik automatski se šifriraju prije nego što ih se učita na disk i svi se podaci automatski
dešifriraju prije nego što ih se vrati u proces pozivanja. Šifriranje osigurava da čak i ako
neovlaštena strana pokuša pristupiti podacima, neće ih moći čitati [16].
U nastavku će biti objašnjeni ključni elementi kriptiranja i spremanja podataka na uređaj.
Sustav Android Keystore omogućuje pohranjivanje kriptografskih ključeva u spremnik kako
bi ih bilo teže izdvojiti iz uređaja. Kada se ključevi nalaze u spremištu ključeva (engl.
keystore), mogu se koristiti za kriptografske operacije s ključnim materijalom (engl. key
material) koji se ne može izvesti. Štoviše, nudi mogućnosti za ograničavanje kada i kako se
ključevi mogu koristiti, kao što je zahtjevanje autentikacije korisnika za korištenje ključa ili
ograničavanje ključeva koji će se koristiti samo u određenim kriptografskim načinima [15].
Sustav Android Keystore štiti ključni materijal od neovlaštene uporabe. Prvo, Android
Keystore ublažava neovlašteno korištenje ključnog materijala izvan Android uređaja
sprječavajući vađenje ključnog materijala iz aplikacijskih procesa i Android uređaja kao
cjeline. Drugo, Android KeyStore ublažava neovlaštenu upotrebu ključnog materijala na
uređaju Android tako što aplikacije navode ovlaštene upotrebe njihovih ključeva, a zatim
provode ta ograničenja izvan procesa aplikacija.
Ključni materijal ključeva za Android Keystore zaštićen je od vađenja pomoću dvije
sigurnosne mjere. Prva je da ključni materijal nikada ne ulazi u proces prijave. Kada
aplikacija izvodi kriptografske operacije pomoću ključa Android Keystorea, u pozadini, čisti
tekst, šifrirani tekst i poruke koje se potpisuju ili potvrđuju, unose se u proces sustava koji
izvodi kriptografske operacije. Ako je proces aplikacije ugrožen, napadač možda može
koristiti ključeve aplikacije, ali ne može izdvojiti ključni materijal (na primjer, za upotrebu
izvan Android uređaja). A druga mjera je da ključni materijal može biti povezan sa sigurnim
27
sklopovljem (npr. Trusted Execution Environment (TEE), Secure Element (SE)) uređaja
Android. Kada je ova značajka omogućena za ključ, materijal ključa nikada nije izložen
izvan sigurnog hardvera. Ako je Android OS ugrožen ili napadač može pročitati internu
pohranu uređaja, napadač će možda moći koristiti ključeve iz Android Keystorea iz bilo koje
aplikacije na Android uređaju, ali ih neće moći izdvojiti iz uređaja. Ta je značajka
omogućena samo ako sigurno sklopovlje uređaja podržava određenu kombinaciju algoritma
ključa, načina blokiranja, shema oblaganja (engl. padding schemes) i digestije (engl. digests)
kojima je ključ dopušten za korištenje [15].
Ako je potrebno spremiti ključ/vrijednost parove na uređaj moguće je koristiti
SharedPreferences API-je. Objekt SharedPreferences pokazuje na datoteku koja sadrži
parove ključ/vrijednost i pruža jednostavne metode za njihovo čitanje i pisanje. Svakom
SharedPreferences datotekom upravlja okvir i može biti privatno ili dijeljeno [17].
Moguće je stvoriti novu datoteku zajedničkih postavki ili pristupiti postojećoj tako što
pozvanjem getSharedPreferences() ili getPreferences() metode.
Metoda getSharedPreferences()se koristiti ovo ako je potrebno više datoteka
zajedničkih postavki koje su identificirane imenom.
Metoda getPreferences() se koristiti iz Activity klase ako je potrebno koristiti samo
jednu datoteku zajedničkih postavki za aktivnost. Budući da ova metoda dohvaća zadanu
datoteku zajedničkih postavki koja pripada aktivnosti, nije potrebno unijeti ime.
U nastavku je primjer koda za obje metode.
val sharedPref = activity?.getSharedPreferences(
“SHARED_PREFERENCES”,
Context.MODE_PRIVATE
)
val sharedPref = activity?.getPreferences(
Context.MODE_PRIVATE
)
SecureSharedPreferences je klasa koja proširuje SharedPreferences objekt, na način da prije
nego što spremi par ključ/vrijednost, kriptira vrijednost. Također kod dohvaćanja vrijednosti,
prvo dekriptira vrijednost.
28
5. The Great Guide Android aplikacija
The Great Guide je Android aplikacija koja prikazuje život Nikole Tesle kroz mjesta na
kojima je bio ili kroz mjesta koja su njemu posvećena.
U nastavku se nalaze slike ekrana iz aplikacije.
Slika 5 Prvi uvodni ekran Slika 6 Drugi uvodni ekran Slika 7 Treći uvodni ekran
Slike 5, 6 i 7 predstavljaju uvodne ekrane na kojima je u kratkim crtama objašnjena
aplikacija.
29
Slika 8 Prikaz gradova – Smiljan Slika 9 Prikaz gradova – Zagreb
Slike 8 i 9 prikazuju gradove koji su značajni za Teslin život. Ovdje su prikazani samo
Smiljan i Zagreb, ali ako se doda neki novi grad u bazu podataka, bit će prikazan u ovom
nizu gradova. Iznad imena svakog grada se nalazi krug. Veličina kruga ovisi o tome koliko
je velik grad, a boja ovisi o tome da li ima nekih dodatnih podatka o tom gradu ili ne. Ako
ima podataka onda će krug biti žuti i pojavit će se gumb „Visit“, inače će biti sivi.
30
Slika 10 Prikaz mape grada Slika 11 Prikaz odabrane lokacije
Slika 10 prikazuje mapu grada s nizom lokacija značajnih za Teslu. Lokacije se mogu
odabirati klikom na tamno-plave krugove ili pomicanjem kartice s podacima o lokaciji. Na
vrhu ekrana se uz ime grada nalazi gumb za detalje o lokaciji, gumb za upute kako doći do
lokacije i gumb za dijeljenje lokacije.
Ako korisnik odabere gumb za detalje o lokaciju otvorit će mu se ekran na slici 11. Na njemu
će korisnik moći vidjeti detalje o lokaciji, kao npr. neki opis lokacije i slike vezane uz
lokaciju.
5.1. Dijelovi koda iz The Great Guide aplikacije
U gornjim poglavljima su teorijski objašnjeni različiti aspekti koji su utjecali na izgled koda
u aplikaciji. U nastavku se nalaze dijelovi koda koji potkrepljuju gornja poglavlja.
5.1.1. Primjer koda za MVVM obrazac
U nastavku je primjer MVVM obrasca iz The Great Guide aplikacije, ali bez komponente
Modela. Dio s Modelom se nalazi u dijelu koda vezen za komunikaciju s poslužiteljem.
31
View:
class CitiesActivity : BaseActivity() {
@Inject
@field:ViewModelInjection
lateinit var viewModel: CitiesViewModel
override fun provideCommonViewModel(): BaseViewModel<*>?=
viewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_cities)
viewModel.viewStateRender()
.observe(this, Observer {state ->
when(state){
is CitiesInit -> initUI(state.list)
}
})
viewModel.start(null)
}
fun initUI(list: List<CitiesModel>){
citiesViewPager.adapter =
CitiesAdapter(supportFragmentManager, list)
citiesDotsIndicator.count = list.size
}
}
ViewModel:
class CitiesViewModel @Inject constructor(
private val citiesInteractor:
Interactors.CitiesInteractor,
@IoThreads private val ioScheduler: Scheduler,
@Callback private val callbackScheduler: Scheduler
) : BaseViewModel<CitiesState>() {
override fun started() {
loadCities()
}
32
private fun loadCities() {
citiesInteractor
.execute(Unit)
.subscribeOn(ioScheduler)
.observeOn(callbackScheduler)
.subscribe(this,
onSuccess = { citiesResponse ->
var list = mutableListOf<CitiesModel>()
for(item in citiesResponse){
list.add(item.value)
}
viewStateLiveData.value = CitiesInit(
list.sortedBy {
it.position.toInt()
}
)
}, onErrorResponse = { code, errorMessage ->
commonStateLiveData.value =
ErrorMessage(errorMessage)
true
})
}
}
Klasa stanja:
sealed class CitiesState
class CitiesInit(val list: List<CitiesModel>) : CitiesState()
Klasa za izgradnju aktivnosti:
class CitiesActivityBuilder : ActivityBundleArguments {
override fun intent(context: Context) =
context.intentFor<CitiesActivity>()
}
Adapter za prikaz gradova:
class CitiesAdapter constructor(
private val fragmentManager: FragmentManager,
private val citiesList: List<CitiesModel>
) : FragmentPagerAdapter(fragmentManager) {
33
override fun getItem(position: Int): Fragment {
return CitiesFragment.newInstance(
citiesList[position],
position
)
}
override fun getCount(): Int = citiesList.size
}
Fragment koji prikazuje jedan grad:
class CitiesFragment : Fragment() {
companion object {
const val CITIES_FRAGMENT_KEY = "cities_item"
const val CITIES_FRAGMENT_POSITION =
"cities_position"
fun newInstance(
citiesModel: CitiesModel,
position: Int
): CitiesFragment {
val args = Bundle()
args.putSerializable(
CITIES_FRAGMENT_KEY,
citiesModel
)
args.putSerializable(
CITIES_FRAGMENT_POSITION,
position
)
return CitiesFragment().apply { arguments = args}
}
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
34
): View? {
val view = inflater.inflate(
R.layout.item_cities,
container,
false
)
var position = arguments?
.getSerializable(CITIES_FRAGMENT_POSITION)
(arguments?.getSerializable(
CITIES_FRAGMENT_KEY
) as? CitiesModel
)?.let { citiesItem ->
//kod za inicijalizaciju elementata unutar
//fragementa, kao npr. naziv grada ili opis grada
}
return view
}
}
5.1.2. Primjer koda za injekciju ovisnosti
U gornjem kodu je vidljiva uporaba DI-a (Vidljivo je iz @Inject anotacije). DI, konkretnije
biblioteka Dagger, se koristi kroz cijelu aplikaciju, a u nastavku je primjer koda koji se
nadovezuje na već gore viđeni kod.
Dagger modul kojim se stvara ViewModel, konkretnije CitiesViewModel:
@Module
class CitiesActivityModule{
@Provides
@ViewModelInjection
fun provideViewModel(
activity: CitiesActivity,
provider: InjectionViewModelProvider<CitiesViewModel>
): CitiesViewModel = provider.get(activity)
}
Modul kojim se stvaraju Viewovi, u ovom slučaju aktivnosti:
@Module
abstract class ActivityBuilder {
@ContributesAndroidInjector(modules =
[CitiesActivityModule::class])
35
abstract fun bindCitiesActivity(): CitiesActivity
}
Glavna DI klasa, AppComponent:
@Singleton
@Component(
modules = arrayOf(
AndroidSupportInjectionModule::class,
ActivityBuilder::class,
AppModule::class,
SchedulersModule::class,
ApiModule::class,
InteractorsModule::class
)
)
interface AppComponent {
fun inject(teslasjourney: TeslasJourneyApp)
fun okHttpClient(): OkHttpClient
fun retrofit(): Retrofit
}
5.1.3. Primjer koda za komunikaciju s poslužiteljem
Kako bi se ostvarili API pozivi prema poslužitelju, potrebno je postaviti network dio
aplikacije. U nastavku će biti prikazane osnovne klase koje su potrebne inicijalizaciju
komunikacije prema poslužitelju, te slanja zahtjeva i primanja odgovora od poslužitelja.
Osnovna klasa Interactora koju nasljeđuju svi izvedeni Interactori.
interface BaseInteractor<in Request, out Response> {
fun execute(req: Request): Response
}
Sučelje u kojem su definirani svi Interactori:
interface Interactors {
interface CitiesInteractor :
BaseInteractor<Unit, Single<Map<String, CitiesModel>>>
}
Klasa koja implementira sučelje CitiesInteractora:
class CitiesInteractor @Inject constructor(
private val apiService: ApiService
) : Interactors.CitiesInteractor {
36
override fun execute(req: Unit):
Single<Map<String, CitiesModel>> {
return apiService.cities()
}
}
Klasa u kojoj su definirani sve potrebne HTTP metode i funkcije:
const val TESLA_PATH = "/persons/Tesla"
interface ApiService {
@GET("$TESLA_PATH/cities.json")
fun cities(): Single<Map<String, CitiesModel>>
}
Data klasa u kojom je definiran odgovor poslužitelja za cities() funkciju
data class CitiesModel (
@SerializedName("name") val cityName: String,
@SerializedName("description") val cityDescription:
String,
@SerializedName("size") val citySize: Int,
@SerializedName("hasData") val hasData: Boolean,
@SerializedName("id") val id: String,
@SerializedName("position") val position: String
) : Serializable
Definicije anotacija koje se koriste u ViewModelima kod registracije na različite dretve
prilikom API poziva:
@Qualifier
@Retention(AnnotationRetention.RUNTIME)
annotation class Callback
@Qualifier
@Retention(AnnotationRetention.RUNTIME)
annotation class IoThreads
Ekstenzije metode koja se koristi za pretplatu na odgovore od poslužitelja:
fun <T : Any> Single<T>.subscribe(
viewModel: BaseViewModel<*>,
onSuccess: (T) -> Unit,
onErrorResponse: (
code: Int,
errorMessage: String
) -> Boolean = { _, _ -> false }
37
) {
subscribe(object :
ErrorHandlingSingleObserver<T>(viewModel) {
override fun onSuccess(t: T) {
onSuccess.invoke(t)
}
override fun onErrorResponse(
code: Int,
errorMessage: String
): Boolean {
return onErrorResponse.invoke(code, errorMessage)
}
})
}
Apstraktna klasa koja definira methode za rješavanje odgovora od poslužitelja koji su došli
u obliku greške, npr. 4XX ili 5XX error kodovi.
abstract class ErrorHandlingSingleObserver<T>(
val viewModel: BaseViewModel<*>
) : SingleObserver<T>, ErrorHandlingObserver {
override fun onSubscribe(d: Disposable) {
viewModel.compositeDisposable.add(d)
}
override fun onError(e: Throwable) {
handleException(e, this)
viewModel.clearCommonState()
}
override fun onServiceError(errorMessage: String) {
viewModel.errorMessage(errorMessage)
viewModel.clearCommonState()
}
}
Metoda koja prosljeđuje sve odgovore, koji dolaze kao error.
fun handleException(
t: Throwable,
errorListener: ErrorHandlingObserver) {
38
try {
when (t) {
is HttpException -> {
val retrofit = TeslasJourneyApp
.instance
.appComponent
.retrofit()
val error = extractErrorMessage(t, retrofit)
if (error != null &&
errorListener.onErrorResponse(
t.code(),
error.errorMessage
)
) {
return
}
if (error != null) {
Timber.e(error.toString())
errorListener
.onServiceError(error.errorMessage)
} else {
Timber.e(t)
errorListener.onUnknownProblem()
}
}
is SocketTimeoutException, is
UnknownHostException -> {
Timber.w(t)
errorListener.onNetworkProblem()
}
else -> {
Timber.e(t)
errorListener.onUnknownProblem()
}
}
} catch (secondThrowable: Throwable) {
Timber.e(secondThrowable)
errorListener.onUnknownProblem()
}
39
}
Modul koji se koristi za inicijalizaciju Retrofita i OkHttpClienta:
@Module
class ApiModule {
@Provides
@Singleton
fun okHttpClient(): OkHttpClient {
val okHttpBuilder: OkHttpClient.Builder =
OkHttpClient.Builder()
if (BuildConfig.DEBUG || BuildConfig.FLAVOR ==
"staging") {
val loggingInterceptor = HttpLoggingInterceptor(
HttpLoggingInterceptor.Logger { s ->
Timber.d(s)
})
.setLevel(HttpLoggingInterceptor.Level.BODY)
okHttpBuilder.addInterceptor(loggingInterceptor)
okHttpBuilder.addInterceptor(
ChuckInterceptor(TeslasJourneyApp.instance)
)
}
okHttpBuilder.connectTimeout(30, TimeUnit.SECONDS)
return okHttpBuilder.build()
}
@Provides
@Singleton
fun apiService(retrofit: Retrofit): ApiService {
return retrofit.create(ApiService::class.java)
}
@Provides
@Singleton
fun retrofit(okHttpClient: OkHttpClient): Retrofit {
val retrofitBuilder = Retrofit.Builder()
.baseUrl(BuildConfig.API_URL)
.addConverterFactory(
40
ScalarsConverterFactory.create()
).addConverterFactory(
GsonConverterFactory.create(
GsonProvider.provide()
)
).addCallAdapterFactory(
RxJava2CallAdapterFactory.create()
).client(okHttpClient)
return retrofitBuilder.build()
}
}
Povider za json converter:
object GsonProvider {
private val gson: Gson = GsonBuilder()
.create()
fun provide(): Gson {
return gson
}
}
5.1.4. Primjer koda za kriptiranje podataka koji se spremaju na
Android uređaj
U kodu se nalaza dvije vrste Keystora. Broj i vrsta Keystora ovisi o minimalnoj verziji
Androida. Ako se minimalna verzija Androida postavljena na manje od Marshmallow (API
level 23, Android Version 6.0), onda je potrebno imati dvije vrste Keystora. Na Androidima
manjim od Marshmallow ne postoji Android Keystore, pa je potrebno napraviti datoteku
gdje će se spremati ključevi i tu datoteku kriptirati. U nastavku su primjeri koda za
inicijalizaciju Android Keystora i za stvaranje i inicijalizacije Default Keystora (za verzije
Androida manje od Marshmallow).
Klasa za kreiranje i dohvaćanje Keystora i kreiranje simetričnih ključeva:
class KeyStoreWrapper(defaultKeyStoreName: String) {
private val keyStore: KeyStore = createAndroidKeyStore()
private val defaultKeyStoreFile = File(
TeslasJourneyApp.instance.filesDir,
41
defaultKeyStoreName
)
private val defaultKeyStore = createDefaultKeyStore()
/**
* Metoda vraća simetrični ključ kojem pripada dani alias
* iz Android Keystora
*/
fun getAndroidKeyStoreSymmetricKey(alias: String):
SecretKey? = keyStore.getKey(alias, null) as SecretKey?
/**
* Metoda vraća simetrični ključ kojem pripada dani alias
* iz Default Keystora
*/
fun getDefaultKeyStoreSymmetricKey(
alias: String,
keyPassword: String
): SecretKey? {
return try {
defaultKeyStore.getKey(
alias,
keyPassword.toCharArray()
) as SecretKey
} catch (e: UnrecoverableKeyException) {
null
}
}
/**
* Metoda koja briše ključ s danim aliasom iz Android
* Keystora
*/
fun removeAndroidKeyStoreKey(alias: String) =
keyStore.deleteEntry(alias)
fun createDefaultKeyStoreSymmetricKey(
alias: String,
password: String
) {
val key = generateDefaultSymmetricKey()
42
val keyEntry = KeyStore.SecretKeyEntry(key)
defaultKeyStore.setEntry(
alias,
keyEntry,
KeyStore.PasswordProtection(password.toChar
Array())
)
defaultKeyStore.store(
FileOutputStream(defaultKeyStoreFile),
password.toCharArray()
)
}
fun generateDefaultSymmetricKey(): SecretKey {
val keyGenerator = KeyGenerator.getInstance("AES")
return keyGenerator.generateKey()
}
fun createAndroidKeyStoreSymmetricKey(
alias: String
): SecretKey {
val keyGenerator = KeyGenerator.getInstance(
KeyProperties.KEY_ALGORITHM_AES,
"AndroidKeyStore"
)
val builder = KeyGenParameterSpec.Builder(
alias,
KeyProperties.PURPOSE_ENCRYPT or
KeyProperties.PURPOSE_DECRYPT
).setBlockModes(
KeyProperties.BLOCK_MODE_CBC
).setEncryptionPaddings(
KeyProperties.ENCRYPTION_PADDING_PKCS7
)
keyGenerator.init(builder.build())
return keyGenerator.generateKey()
}
private fun createAndroidKeyStore(): KeyStore {
43
val keyStore =
KeyStore.getInstance("AndroidKeyStore")
keyStore.load(null)
return keyStore
}
private fun createDefaultKeyStore(): KeyStore {
val keyStore =
KeyStore.getInstance(KeyStore.getDefaultType())
if (!defaultKeyStoreFile.exists()) {
keyStore.load(null)
} else {
keyStore.load(
FileInputStream(defaultKeyStoreFile),
Null
)
}
return keyStore
}
}
Klasa za kriptiranje podataka:
class CipherWrapper(transformation: String) {
companion object {
var TRANSFORMATION_SYMMETRIC = "AES/CBC/PKCS7Padding"
val IV_SEPARATOR = "]"
}
val cipher: Cipher = Cipher.getInstance(transformation)
/**
* Metoda koja kriptira podatke korištenjem ključa
*
* @param data je podatak koji će kriptirati
* @param key je ključ za kriptiranje
* @param useInitializationVector podatak se kriptira
* korištenjem inicijalizacijskog vektora kojeg je sustav
* generirao
44
*
*/
fun encrypt(
data: String,
key: Key?,
useInitializationVector: Boolean = false
): String {
cipher.init(Cipher.ENCRYPT_MODE, key)
var result = ""
if (useInitializationVector) {
val iv = cipher.iv
val ivString = Base64.encodeToString(
iv,
Base64.DEFAULT
)
result = ivString + IV_SEPARATOR
}
val bytes = cipher.doFinal(data.toByteArray())
result += Base64.encodeToString(
bytes,
Base64.DEFAULT
)
return result
}
/**
* Metoda koja dekriptira podatke korištenjem ključa
*
* @param data je podatak koji će dekriptirati
* @param key je ključ za dekriptiranje
* @param useInitializationVector podatak se dekriptira
* korištenjem inicijalizacijskog vektora
*/
fun decrypt(
data: String,
key: Key?,
useInitializationVector: Boolean = false
): String {
var encodedString: String
45
if (useInitializationVector) {
val split = data.split(IV_SEPARATOR.toRegex())
if (split.size != 2) throw
IllegalArgumentException("Passed data is
incorrect. There was no IV specified with
it.")
val ivString = split[0]
encodedString = split[1]
val ivSpec = IvParameterSpec(Base64.decode(
ivString,
Base64.DEFAULT)
)
cipher.init(Cipher.DECRYPT_MODE, key, ivSpec)
} else {
encodedString = data
cipher.init(Cipher.DECRYPT_MODE, key)
}
val encryptedData = Base64.decode(
encodedString,
Base64.DEFAULT
)
val decodedData = cipher.doFinal(encryptedData)
return String(decodedData)
}
}
Klasa koja vrši kriptiranje i dekriptiranje podatka korištenjem metoda iz klasa navedenih
iznad (CipherWrapper i KeyStoreWrapper):
class EncryptionServices {
companion object {
val DEFAULT_KEY_STORE_NAME = "default_keystore"
}
private var keyStoreWrapper: KeyStoreWrapper
init {
keyStoreWrapper =
KeyStoreWrapper(DEFAULT_KEY_STORE_NAME)
46
}
/**
* Metoda za kreiranje i spremanje ključa kojim se čuvaju
* podaci.
*/
fun createMasterKey(password: String? = null) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
createAndroidSymmetricKey()
} else {
createDefaultSymmetricKey(password ?: "")
}
}
/**
* Metoda za brisanje glavnog ključa za kriptiranje iz
* Android Keystora.
*/
fun removeMasterKey(masterKey: String) {
keyStoreWrapper.removeAndroidKeyStoreKey(masterKey)
}
/**
* Kriptiranje user passworda i podataka korištenjem
* kreiranog glavnog ključa.
*/
fun encrypt(
data: String,
keyPassword: String? = null
): String {
// ako je Android verzija veća ili jednaka
// Mashmallow, onda koristi Android Keystore
return if (Build.VERSION.SDK_INT >=
Build.VERSION_CODES.M) {
encryptWithAndroidSymmetricKey(data)
} else {
encryptWithDefaultSymmetricKey(
data,
keyPassword ?: ""
)
}
47
}
/**
* Dekriptiranje user passworda i podataka korištenjem
* kreiranog glavnog ključa.
*/
fun decrypt(
data: String,
keyPassword: String? = null
): String {
return if (Build.VERSION.SDK_INT >=
Build.VERSION_CODES.M) {
decryptWithAndroidSymmetricKey(data)
} else {
decryptWithDefaultSymmetricKey(
data,
keyPassword ?: ""
)
}
}
private fun createAndroidSymmetricKey() {
keyStoreWrapper.createAndroidKeyStoreSymmetricKey(
BuildConfig.KEY_PASS
)
}
private fun encryptWithAndroidSymmetricKey(
data: String
): String {
val masterKey = keyStoreWrapper
.getAndroidKeyStoreSymmetricKey(
BuildConfig.KEY_PASS
)
return CipherWrapper(
CipherWrapper.TRANSFORMATION_SYMMETRIC
).encrypt(data, masterKey, true)
}
private fun decryptWithAndroidSymmetricKey(
data: String
48
): String {
val masterKey = keyStoreWrapper
.getAndroidKeyStoreSymmetricKey(
BuildConfig.KEY_PASS
)
return CipherWrapper(
CipherWrapper.TRANSFORMATION_SYMMETRIC
).decrypt(data, masterKey, true)
}
private fun createDefaultSymmetricKey(password: String) {
keyStoreWrapper.createDefaultKeyStoreSymmetricKey(
BuildConfig.KEY_PASS,
Password
)
}
private fun encryptWithDefaultSymmetricKey(
data: String,
keyPassword: String
): String {
val masterKey = keyStoreWrapper
.getDefaultKeyStoreSymmetricKey(
BuildConfig.KEY_PASS,
keyPassword
)
return CipherWrapper(
CipherWrapper.TRANSFORMATION_SYMMETRIC
).encrypt(data, masterKey, true)
}
private fun decryptWithDefaultSymmetricKey(
data: String,
keyPassword: String
): String {
val masterKey = keyStoreWrapper
.getDefaultKeyStoreSymmetricKey(
BuildConfig.KEY_PASS,
keyPassword
)
return masterKey?.let { CipherWrapper(
49
CipherWrapper.TRANSFORMATION_SYMMETRIC
).decrypt(data, masterKey, true)
} ?: ""
}
}
Sučelje koje definira metode koje koristi ViewModel za spremanje tajnih podataka na
Android uređaj:
interface SecurePrefsStore {
fun saveSecureString(
value: String,
key: String,
keyStorePassword: String
)
fun getSecureString(
key: String,
keyStorePassword: String
):String
fun hasString(key: String): Boolean
}
Klasa koja sprema i dohvaća kriptirane podatke iz SharedPreferences:
class SecurePreferenceStore @Inject constructor() :
SecurePrefsStore {
companion object {
private const val SECURE_PREFERENCE =
"SECURE_PREFERENCE"
const val SECRET = "SECRET"
}
private lateinit var password: String
private var securePreferences: SharedPreferences
init {
securePreferences = TeslasJourneyApp.instance
.getSharedPreferences(
SECURE_PREFERENCE,
Context.MODE_PRIVATE
)
50
}
override fun saveSecureString(
value: String,
key: String,
keyStorePassword: String
) {
password = keyStorePassword
if (securePreferences.getString(key, "") == "") {
EncryptionServices().createMasterKey(password)
saveString(encryptSecret(value), key)
} else {
editSecureString(encryptSecret(value), key)
}
}
override fun getSecureString(
key: String,
keyStorePassword: String
): String {
password = keyStorePassword
return decryptSecret(securePreferences.getString(
key,
""
))
}
override fun hasString(key: String): Boolean {
return securePreferences.getString(key, "")
.isNotEmpty()
}
private fun editSecureString(value: String, key: String)
{
securePreferences.edit().remove(key).apply()
securePreferences.edit()
.putString(key, value)
.apply()
}
private fun saveString(value: String, key: String) {
51
securePreferences.edit()
.putString(key, value)
.apply()
}
private fun encryptSecret(secret: String): String {
return EncryptionServices().encrypt(secret, password)
}
private fun decryptSecret(secret: String): String {
return EncryptionServices().decrypt(secret, password)
}
}
52
Zaključak
U ovom radu su objašnjeni bitni aspekti izrade kvalitetne Android aplikacije. Poznavanje
Gradlea omogućuje kreiranje kvalitetnih skripti za izgradnju projekta koje će ubrzati i
olakšati, ne samo izgradnju projekta, već i dodatne zadatke kao npr. pokretanje različitih
zadataka za pregled koda. Također, bitno je na početku projekta dogovoriti arhitekturu
sustava kako bi se znalo na koji način održavati sustav, te dodavati nove značajke u projekt.
Komunikacija Android aplikacije s poslužiteljem jedan je od ključnih aspekata izrade
aplikacije. Skoro svaka Android aplikacija ima neku vrstu komunikacije s poslužiteljem, a u
radu je objašnjeno kako napraviti takvu komunikaciju korištenjem Retrofita i okHTTP
klijenta. Aplikacije ponekad moraju spremati osjetiljive podatke na uređaj, pa je potrebno te
podatke prije spremanja kriptirati.
Razvoj kvalitetnih Android aplikacija je složen i zahtjevan proces. Potrebno je posvetiti puno
vremena, ne samo implementaciji već i planiranju svih aspekata koji će biti ključni za razvoj
aplikacije.
53
Literatura
[1] GRADLE, What is Gradle?,
https://docs.gradle.org/current/userguide/what_is_gradle.html, 08.04.2019.
[2] MEDIUM, Understanding Android Gradle build files,
https://medium.com/@wasyl/understanding-android-gradle-build-files-
e4b45b73cc4c, 11.04.2019.
[3] GRADLE BLOG, Kotlin Meets Gradle, https://blog.gradle.org/kotlin-meets-gradle,
17.04.2019.
[4] GRADLE DOCS, Gradle Kotlin DSL Primer,
https://docs.gradle.org/current/userguide/kotlin_dsl.html, 17.04.2019.
[5] GRADLE GUIDES, Migrating build logic from Groovy to Kotlin,
https://guides.gradle.org/migrating-build-logic-from-groovy-to-kotlin/, 15.5.2019.
[6] GRADLE DOCS, Using Gradle Plugins,
https://docs.gradle.org/5.0/userguide/plugins.html#sec:plugins_block, 15.5.2019.
[7] MEDIUM, Considering architecture for Android app, https://medium.com/stepstone-
tech/considering-architecture-for-android-app-f7f0fabf680a, 10.6.2019.
[8] MEDIUM, Model-View-Presenter: Android guidelines,
https://medium.com/@cervonefrancesco/model-view-presenter-android-guidelines-
94970b430ddf, 13.6, 2019.
[9] MEDIUM, Android Architecture Patterns Part 3: Model-View-ViewModel,
https://medium.com/upday-devs/android-architecture-patterns-part-3-model-view-
viewmodel-e7eeee76b73b, 13.6.2019.
[10] ANDROIDPUB, Consuming REST API using Retrofit Library in Android,
https://android.jlelse.eu/consuming-rest-api-using-retrofit-library-in-android-
ed47aef01ecb, 19.06.2019.
[11] SQUARE, Retrofit, https://square.github.io/retrofit/, 19.06.2019
[12] MEDIUM, Consuming REST API using Retrofit Library with the help of MVVM,
Dagger 2, LiveData and RxJava 2 in Android,
https://medium.com/@saquib3705/consuming-rest-api-using-retrofit-library-with-
the-help-of-mvvm-dagger-livedata-and-rxjava2-in-67aebefe031d, 19.06.2019.
[13] GITHUB, RxJava, https://github.com/ReactiveX/RxJava, 19.06.2019.
[14] SQUARE, okHttp, https://square.github.io/okhttp/, 21.06.2019.
[15] ANDROID DEVELOPERS, Android keystore system,
https://developer.android.com/training/articles/keystore, 24.06.2019.
[16] SOURCE, Encryption, https://source.android.com/security/encryption, 24.06.2019.
[17] ANDROID DEVELOPERS, Save key-value data,
https://developer.android.com/training/data-storage/shared-preferences, 24.06.2019.
54
Sažetak
Napredne tehnike i alati za izradu aplikacija za operacijski sustav
Android
Cilj ovog rada je pokazati kako oblikovati kvalitetnu Android aplikaciju. Postoje različite
tehnike i alati koji se mogu koristiti kod konstruiranja Android aplikacija, a u ovom radu su
pokrivene one koje se danas smatraju najboljim izborom za izradu Android aplikacija. One
smanjuju količinu koda, čine kod čitljivijim, podržavaju SOLID principe. Na taj način se
smanjuje vrijeme potrebno za održavanje i dodavanje novih značajki.
Ključne riječi: Android, Gradle, Arhitektura sustava, Model-View-Presenter obrazac,
Model-View-ViewModel obrazac, REST, Retrofit, okHTTP klijent, RXJava, kriptiranje i
dekriptiranje podataka
55
Summary
Advanced Techniques and Tools for Android Application Development
The goal of this thesis is to show how to create a quality Android application. There are a
variety of techniques and tools that can be used in the design of Android applications, and
in this thesis are the ones that are considered to be the best choice for Android applications
today. They reduce the amount of code, make it more readable and they support SOLID
principles. This reduces the time it takes for maintaining and adding new features.
Keywords: Android, Gradle, System architecture, Model-View-Presenter pattern, Model-
View-ViewModel pattern, REST, Retrofit, okHTTP client, RXJava, encryption and
decryption of data
56
Privitak
Upute za instalaciju
Za pokretanje aplikacije potrebno je imati instaliran Android Studio. Ako je potrebno
instalirati Android Studio na onom linku (https://developer.android.com/studio/install) se
nalaze upute za instalaciju i zadnja verzija Android Studija.
Na onom linku (https://developer.android.com/training/basics/firstapp/running-app) se
nalaze upute kako se instalira aplikacija na Android uređaj.