20 često postavljanih pitanja za Mobile Developer intervju. Mobilni razvoj je tražena oblast usmerena na pouzdane iOS, Android i cross-platform aplikacije. Pitanja pokrivaju različite nivoe, a odgovore možete vežbati naglas u našem treneru za intervjue.
1Koja je razlika između skladištenja tokena za autentifikaciju u standardnom skladištu ključ-vrednost kao što su SharedPreferences ili UserDefaults i platformskih bezbednih skladišta kao što su Android Keystore ili iOS Keychain?
Standardni mehanizmi skladištenja kao što su SharedPreferences na Android platformi i UserDefaults na iOS platformi dizajnirani su za jednostavna, neosetljiva korisnička podešavanja. Oni skladište podatke u nešifrovanim tekstualnim datotekama (XML ili plist) unutar izolovanog direktorijuma aplikacije (sandbox). Svako ko ima fizički pristup rutovanom (rooted) ili jailbroken uređaju, nešifrovanoj rezervnoj kopiji (backup) ili sistemu datoteka može direktno pročitati ove tokene. Nasuprot tome, platformsko bezbedno skladište — konkretno iOS Keychain i Android Keystore (koji se često koristi preko EncryptedSharedPreferences) — obezbeđuje šifrovanje podataka u mirovanju (data-at-rest encryption). iOS Keychain šifruje sačuvane stavke koristeći ključeve vezane za hardver uređaja (kao što je Secure Enclave) i omogućava detaljno definisanje pravila pristupa. Android Keystore generiše i skladišti kriptografske ključeve unutar hardverski izolovanih bezbednosnih modula (Trusted Execution Environment ili StrongBox), obezbeđujući da kriptografski ključevi nikada ne budu izloženi u memoriji aplikacije i da se ne mogu ekstrahovati iz sistema datoteka.
// --- ANDROID ---
// INSECURE (Plain SharedPreferences):
val prefs = context.getSharedPreferences("app_prefs", Context.MODE_PRIVATE)
prefs.edit().putString("auth_token", token).apply() // Plaintext XML on disk
// SECURE (EncryptedSharedPreferences backed by Android Keystore):
val masterKey = MasterKey.Builder(context)
.setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
.build()
val securePrefs = EncryptedSharedPreferences.create(
context,
"secure_prefs",
masterKey,
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
)
securePrefs.edit().putString("auth_token", token).apply()
// --- IOS (Swift) ---
// INSECURE:
UserDefaults.standard.set(token, forKey: "auth_token") // Plain plist on disk
// SECURE (Keychain Services API):
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: "user_auth_token",
kSecValueData as String: token.data(using: .utf8)!,
kSecAttrAccessible as String: kSecAttrAccessibleWhenUnlocked
]
SecItemAdd(query as CFDictionary, nil)
2Koja je primarna svrha mobilnog CI/CD (Continuous Integration / Continuous Delivery) toka i koje jedinstvene faze i ograničenja ga razlikuju od procesa isporuke za web ili backend sisteme?
Primarna svrha mobilnog CI/CD toka jeste automatizacija izgradnje (build), testiranja, potpisivanja i distribucije klijentskih mobilnih aplikacija kako bi se održao dosledan kvalitet i omogućila ponovljiva objavljivanja izdanja. Kontinuirana integracija (CI) validira izmene u kodu putem automatizovane izgradnje, statičke analize i jediničnog testiranja (unit testing). Kontinuirana isporuka/distribucija (CD) pakuje, potpisuje i distribuira binarne artefakte na kanale za testiranje (kao što su TestFlight ili Firebase App Distribution) ili u prodavnice aplikacija.
Mobilni CI/CD se razlikuje od web i backend procesa na nekoliko ključnih načina:
1. Tip artefakata: Izgradnja proizvodi kompajlirane klijentske binarne fajlove (.ipa, .apk, .aab) umesto servisa koji se izvršavaju direktno na serverima ili unutar kontejnerskih slika.
2. Hardverska ograničenja izvršnih mašina (runners): Kompajliranje za iOS zahteva macOS hardver i Xcode alate komandne linije, dok se backend tokovi obično pokreću na laganim Linux kontejnerima.
3. Kašnjenje pri objavljivanju i odobrenja: Objavljivanje krajnjim korisnicima uključuje cikluse pregleda eksternih prodavnica aplikacija (Apple App Store / Google Play Store), što znači da se izdanja ne mogu trenutno poništiti (rollback) ponovnim raspoređivanjem na serveru. Ispravke zahtevaju slanje novog potpisanog binarnog fajla ili korišćenje funkcionalnih zastavica (feature flags) tokom izvršavanja.
name: Mobile CI/CD Workflow
on:
pull_request:
branches: [main]
push:
tags:
- 'v*.*.*'
jobs:
ci_validation:
name: Lint & Unit Tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run Linter
run: ./gradlew lint
- name: Run Unit Tests
run: ./gradlew testDebugUnitTest
cd_release_ios:
name: Build & Distribute iOS
needs: ci_validation
if: startsWith(github.ref, 'refs/tags/v')
runs-on: macos-14
steps:
- uses: actions/checkout@v4
- name: Install Signing Certificates
run: fastlane match appstore --readonly
- name: Build & Upload to TestFlight
run: fastlane ios beta
3U kros-platformskoj mobilnoj aplikaciji, koja je svrha razdvajanja prezentacionog, domenskog i sloja podataka, i kako ova struktura olakšava deljenje koda između iOS i Android platformi?
Razdvajanje aplikacije na prezentacioni, domenski i sloj podataka uspostavlja jasne granice odgovornosti:
1. **Prezentacioni sloj (Presentation Layer):** Zadužen je za iscrtavanje korisničkog interfejsa, obradu korisničkog unosa i stanje na nivou ekrana (npr. Views, Widgets, ViewModels ili Presenters).
2. **Domenski sloj (Domain Layer):** Enkapsulira čistu poslovnu logiku, domenske modele/entitete i slučajeve upotrebe (use cases). Ostaje nezavisan od korisničkog interfejsa i platformskih okvira.
3. **Sloj podataka (Data Layer):** Upravlja preuzimanjem i perzistencijom podataka sa udaljenih API (Application Programming Interface) servisa, iz lokalnih baza podataka ili keša uređaja putem repozitorijuma i izvora podataka.
U razvoju kros-platformskih mobilnih aplikacija, ova struktura olakšava deljenje koda jer su domenski i sloj podataka agnostični u odnosu na platformu. Ključna poslovna pravila, transformacije podataka i mrežna komunikacija mogu se 100% deliti između iOS i Android platformi, omogućavajući timovima da dele celokupan stek (kao u Flutter ili React Native rešenjima) ili da dele poslovnu logiku i logiku podataka dok zadržavaju izvorne prezentacione slojeve specifične za platformu (kao u Kotlin Multiplatform pristupu).
[Presentation Layer] (UI, Screens, ViewModels)
│
▼ calls
[Domain Layer] (Use Cases, Business Rules, Entities)
▲
│ implements interfaces
[Data Layer] (Repositories, API Clients, Local Storage)
4Koje su osnovne razlike između imperativne i deklarativne paradigme razvoja UI (User Interface) u mobilnom inženjeringu i kako Flutter vidžeti i React Native komponente oslikavaju deklarativni model?
U imperativnom razvoju UI-ja, programeri pišu eksplicitne instrukcije korak po korak kako bi kreirali, menjali i uništavali elemente korisničkog interfejsa (kao što je pronalaženje prikaza po ID-ju i direktno pozivanje metoda poput `setText` ili `setVisibility`). Nasuprot tome, deklarativni UI modeli opisuju kako korisnički interfejs treba da izgleda za dato stanje (često izraženo kao UI = f(state)). Kada se stanje promeni, radni okvir određuje kako da efikasno ažurira vizuelni prikaz. I Flutter i React Native oličavaju ovu deklarativnu paradigmu. U Flutter-u, vidžeti su nepromenljivi opisi konfiguracije; kada se dinamički podaci promene, pozivanje `setState()` u okviru `StatefulWidget`-a zakazuje ponovnu izgradnju gde metoda `build()` vraća novi opis stabla vidžeta koji Flutter usklađuje sa svojim stablima `Element` i `RenderObject`. U React Native-u, komponente su funkcije ili klase koje vraćaju JSX; ažuriranje stanja ili svojstava (`props`) pokreće ponovno renderovanje gde React usklađuje stablo virtuelnih elemenata i primenjuje minimalne neophodne izvorne izmene preko mosta (bridge) ili izvornog izvršnog okruženja.
5Koja je operativna razlika između hladnog (cold start), toplog (warm start) i vrućeg pokretanja (hot start) u mobilnim aplikacijama i zašto je hladno pokretanje najkritičnija metrika za budžete performansi?
U životnom ciklusu mobilnih aplikacija, stanja pokretanja se razlikuju prema tome da li proces aplikacije i njeno stanje u memoriji već postoje u operativnom sistemu.
- Hladno pokretanje (Cold Start): Aplikacija se pokreće od nule jer operativni sistem još nije kreirao njen proces ili je prethodni proces uništen. Operativni sistem mora da alocira novi proces, učita binarne datoteke/izvršno okruženje, inicijalizuje Application/runtime kontekst i prikaže (inflate) prvi prikaz. Ono ima najveće kašnjenje.
- Toplo pokretanje (Warm Start): Proces aplikacije se obično već nalazi u memoriji, ali je hijerarhija aktivnosti/prikaza uništena ili izbačena (npr. zbog ponovnog kreiranja u pozadini ili navigacije unazad). Operativni sistem ponovo kreira korisnički interfejs/aktivnost bez potrebe za pokretanjem novog procesa od nule.
- Vruće pokretanje (Hot Start): Aplikacija i stanje njenog korisničkog interfejsa su i dalje u potpunosti u memoriji (npr. korisnik je pritisnuo dugme Home i odmah se vratio). Operativni sistem samo postavlja postojeću hijerarhiju prikaza u prvi plan uz gotovo nulto vreme inicijalizacije.
Hladno pokretanje je najkritičnija metrika za budžete performansi jer predstavlja najsporije iskustvo prvog utiska za korisnike. Dugo hladno pokretanje direktno dovodi do ranog napuštanja aplikacije, niže stope zadržavanja korisnika i lošijeg rangiranja u prodavnicama aplikacija (kao što su pragovi u okviru Android Vitals).
6Šta je izvorni most (native bridge) u višeplatformskoj mobilnoj aplikaciji i kada biste ga koristili umesto pisanja funkcionalnosti u potpunosti u Flutter ili React Native kodu?
Izvorni most (native bridge) je komunikacioni sloj koji omogućava višeplatformskom kodu da poziva specifičan iOS ili Android kod platforme, kao i da izvorni kod vraća rezultate ili šalje događaje nazad. U Flutter-u se to obično postiže pomoću platformskih kanala (platform channels) ili dodataka (plugins). U React Native-u se to obično radi pomoću izvornih modula (native modules), TurboModules ili izvornih UI komponenti. Most koristite kada je funkcionalnosti potrebno nešto čemu čist Flutter ili React Native kod ne može dovoljno dobro pristupiti, kao što su API platforme, mogućnosti uređaja, izvorni SDK, izvorna implementacija kritična za performanse ili izvorna UI komponenta. Uobičajeni primeri uključuju rad sa kamerom, Bluetooth, push obaveštenja, plaćanja, bezbedno skladištenje podataka, pozadinske servise, zdravstvene API-je ili SDK-ove koji nude samo Swift/Objective-C ili Kotlin/Java integracije. Dobar most obično izlaže mali, jasan API višeplatformskom sloju, kao što su getBatteryLevel, startBluetoothScan ili openNativePaymentSheet, dok implementacija specifična za platformu upravlja stvarnim detaljima za iOS i Android.
7Šta označava „offline-first” arhitektura u razvoju mobilnih aplikacija i po čemu se suštinski razlikuje od standardnog keširanja HTTP (Hypertext Transfer Protocol) odgovora?
U razvoju mobilnih aplikacija, „offline-first” arhitektura tretira lokalno skladište kao primarni izvor istine (source of truth) i za operacije čitanja i za operacije pisanja. Umesto čekanja da se mrežni zahtevi završe pre prikazivanja ili omogućavanja interakcije korisnika, aplikacija direktno komunicira sa lokalnom bazom podataka ili slojem skladišta, dok pozadinski procesi sinhronizacije usklađuju lokalne izmene sa udaljenim serverom kada je mrežna veza dostupna. Ovo se suštinski razlikuje od standardnog keširanja HTTP odgovora na nekoliko načina: 1. **Model podataka**: HTTP keširanje čuva sirove mrežne odgovore (npr. JSON sadržaj) indeksirane prema URL-ovima zahteva i zaglavljima. Offline-first arhitektura čuva strukturisane domenske entitete u lokalnoj bazi podataka (kao što su Room, SQLite ili SwiftData/Core Data). 2. **Pisanje naspram čitanja**: HTTP keširanje je prvenstveno mehanizam za optimizaciju čitanja i ne pruža ugrađenu podršku za offline transakcije ili mutacije pisanja. Offline-first omogućava potpuno lokalno pisanje, stavljajući mutacije u red čekanja za kasniju sinhronizaciju sa serverom. 3. **Mogućnost upita i životni vek**: Keširani HTTP odgovori podležu automatskim pravilima izbacivanja iz keša i ne mogu se proizvoljno pretraživati, filtrirati ili spajati (join). Offline-first perzistencija pruža potpunu podršku za upite, indeksiranje i determinističko upravljanje životnim vekom nezavisno od stanja mreže.
/* Network-First with HTTP Caching */
UI -> Network Request -> [Cache Hit ? Return Cached JSON : Fetch from API -> Save to HTTP Cache] -> UI
// Limitation: Read-only optimization; offline writes fail immediately.
/* Offline-First with Local Source of Truth */
UI <-> Observes Local DB (e.g., Room / Core Data / SQLite)
User Write -> Write to Local DB -> Queue Sync Task
Sync Worker (Background) -> Send queued mutations to Server -> Update Local DB with Server Response
8Koja je razlika između udaljenog push obaveštenja (remote push notification) i lokalnog obaveštenja u mobilnoj aplikaciji?
Glavna razlika između lokalnih i udaljenih push obaveštenja leži u tome gde nastaju i kako se pokreću:
1. Lokalna obaveštenja se u potpunosti kreiraju, raspoređuju i pokreću na uređaju od strane same aplikacije koristeći API-je operativnog sistema (kao što su UserNotifications na iOS-u ili AlarmManager/WorkManager/NotificationManager na Android-u). Ona se pokreću na osnovu uslova na samom uređaju, kao što su određeni datum/vreme, tajmer za odbrojavanje ili geografska granica (geofencing). Pošto se izvršavaju lokalno na sistemskom servisu operativnog sistema (daemon), ne zahtevaju serversku stranu (backend) niti aktivnu internet vezu u trenutku isporuke.
2. Udaljena push obaveštenja (remote push notifications) potiču sa spoljnog aplikativnog servera i prenose se preko interneta putem platformskih push gejtveja (APNs za Apple uređaje, FCM za Android). Gejtvej isporučuje sadržaj servisu na nivou operativnog sistema uređaja, koji budi aplikaciju ili prikazuje obaveštenje. Udaljena obaveštenja su neophodna za spoljne događaje u realnom vremenu, kao što su dolazne poruke u ćaskanju, zahtevi za prijateljstvo ili vanredne vesti.
let content = UNMutableNotificationContent()
content.title = "Workout Reminder"
content.body = "Time for your daily exercise routine."
content.sound = .default
// Triggers locally after 1 hour (3600 seconds) without any server interaction
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 3600, repeats: false)
let request = UNNotificationRequest(identifier: "dailyWorkout", content: content, trigger: trigger)
UNUserNotificationCenter.current().add(request) { error in
if let error = error {
print("Failed to schedule notification: \(error)")
}
}
9Koja je strukturalna i operativna razlika između formata Android App Bundle (.aab), APK (Android Package) paketa i iOS IPA arhive prilikom distribucije aplikacija preko prodavnica aplikacija?
APK (Android Package) je izvršni format paketa koji sadrži kompajlirani DEX bajtkod, resurse, asete i nativne biblioteke koji se mogu direktno instalirati i pokrenuti na Android uređaju. Univerzalni APK sadrži resurse za sve gustine ekrana, CPU (Central Processing Unit) arhitekture i jezike, što dovodi do veće veličine preuzimanja. Android App Bundle (.aab) je format za otpremanje i objavljivanje koji Google Play zahteva za distribuciju. AAB se ne može instalirati direktno na uređaj; umesto toga, Google Play koristi bundle i Play App Signing kako bi dinamički generisao optimizovane razdvojene APK pakete prilagođene specifičnoj arhitekturi uređaja, gustini ekrana i jeziku (Dynamic Delivery). iOS IPA (.ipa) je arhivski fajl aplikacije (zip kontejner koji sadrži Payload direktorijum, `.app` paket, elemente za potpisivanje i metapodatke). Prilikom otpremanja na App Store Connect, Apple primenjuje App Thinning (kao što je App Slicing) kako bi isporučio samo one resurse i binarne fajlove koji su potrebni za konkretan uređaj. Operativno, APK i IPA fajlovi se mogu direktno instalirati na fizičke test uređaje (u zavisnosti od potpisivanja i dodeljivanja profila), dok se AAB prvo mora konvertovati u razdvojene APK pakete (npr. pomoću alata `bundletool`) pre lokalne instalacije na uređaj.
# 1. Standalone APK installs directly via ADB
adb install myapp-universal.apk
# 2. AAB requires bundletool to generate device-tailored split APKs
bundletool build-apks --bundle=myapp.aab --output=myapp.apks --connected-device
# 3. Install the generated split APK set onto the connected device
bundletool install-apks --apks=myapp.apks
10Kakvu zaštitu pruža TLS (Transport Layer Security) za mrežne pozive mobilne aplikacije i kako moderni mobilni operativni sistemi nameću bezbedne podrazumevane postavke prenosa?
Transport Layer Security (TLS) štiti mrežne pozive mobilnih aplikacija pružajući tri osnovne garancije: poverljivost (šifrovanje podataka u prenosu tako da prisluškivači ne mogu čitati sadržaj ili zaglavlja), integritet podataka (otkrivanje bilo kakvih izmena ili neovlašćenih modifikacija zahteva i odgovora tokom prenosa) i autentifikaciju servera (validacija digitalnog sertifikata servera u odnosu na pouzdana sertifikaciona tela radi sprečavanja Man-in-the-Middle napada). Moderni mobilni operativni sistemi podrazumevano nameću bezbedan prenos blokiranjem nešifrovanog HTTP saobraćaja u čistom tekstu (cleartext): 1. iOS nameće App Transport Security (ATS), zahtevajući da mrežne veze (kao što su one putem URLSession) koriste HTTPS sa TLS 1.2+ osim ako izuzeci za domene nisu eksplicitno definisani u Info.plist. 2. Android (API 28+) podrazumevano onemogućava nešifrovani HTTP saobraćaj (`cleartextTrafficPermitted=false`). Dozvoljavanje nešifrovanog HTTP-a zahteva eksplicitne izuzetke putem Network Security Configuration (`network_security_config.xml`) ili manifest atributa `android:usesCleartextTraffic`.
11Kako implementirate biometrijsku autentifikaciju (Face ID / otisak prsta) sa bezbednim skladištem platforme kako biste osigurali da se kriptografski ključevi otključavaju isključivo nakon uspešne biometrijske verifikacije?
Da bi se kriptografski ključevi bezbedno zaštitili biometrijom, aplikacija se ne sme oslanjati samo na logički (boolean) UI povratni poziv iz biometrijskog dijaloga. Umesto toga, kriptografski ključevi moraju biti generisani i uskladišteni unutar hardverski obezbeđenog skladišta (Secure Enclave na iOS-u, Android Keystore / StrongBox na Android-u) konfigurisanog sa polisama kontrole pristupa koje zahtevaju biometrijsku autentifikaciju pre upotrebe ključa.
Na iOS-u, ključevi ili Keychain stavke se konfigurišu pomoću `SecAccessControlCreateWithFlags` sa zastavicama kao što su `.biometryCurrentSet` (ili `.userPresence`). Kada se zatraži privatni ključ za potpisivanje ili dešifrovanje, operativni sistem automatski traži od korisnika Face ID / Touch ID verifikaciju.
Na Android-u, ključevi se generišu pomoću `KeyGenParameterSpec.Builder` uz `.setUserAuthenticationRequired(true)`. Kriptografska operacija (kao što je `Cipher` ili `Signature`) se inicijalizuje i prosleđuje kao `BiometricPrompt.CryptoObject` metodi `BiometricPrompt.authenticate()`. Ključ se otključava i postaje upotrebljiv isključivo unutar `onAuthenticationSucceeded` preko tog autentifikovanog `CryptoObject` objekta.
Korišćenje `.biometryCurrentSet` (iOS) ili `setInvalidatedByBiometricEnrollment(true)` (Android) obezbeđuje da, ukoliko se na uređaju registruje novi otisak prsta ili biometrijski podatak, postojeći ključevi postaju trajno nevažeći, čime se sprečava neovlašćen pristup ako je lozinka uređaja kompromitovana.
val keyGen = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore")
val spec = KeyGenParameterSpec.Builder(
"biometric_auth_key",
KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT
)
.setBlockModes(KeyProperties.BLOCK_MODE_GCM)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
.setUserAuthenticationRequired(true)
.setInvalidatedByBiometricEnrollment(true)
.build()
keyGen.init(spec)
keyGen.generateKey()
12Koje strategije keširanja i optimizacije procesa izvršavanja (pipeline) možete primeniti kako biste značajno smanjili vreme izrade za zahteve za spajanje (pull request) na međuplatformskim mobilnim CI (Continuous Integration) izvršavaocima?
Da bi se drastično smanjilo vreme izrade za zahteve za spajanje koda na mobilnim CI izvršavaocima, optimizacije moraju biti usmerene na razrešavanje zavisnosti, keširanje kompilacije i selektivno izvršavanje.
Prvo, primenite efikasno keširanje zavisnosti na osnovu heš vrednosti zaključanih datoteka (npr. `package-lock.json`, `Podfile.lock`, `gradle/verification-metadata.xml` ili SPM `Package.resolved` datoteka). Ovo sprečava ponovno preuzimanje i razrešavanje spoljnih paketa na čistim instancama izvršavalaca.
Drugo, konfigurišite nativni keš za izradu koda. Za Android, omogućite Gradle Build Cache (`--build-cache`) uz udaljeno HTTP keširanje ili trajno keširanje unutar CI okruženja za `~/.gradle/caches` i direktorijume za izradu. Za iOS, pažljivo keširajte Xcode `DerivedData` i artefakte kompilacije za Swift Package / CocoaPods, ili iskoristite savremene alate za udaljeno keširanje kao što su Tuist ili Bazel. Za React Native/Flutter, keširajte izlaz JS paketa, `node_modules` i keš SDK-a za Flutter engine.
Treće, primenite selektivno izvršavanje poslova (detekcija izmena i analiza uticaja testova). Koristite Git filtere putanja da biste preskočili iOS izradu kada su promenjene samo Android ili backend datoteke, pokrenite brze faze analize koda (linting) i jediničnih testova pre skupih UI testova ili faza kompilacije i izbegavajte generisanje kompletnih produkcionih binarnih datoteka (poput AAB ili univerzalnih IPA paketa) na granama zahteva za spajanje gde su dovoljni samo simulatorski/debug build ili jedinični testovi.
name: PR Fast Check
on:
pull_request:
paths:
- 'android/**'
- 'shared/**'
jobs:
android-unit-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with:
distribution: 'zulu'
java-version: '17'
# Gradle build caching for dependencies & task outputs
- uses: gradle/actions/setup-gradle@v3
with:
cache-read-only: false
- name: Run Unit Tests & Lint Only
run: ./gradlew testDebugUnitTest lintDebug --build-cache --parallel
13Kako biste struktuirali deljenu funkcionalnost tako da prezentacija, poslovni slučajevi upotrebe i pristup podacima ostanu nezavisno testabilni i ponovo iskoristivi na platformama iOS i Android?
Da biste struktuirali deljenu funkcionalnost radi nezavisne testabilnosti i ponovne upotrebljivosti na platformama iOS i Android, primenite čistu, slojevitu arhitekturu organizovanu u slojeve Presentation, Domain i Data:
1. **Sloj domena (čista deljena logika):** Sadrži čiste poslovne entitete, interfejse skladišta (repocitory ugovore) i slučajeve upotrebe/interaktore (npr. `GetCartUseCase`, `ApplyPromoCodeUseCase`). Ovaj sloj nema zavisnosti od UI okruženja (UIKit, SwiftUI, Android Views, Jetpack Compose, Flutter vidžeti) niti platformskih API-ja. Svaki slučaj upotrebe enkapsulira jednu poslovnu operaciju, čineći ga u potpunosti pogodnim za jedinično testiranje pomoću običnih lažnih objekata.
2. **Sloj podataka (pristup podacima i enkapsulacija):** Implementira interfejse skladišta iz domenskog sloja. Komunicira sa udaljenim izvorima podataka (REST/GraphQL) i lokalnim skladištenjem (SQLite/Key-Value). Mrežni DTO (Data Transfer Object) objekti i entiteti baze podataka se strogo preslikavaju u čiste domenske entitete pre nego što napuste ovaj sloj, sprečavajući curenje eksternih šema serijalizacije ili promena u bazi u domenski ili UI sloj.
3. **Prezentacioni sloj (UI i upravljanje stanjem):** Sastoji se od nosilaca stanja (ViewModels, Blocs ili Presenters) i UI prikaza/vidžeta. Nosilac stanja poziva domenske slučajeve upotrebe, upravlja stanjem korisničkog interfejsa (učitavanje, uspeh, greška) i izlaže posmatrano stanje prikazu. Prikaz jednostavno posmatra ovo stanje i iscrtava ga.
Ova struktura omogućava jedinično testiranje slučajeva upotrebe u izolaciji bez UI-ja, pravljenje lažnih skladišta radi testiranja modela prikaza i zamenu implementacija prezentacije između platformi uz ponovno korišćenje celokupne logike domena i podataka.
[ UI View / Compose / SwiftUI ]
|
v (Observes State / Dispatches Events)
[ ViewModel / Presenter / BLoC ] (Presentation Layer)
|
v (Executes pure business operations)
[ Use Case / Interactor ] (Domain Layer - Pure Shared Logic)
|
v (Calls interface contract)
[ Repository Interface ] (Domain Layer Contract)
^
| (Implements)
[ Repository Implementation ] (Data Layer)
|---- Maps NetworkDTO -> DomainEntity
|---- Maps DatabaseEntity -> DomainEntity
[ API Client / Local DB / Storage ]
14Kako procenjujete kada lokalno stanje komponente više nije dovoljno i birate odgovarajući arhitektonski pristup upravljanju stanjem za višeplatformsku bazu koda na kojoj radi više programera?
U višeplatformskom razvoju mobilnih aplikacija, prelazak sa lokalnog stanja komponente (`useState`/`StatefulWidget`) na arhitektonsko rešenje za upravljanje stanjem zavisi od opsega stanja, zahteva životnog veka i mogućnosti testiranja:
1. **Kada lokalno stanje nije dovoljno**:
- **Deljenje kroz više komponenti i prop drilling**: Kada stanju mora da se pristupi ili se mora menjati kroz različite rute navigacije ili udaljene grane stabla vidžeta/komponenti.
- **Očuvanje kroz životni vek**: Kada podaci moraju da prežive zatvaranje ekrana, prelaze između ruta ili resetovanje navigacije (npr. korisnička sesija, sadržaj korpe, keširani fidovi).
- **Razdvajanje odgovornosti i testabilnost**: Kada se poslovna logika, validacija i sporedni efekti isprepliću sa UI komponentama, čineći automatizovano jedinično testiranje bez grafičkog interfejsa (headless) otežanim.
2. **Izbor arhitekture za baze koda sa više programera**:
- **Jednosmerni tok podataka / predvidivi kontejneri (npr. BLoC, Redux, Riverpod)**: Nameću striktno razdvajanje gde UI šalje eksplicitne događaje/akcije i renderuje nepromenljivo stanje koje emituju namenske jedinice poslovne logike. Ovo stvara jasne ugovore, smanjuje sporedne efekte i omogućava nezavisno jedinično testiranje poslovne logike.
- **Atomski / scoped reaktivni magacini stanja (npr. Zustand, MobX, Provider)**: Nude manje šablonskog koda (boilerplate) i fleksibilne pretplate preko selektora, što je pogodno kada timovi žele lagano razdvajanje i ciljano ponovno renderovanje komponenti.
U timskom okruženju, primarni arhitektonski cilj je izolacija čiste poslovne logike od slojeva UI renderovanja i korišćenje selektivnih pretplata kako bi se sprečilo nepotrebno ponovno renderovanje čitavog stabla.
15Kako biste istražili i smanjili seckanje (jank) na ekranu međuplatformske mobilne aplikacije koji koči prilikom skrolovanja dugačke liste slika i dinamičkog sadržaja?
Prvo bih reprodukovao seckanje na stvarnim uređajima i izvršio profilisanje, jer seckanje pri skrolovanju može nastati iz nekoliko razloga: prekoračenje budžeta za frejm, preopterećenje u JS ili Dart kodu, rad na glavnoj/UI niti, rad na GPU/rasterizaciji, raspored elemenata (layout), dekodiranje slika, pritisak na memoriju ili mrežno učitavanje. Na 60 Hz aplikacija ima oko 16,7 ms po frejmu, a na 120 Hz oko 8,3 ms, pa zahtevno renderovanje, dekodiranje, raspoređivanje elemenata ili sinhrona izračunavanja tokom skrolovanja mogu dovesti do propuštenih frejmova. Koristio bih alate kao što su Flutter DevTools, React Native alati za performanse ili Flipper, Android Studio Profiler, Xcode Instruments i vremenske linije frejmova (frame timeline) da bih identifikovao stvarno usko grlo. Za dugačku listu bih obezbedio lenjo (lazy) učitavanje ili virtuelizaciju: na primer, FlatList, FlashList ili RecyclerListView u React Native-u, ili ListView.builder, SliverList ili slične mehanizme u Flutter-u. Koristio bih stabilne ključeve, izbegavao ponovno građenje ili ponovno renderovanje svakog reda kada se promeni stanje roditelja, memoizovao komponente redova ili selektore gde je to prikladno, održavao `build`/`renderItem` efikasnim i izmestio sortiranje, filtriranje, parsiranje JSON-a, formatiranje ili obradu slika van toka skrolovanja. Ako su dimenzije stavki predvidive, definisao bih smernice za raspored kao što je `getItemLayout` u React Native-u ili fiksne dimenzije stavki u Flutter-u. Za slike bih obezbedio sličice (thumbnails) odgovarajuće veličine, keširao ih, izbegavao dekodiranje slika pune rezolucije za male ćelije, koristio privremene oznake (placeholders) i lenjo učitavanje, i pazio na opterećenje memorije usled prevelikog broja velikih bitmapa. Takođe bih pojednostavio prekomerno složene rasporede redova, smanjio zahtevne senke/isecanje (clipping)/prekomerno iscrtavanje (overdraw)/providnost gde je relevantno, grupisao ili paginirao podatke, a zatim ponovo profilisao kako bih potvrdio poboljšanje vremena frejma i smanjenje propuštenih frejmova.
1. Record a trace while scrolling on a real device.
2. Check frame timeline: are frames exceeding 16.7 ms / 8.3 ms?
3. Identify bottleneck: JS/Dart, main/UI thread, raster/GPU, image decode, memory, or network.
4. Fix the largest measured bottleneck: virtualization, image resizing/caching, row memoization, layout simplification, moving work off scroll path.
5. Re-test on low-end and target-refresh-rate devices.
16Kako funkcionišu granice među nitima pri pozivima kroz izvorni most (native bridge) i kako sprečavate zastoje usled prebacivanja niti ili seckanje korisničkog interfejsa (UI - User Interface) kada izvorni moduli izvršavaju zahtevan rad u pozadini?
Okviri za više platformi koriste specifične konvencije za rad sa nitima pri pozivima preko mosta: na primer, standardni Flutter `MethodChannel` pozivi stižu na glavnu UI nit platforme, dok tradicionalni React Native pozivi preko mosta teku na namenskoj JavaScript niti i asinhrono se prosleđuju izvornim nitima. Kada se izvorni metod mosta izvršava na glavnoj/UI niti, pokretanje procesorski zahtevnih operacija, dugog sinhronog I/O rada sa diskom ili blokirajućih mrežnih zadataka blokira glavnu petlju izvršavanja. To dovodi do ispuštanja frejmova, vidljivog seckanja interfejsa, a na Android-u do grešaka tipa ANR (Application Not Responding) ili prinudnog prekida aplikacije od strane iOS watchdog mehanizma. Da bi se to sprečilo, funkcije za obradu na mostu treba da preusmere zahtevan ili blokirajući rad u pozadinske skupove niti (npr. Kotlin Coroutines uz `Dispatchers.IO`/`Dispatchers.Default`, Android `ThreadPoolExecutor` ili Swift `Task.detached` / GCD `DispatchQueue.global()`). Po završetku posla, rezultat se mora proslediti nazad na nit mosta koju radni okvir očekuje (npr. vraćanjem rezultata na UI nit za standardne MethodChannel pozive ili korišćenjem redova za pozadinske zadatke), čime se obezbeđuje da ažuriranja stanja na više platformi ne blokiraju ciklus iscrtavanja interfejsa.
class ImageProcessorPlugin : MethodChannel.MethodCallHandler {
private val scope = CoroutineScope(Dispatchers.Default + SupervisorJob())
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
if (call.method == "applyFilter") {
val imageBytes = call.argument<ByteArray>("image") ?: return result.error("INVALID_ARG", "Image null", null)
// Dispatch heavy computation off the main UI thread
scope.launch {
try {
val processed = processBitmapBytes(imageBytes) // CPU heavy work
withContext(Dispatchers.Main) {
result.success(processed)
}
} catch (e: Exception) {
withContext(Dispatchers.Main) {
result.error("PROCESSING_FAILED", e.localizedMessage, null)
}
}
}
} else {
result.notImplemented()
}
}
}
17Kako biste osmislili strategiju keširanja i invalidacije za ekrane sa listama i fidovima koristeći obrazac Stale-While-Revalidate i uslovna HTTP (Hypertext Transfer Protocol) zaglavlja?
Pouzdana strategija keširanja i invalidacije za ekrane sa fidovima i listama kombinuje obrazac Stale-While-Revalidate (SWR) sa uslovnim HTTP zaglavljima za validaciju (kao što su `ETag` / `If-None-Match` ili `Last-Modified` / `If-Modified-Since`). Kada se ekran otvori, on odmah čita i prikazuje keširane podatke iz lokalne baze podataka (jedinstveni izvor istine, npr. Room ili Core Data/SQLite), pružajući korisniku trenutni prikaz korisničkog interfejsa. Istovremeno se u pozadini šalje mrežni zahtev sa keširanim zaglavljem `If-None-Match: <etag>`. Ako server odgovori sa `304 Not Modified`, telo odgovora se ne prenosi, čime se potvrđuje lokalni keš i štede protok i baterija. Ako server odgovori sa `200 OK`, novi podaci i ažurirani `ETag` se upisuju u lokalnu bazu podataka u okviru transakcije, a reaktivni posmatrači baze automatski šalju ažuriranu listu korisničkom interfejsu. Za paginaciju, tokeni stranica ili pomeraji čuvaju se zajedno sa keširanim zapisima. Invalidacija se pokreće istekom TTL-a, korisničkim akcijama (kao što je povlačenje radi osvežavanja koje zaobilazi uslovna zaglavlja) ili sporednim efektima lokalnih promena (npr. kreiranje ili brisanje stavke lokalno optimistički ažurira bazu podataka i označava keširane kursore stranica za ponovnu validaciju).
fun getFeed(): Flow<List<FeedItem>> = flow {
val cached = feedDao.getFeedItems()
if (cached.isNotEmpty()) emit(cached)
val lastEtag = feedDao.getFeedEtag()
try {
val response = api.fetchFeed(ifNoneMatch = lastEtag)
if (response.code() == 200 && response.body() != null) {
feedDao.updateFeedTransaction(response.body()!!, response.headers()["ETag"])
emit(feedDao.getFeedItems())
}
} catch (e: Exception) {
if (cached.isEmpty()) throw e
}
}
18Finansijska mobilna aplikacija mora bezbedno da čuva tokene za osvežavanje (refresh tokens) na uređaju, podržava biometrijsko otključavanje, očuva funkcionalnost nakon nadogradnje OS (Operating System) i radi bezbedno tokom privremenih sesija bez pristupa mreži. Kako biste projektovali ovu arhitekturu za skladištenje i pristup tokenima?
Pouzdana arhitektura za skladištenje mobilnih tokena koristi hardverski podržane bezbednosne module: iOS Keychain podržan modulom Secure Enclave i Android Keystore podržan mehanizmom StrongBox ili izolovanim izvršnim okruženjem TEE (Trusted Execution Environment). Tokeni za osvežavanje moraju biti šifrovani u stanju mirovanja pomoću ključeva zaštićenih biometrijskom kriptografskom kontrolom pristupa (kao što su `kSecAccessControlBiometryCurrentSet` ili `kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly` na platformi iOS i `setUserAuthenticationRequired(true)` sa `AUTH_BIOMETRIC_STRONG` na platformi Android). Kriptografska kontrola pristupa garantuje da hardver odmotava kriptografski ključ samo nakon uspešne biometrijske autentifikacije, umesto da se oslanja na promenljivu logičku (boolean) proveru u kodu aplikacije koju je moguće zaobići. Da bi se izdržale nadogradnje operativnog sistema i istovremeno sprečio neovlašćeni pristup, ključevi se vezuju za hardver uređaja uz primenu pravila o promeni registracije koja poništavaju ključ ili zahtevaju ponovnu autentifikaciju ako se registruju novi biometrijski podaci (npr. `setInvalidatedByBiometricEnrollment(true)` na Androidu). Za privremene oflajn sesije, kratkotrajni šifrovani pristupni tokeni i ograničena lokalna stanja oflajn sesije mogu funkcionisati unutar definisanih lokalnih TTL vrednosti i ograničenih privilegija, dok se privilegovane radnje osvežavanja odlažu dok se mrežna veza ne uspostavi. Nakon ponovnog povezivanja, rotacija tokena za osvežavanje sa detekcijom jednokratne ponovne upotrebe na backendu validira sesiju; ako se otkrije ponovno korišćenje tokena ili opoziv naloga, backend vraća signal za poništavanje koji navodi klijenta da obriše hardverski zaštićene ključeve i očisti lokalno skladište.
val keyStore = KeyStore.getInstance("AndroidKeyStore").apply { load(null) }
val keyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore")
val spec = KeyGenParameterSpec.Builder(
"refreshTokenKeyAlias",
KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT
)
.setBlockModes(KeyProperties.BLOCK_MODE_GCM)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
.setUserAuthenticationRequired(true)
.setUserAuthenticationParameters(0, KeyProperties.AUTH_BIOMETRIC_STRONG)
.setInvalidatedByBiometricEnrollment(true)
.build()
keyGenerator.init(spec)
keyGenerator.generateKey()
19Kako biste projektovali skalabilnu CI/CD (Continuous Integration / Continuous Deployment) infrastrukturu za višeplatformski mobilni monorepo koji podržava više timova, uz upravljanje ograničenim i skupim kapacitetima macOS izvršilaca?
Da bi se projektovala skalabilna CI/CD infrastruktura za višeplatformski mobilni monorepo uz optimizaciju oskudnih i skupih macOS kapaciteta, arhitektura se mora oslanjati na prepoznavanje izmenjenih delova koda na osnovu grafa zavisnosti, strogu podelu poslova između hibridnih grupa izvršilaca i virtuelizaciju odnosno efemernu orkestraciju za macOS. Prvo, implementirajte alat za graf izgradnje u monorepou (kao što su Bazel, Nx ili Turborepo) sa distribuiranim udaljenim keširanjem. Prilikom svakog pull request-a, CI pajplajn izračunava razliku u usmerenom acikličnom grafu (DAG) u odnosu na ciljnu granu, kako bi gradio i testirao samo obuhvaćene pakete i njihove zavisnosti, potpuno preskačući nepromenjene module. Drugo, primenite hibridnu strategiju alokacije izvršilaca: sve zadatke koji striktno ne zahtevaju Xcode (kao što su linting i kompilacija za TypeScript/Dart, jedinični testovi, statička analiza koda, bezbednosno skeniranje i Android Gradle izgradnja) prebacite na isplativije, skalabilne Linux/Kubernetes izvršioce. macOS izvršioce rezervišite isključivo za završno sklapanje iOS paketa, kompilaciju za Swift/Objective-C, potpisivanje koda i izvršavanje testova na iOS simulatoru. Treće, upravljajte macOS izvršiocima pomoću infrastrukture za virtuelizaciju (kao što su Tart, Anka ili AWS/MacStadium namenski hardverski čvorovi orkestrirani preko alata Nomad ili Kubernetes). Svaka iOS izgradnja se pokreće u čistoj, efemernoj virtuelnoj mašini sa unapred pripremljenim alatima i kešom izvedenih podataka (derived data), što sprečava neusklađenost konfiguracije izvršilaca i omogućava brzo automatsko skaliranje na osnovu dužine reda u pajplajnu.
20Kako biste projektovali višeplatformsku (cross-platform) mobilnu arhitekturu za proizvod od koga se očekuje da deli većinu poslovne logike između iOS i Android platformi, dok istovremeno omogućava da se UI (User Interface), navigacija i nativne integracije specifične za platformu razvijaju nezavisno?
Projektovao bih aplikaciju oko deljenog jezgra koje poseduje stabilno poslovno ponašanje: domenske modele, validaciju, slučajeve upotrebe (use cases), poslovna pravila i ugovore za pristup podacima. Korisnički interfejs specifičan za platformu, navigacija, nativni SDK-ovi, dozvole, upravljanje životnim vekom i opšte komponente aplikacije (app shell) treba da ostanu van tog jezgra. Deljeni kod ne bi trebalo da uvozi UIKit, SwiftUI, Jetpack, API-je radnog okvira Android, navigaciju za React Native, navigaciju za Flutter niti detalje nativnog SDK-a. Umesto toga, trebalo bi da zavisi od uskih interfejsa kao što su AuthRepository, SecureStorage, Analytics, CameraService ili PaymentProvider koje svaka platforma zasebno implementira. Organizovao bih kod po funkcionalnostima što je više moguće, umesto kreiranja jednog velikog deljenog sloja. Na primer, naplata, pretraga, nalog i razmena poruka mogu imati sopstveni deljeni kod domena/slučajeva upotrebe, ugovore o stanju i interfejse repozitorijuma. Zatim iOS i Android omotači upravljaju kompozicijom ekrana, nativnim šablonima korisničkog interfejsa, stekovima za navigaciju, dozvolama i povezivanjem adaptera. Prezentaciona logika se može deliti kada je potpuno nezavisna od UI okvira, kao što su reducer-i, mašine stanja ili ugovori za view-model koji emituju jednostavno stanje i akcije, ali stvarni prikazi i navigacija treba da ostanu u vlasništvu pojedinačne platforme kako bi svaka platforma mogla nezavisno da evoluira. Glavni kompromis je u tome da se agresivno deli uobičajena, stabilna poslovna logika, uz izbegavanje apstrakcija koje skrivaju stvarne razlike među platformama. Granice bih nametnuo pomoću pravila o zavisnostima modula, ubrizgavanjem zavisnosti, javnim API-jima, testovima ugovora, proverama arhitekture i jasnim vlasništvom. Nativne integracije bi trebalo da koriste šablon portova i adaptera (ports and adapters), tako da deljeni kod funkcionalnosti vidi zajedničku mogućnost, dok svaka platforma obrađuje ponašanje SDK-a, životni vek, dozvole, greške i UX razlike u sopstvenom sloju.