napredne tehnike i alati za izradu aplikacija za ... · mvvm (model-view-viewmodel) obrasci....

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

Upload: others

Post on 22-May-2020

13 views

Category:

Documents


0 download

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.