Priprema za Android Developer intervju

Pitanja za Android intervju

20 često postavljanih pitanja za Android Developer intervju. Pitanja pokrivaju različite nivoe, a odgovore možete vežbati naglas u našem treneru za intervjue.

Započni Android Developer AI intervjuKreditna kartica nije potrebna. Dostupna je 1 besplatna sesija.
Vežba tehničkog intervjua na engleskomRežim u kome kandidati kojima engleski nije maternji mogu da vežbaju tehničke intervjue.

Pitanja za početni nivo

1Prođite kroz osnovne povratne pozive životnog veka Activity komponente od kreiranja do uništavanja i objasnite razlike između onPause(), onStop() i onDestroy() u pogledu oslobađanja resursa.

Jedna Activity komponenta prolazi kroz šest osnovnih povratnih poziva životnog veka: onCreate(), onStart(), onResume(), onPause(), onStop() i onDestroy(). onCreate() vrši jednokratnu inicijalizaciju kao što je raspakivanje prikaza (view inflation). onStart() čini aktivnost vidljivom, a onResume() je dovodi u prvi plan gde ima fokus za interakciju. Prilikom napuštanja ekrana, onPause() označava da je aktivnost izgubila fokus, onStop() označava da više nije vidljiva na ekranu, a onDestroy() označava konačno gašenje instance aktivnosti. Što se tiče oslobađanja resursa: - onPause(): Pauzirajte samo brze operacije specifične za rad u prvom planu (kao što su pauziranje UI animacija ili lagani pregled sa kamere) jer izvršavanje koda u onPause() direktno blokira pokretanje sledeće aktivnosti. - onStop(): Ovo je glavno mesto za oslobađanje težih resursa vezanih za vidljivost korisničkog interfejsa (kao što su GPS/lokacijski slušaoci, tokovi sa senzora, reprodukcija medija ili periodično osvežavanje mreže). Pošto je aktivnost potpuno nevidljiva, njihovo držanje aktivnim troši bateriju, a sistem može ugasiti pozadinski proces bez daljeg obaveštenja. - onDestroy(): Koristi se za konačno čišćenje instance Activity komponente (kao što je čišćenje lokalnih niti ili referenci). Međutim, oslobađanje kritičnih resursa ne treba ostavljati za onDestroy() jer Android može direktno ubiti zaustavljeni proces aplikacije radi oslobađanja memorije bez prethodnog izvršavanja onDestroy().

class LocationActivity : AppCompatActivity() {
    private var locationClient: LocationClient? = null

    override fun onStart() {
        super.onStart()
        // Start listening when visible to the user
        locationClient?.startLocationUpdates()
    }

    override fun onStop() {
        super.onStop()
        // Release when invisible to save battery and prevent leaks on process kill
        locationClient?.stopLocationUpdates()
    }
}
Pokušajte da odgovorite na ovo pitanje sa AI trenerom

2Koje su četiri primarne komponente Android aplikacije i kakav je životni vek i model niti njihovih podrazumevanih povratnih poziva?

Četiri primarne komponente Android aplikacije su Activity, Service, BroadcastReceiver i ContentProvider. Activity obezbeđuje korisnički interfejs za interakciju na ekranu. Service izvršava pozadinsku obradu bez namenskog korisničkog interfejsa. BroadcastReceiver osluškuje i reaguje na sistemska ili aplikativna emitovanja obaveštenja (broadcasts). ContentProvider upravlja strukturiranim podacima i izlaže ih drugim aplikacijama ili internim modulima. Podrazumevano, primarni povratni pozivi životnog veka za Activity (kao što su `onCreate`, `onStart`, `onResume`), Service (kao što su `onCreate`, `onStartCommand`, `onBind`) i BroadcastReceiver (`onReceive`) izvršavaju se sinhrono na glavnoj niti aplikacije (UI nit). Metode ContentProvider-a (kao što je `onCreate`) takođe se inicijalizuju na glavnoj niti, mada se operacije upita i unosa (`query`/`insert`) pozvane između procesa izvršavaju na nitima iz Binder skupa niti (thread pool). Pošto se podrazumevani povratni pozivi izvršavaju na glavnoj niti, pokretanje teških blokirajućih operacija direktno u njima (poput mrežnih zahteva ili obimnih ulazno-izlaznih operacija sa bazom podataka na disku) blokira petlju korisničkog interfejsa i izaziva grešku da aplikacija ne reaguje (ANR – Application Not Responding).

class DataSyncService : Service() {
    private val serviceScope = CoroutineScope(Dispatchers.IO + SupervisorJob())

    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
        // onStartCommand runs on the Main Thread; heavy work must be offloaded
        serviceScope.launch {
            performDiskAndNetworkSync()
            stopSelf(startId)
        }
        return START_NOT_STICKY
    }

    override fun onBind(intent: Intent?): IBinder? = null

    override fun onDestroy() {
        super.onDestroy()
        serviceScope.cancel()
    }

    private fun performDiskAndNetworkSync() {
        // Blocking I/O safely executed on Dispatchers.IO
    }
}
Pokušajte da odgovorite na ovo pitanje sa AI trenerom

3U Android MVVM (Model-View-ViewModel) arhitekturi, koje su primarne odgovornosti ViewModel komponente u poređenju sa UI (User Interface) kontrolerima kao što su Activity i Fragment, i zašto se prosleđivanje Context objekta u ViewModel ne preporučuje?

U Android MVVM arhitekturi, ViewModel je odgovoran za čuvanje i upravljanje stanjem vezanim za korisnički interfejs, orkestriranje prezentacione logike i preživljavanje promena konfiguracije (kao što je rotacija ekrana). Nasuprot tome, UI kontroleri (Activity i Fragment) odgovorni su za iscrtavanje elemenata korisničkog interfejsa na ekranu, praćenje promena stanja, obradu direktnih korisničkih interakcija (klikovi, gestovi) i upravljanje događajima životnog veka Android komponenti. Prosleđivanje reference na Activity Context ili View u ViewModel strogo se ne preporučuje jer ViewModel obično nadživljava životni vek UI kontrolera tokom promena konfiguracije. Kada se Activity uništi i ponovo kreira tokom rotacije, ViewModel koji drži referencu na njegov Context sprečava GC (Garbage Collector) da oslobodi stari Activity, što dovodi do curenja memorije (memory leak). Ako je Android Context neophodan za operacije na nivou sistema, umesto toga treba koristiti Context na nivou aplikacije (kao što je putem `AndroidViewModel` klase ili ubacivanjem ApplicationContext zavisnosti).

// Bad: Storing Activity context or View reference causes leaks
class LeakyViewModel(private val activityContext: Context) : ViewModel() {
    fun showToast() { Toast.makeText(activityContext, "Hello", Toast.LENGTH_SHORT).show() }
}

// Good: ViewModel manages state; Activity observes and handles context-specific tasks
class SafeViewModel(private val repository: UserRepository) : ViewModel() {
    private val _uiState = MutableStateFlow<UserUiState>(UserUiState.Loading)
    val uiState: StateFlow<UserUiState> = _uiState.asStateFlow()
    
    fun loadUser(id: String) {
        viewModelScope.launch {
            _uiState.value = UserUiState.Success(repository.getUser(id))
        }
    }
}
Pokušajte da odgovorite na ovo pitanje sa AI trenerom

4Kako se razlikuju uobičajene i opasne dozvole na Android platformi i šta se dešava kada aplikacija zatraži opasnu dozvolu tokom izvršavanja?

Uobičajene dozvole (normal permissions) predstavljaju minimalan rizik po privatnost korisnika ili rad uređaja i sistem ih automatski odobrava prilikom instalacije aplikacije kada su deklarisane u `AndroidManifest.xml` datoteci (kao što je `ACCESS_NETWORK_STATE`). Opasne dozvole (koje se nazivaju i dozvole tokom izvršavanja – runtime permissions) kontrolišu pristup privatnim podacima korisnika ili ograničenim mogućnostima uređaja, kao što su kamera, mikrofon, lokacija i kontakti. Kada aplikacija zatraži opasnu dozvolu tokom izvršavanja, Android sistem prikazuje sistemski dijalog koji od korisnika traži da dozvoli ili odbije pristup. Ako korisnik odobri dozvolu, aplikacija izvršava traženu funkciju. Ako korisnik odbije dozvolu, aplikacija dobija povratni poziv o odbijanju i mora elegantno da obradi tu situaciju onemogućavanjem ili prilagođavanjem zavisne funkcije bez rušenja aplikacije, uz opcionalno prikazivanje korisničkog interfejsa sa obrazloženjem.

val requestPermissionLauncher = registerForActivityResult(
    ActivityResultContracts.RequestPermission()
) { isGranted: Boolean ->
    if (isGranted) {
        accessCameraFeature()
    } else {
        showPermissionDeniedExplanation()
    }
}

fun checkAndRequestPermission() {
    when {
        ContextCompat.checkSelfPermission(context, Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED -> {
            accessCameraFeature()
        }
        shouldShowRequestPermissionRationale(Manifest.permission.CAMERA) -> {
            showRationaleDialog { requestPermissionLauncher.launch(Manifest.permission.CAMERA) }
        }
        else -> {
            requestPermissionLauncher.launch(Manifest.permission.CAMERA)
        }
    }
}
Pokušajte da odgovorite na ovo pitanje sa AI trenerom

5Koja je razlika između debug i release Android verzija (builds), i kako se tipovi izgradnje (build types) i varijacije proizvoda (product flavors) kombinuju u varijante izgradnje (build variants)?

U razvoju za Android, tipovi izgradnje (build types) konfigurišu svojstva pakovanja i izvršavanja za različite faze razvoja. Debug verzije omogućavaju indikatore za otklanjanje grešaka (`isDebuggable = true`), koriste podrazumevani debug keystore i tipično isključuju minimizaciju i skraćivanje koda (R8/ProGuard) radi bržih iteracija izgradnje. Release verzije isključuju indikatore za otklanjanje grešaka, omogućavaju optimizacije i obfuskaciju, i zahtevaju bezbedan ključ za potpisivanje (release signing key) radi distribucije. Varijacije proizvoda (product flavors) predstavljaju različite verzije ili ciljne postavke aplikacije koje dele istu osnovnu bazu koda, kao što su različita okruženja (staging naspram production) ili izdanja proizvoda (besplatna naspram plaćene verzije). U alatu Gradle, varijante izgradnje (build variants) formiraju se Dekartovim proizvodom varijacija proizvoda i tipova izgradnje (Build Variant = Product Flavor × Build Type). Na primer, kombinovanjem varijacija `staging` i `production` sa tipovima izgradnje `debug` i `release` dobijaju se četiri varijante izgradnje: `stagingDebug`, `stagingRelease`, `productionDebug` i `productionRelease`.

android {
    flavorDimensions += "environment"
    productFlavors {
        create("staging") {
            dimension = "environment"
            applicationIdSuffix = ".staging"
        }
        create("production") {
            dimension = "environment"
        }
    }
    buildTypes {
        getByName("debug") {
            isDebuggable = true
        }
        getByName("release") {
            isMinifyEnabled = true
            isShrinkResources = true
            proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
        }
    }
}
Pokušajte da odgovorite na ovo pitanje sa AI trenerom

6Šta je korutina u jeziku Kotlin i kako se suspenzija razlikuje od blokiranja niti tokom izvršavanja na platformi Android?

Korutina u jeziku Kotlin je lagana apstrakcija konkurentnosti koja omogućava pisanje asinhronog, neblokirajućeg koda na sekvencijalan način. Više korutina može se izvršavati konkurentno na jednoj niti ili preko skupa niti bez velikog utroška memorije i opterećenja promene konteksta karakterističnog za niti na nivou operativnog sistema. Suštinska razlika između suspenzije i blokiranja je u iskorišćenosti niti: - Blokiranje (npr. `Thread.sleep()` ili sinhroni ulaz/izlaz na disku ili mreži) zaustavlja osnovnu nit i čini je nedostupnom za bilo koji drugi rad. Na glavnoj niti Android aplikacije, blokiranje zamrzava korisnički interfejs i izaziva greške tipa ANR (Application Not Responding). - Suspenzija (putem suspendujućih funkcija kao što je `delay()`) pauzira korutinu bez zaustavljanja osnovne niti. Korutina čuva svoje stanje izvršavanja u objektu `Continuation` i prepušta nit izvršnom okruženju kako bi ono moglo da obavlja druge poslove (kao što je obrada interakcija u korisničkom interfejsu). Kada se asinhrona operacija završi, izvršavanje korutine se nastavlja.

suspend fun fetchUserData(): String {
    delay(1000) // Suspends execution and yields the thread
    return "User Data"
}

fun fetchUserDataBlocking(): String {
    Thread.sleep(1000) // Blocks and locks the underlying thread
    return "User Data"
}
Pokušajte da odgovorite na ovo pitanje sa AI trenerom

7U radnom okviru Jetpack Compose, koja je razlika između stanja sačuvanog pomoću remember i stanja sačuvanog pomoću rememberSaveable, i kako mutableStateOf pokreće ažuriranja UI (User Interface) kroz rekompozicije?

U Jetpack Compose-u, `remember` čuva objekat u memoriji kroz rekompozicije sve dok se komponenta koja se može komponovati (composable) nalazi u stablu kompozicije. Međutim, stanje koje se čuva samo pomoću `remember` gubi se tokom promena konfiguracije (kao što je rotacija ekrana) i prekida procesa od strane sistema. Nasuprot tome, `rememberSaveable` čuva stanje i tokom promena konfiguracije i prekida procesa automatskim čuvanjem i obnavljanjem vrednosti putem Android mehanizma Saved Instance State (ili preko prilagođenih implementacija `Saver` interfejsa za nepodržane tipove podataka). `mutableStateOf` obmotava vrednost u opservabilni `MutableState` objekat koji podržava sistem stanja snimaka (Snapshot) u Compose-u. Kada komponenta pročita vrednost `MutableState` tokom kompozicije, Compose beleži tu operaciju čitanja. Kada se vrednost kasnije promeni, Compose automatski označava sve komponente koje su pročitale tu vrednost kao nevažeće i raspoređuje ih za ponovnu kompoziciju, obezbeđujući da UI ostane usklađen sa ažuriranim stanjem.

@Composable
fun CounterExample() {
    // Resets to 0 on screen rotation
    var localCount by remember { mutableStateOf(0) }

    // Persists across screen rotation and process death
    var persistentCount by rememberSaveable { mutableStateOf(0) }

    Column {
        Button(onClick = { localCount++; persistentCount++ }) {
            Text("Local: $localCount | Saved: $persistentCount")
        }
    }
}
Pokušajte da odgovorite na ovo pitanje sa AI trenerom

8Kako Kotlin bezbednost u radu sa null vrednostima (null safety) pomaže u sprečavanju rušenja Android aplikacija i kada bi trebalo koristiti tip koji prihvata null (nullable) u odnosu na tip koji ne prihvata null (non-null)?

Kotlin bezbednost u radu sa null vrednostima pomaže u sprečavanju mnogih rušenja Android aplikacija tako što provere da li je vrednost null čini delom sistema tipova. Vrednost deklarisana kao `String` tretira se kao ne-null (non-null), pa kompajler dozvoljava direktan pristup poput `name.length`. Vrednost deklarisana kao `String?` može biti null, pa vas Kotlin primorava da obradite taj slučaj pre upotrebe, što smanjuje mogućnost slučajnih `NullPointerException` izuzetaka usled opcionih Intent extras podataka, odgovora sa API (Application Programming Interface) servisa, Bundle argumenata ili Java/platformskih API-ja. Koristite ne-null tip kada je vrednost obavezna da bi objekat ili funkcija bili validni. Koristite nullable tip kada je odsustvo vrednosti realno očekivano stanje, kao što je opcioni URL profilne slike ili polje koje nedostaje u serverskom odgovoru. Vrednosti koje mogu biti null obično se obrađuju bezbednim pozivima poput `user?.name`, Elvis operatorom poput `user?.name ?: "Guest"` ili eksplicitnim proverama na null. Operator `!!` isključuje bezbednosne provere i izbacuje izuzetak ako je vrednost null, pa ga treba retko koristiti i to samo kada programer može zaista dokazati da vrednost ne može biti null.

val displayName: String? = intent.getStringExtra("display_name")

val greeting = "Hello, ${displayName ?: "Guest"}"
val lengthText = displayName?.length?.toString() ?: "No name provided"

// Risky: crashes if displayName is null
val riskyLength = displayName!!.length
Pokušajte da odgovorite na ovo pitanje sa AI trenerom

9Koja je suštinska razlika između prosleđivanja Activity Context-a i Application Context-a dugovečnim objektima i kako izbor pogrešnog konteksta dovodi do curenja memorije?

Activity Context je direktno vezan za životni vek određenog ekrana i UI hijerarhije, dok je Application Context vezan za životni vek celokupnog procesa aplikacije. Kada se Activity završi ili ponovo kreira (npr. tokom promene konfiguracije kao što je rotacija ekrana), njegov životni vek se završava i GC (Garbage Collector) treba da oslobodi njegovu memoriju. Ako prosledite Activity Context dugovečnom objektu — kao što je singleton, statičko polje ili dugotrajna pozadinska nit — taj dugovečni objekat zadržava jaku referencu ka aktivnosti. Pošto dugovečni objekti deluju kao GC koreni (GC roots) ili su dostupni iz njih, sakupljač smeća ne može da oslobodi uništeni Activity. Time se zadržava ne samo instanca aktivnosti već i celokupna povezana hijerarhija View elemenata, crteži (drawables) i resursi, što dovodi do značajnog curenja memorije. Nasuprot tome, prosleđivanje `applicationContext` je bezbedno za dugovečne komponente jer se očekuje da se njegov životni vek poklapa sa životnim vekom samog procesa.

class LocationRepository private constructor(private val context: Context) {
    companion object {
        @Volatile
        private var INSTANCE: LocationRepository? = null

        fun getInstance(context: Context): LocationRepository {
            return INSTANCE ?: synchronized(this) {
                // SAFE: Using context.applicationContext prevents retaining an Activity reference
                INSTANCE ?: LocationRepository(context.applicationContext).also { INSTANCE = it }
            }
        }
    }
}
Pokušajte da odgovorite na ovo pitanje sa AI trenerom

10Koja je razlika između ključa za otpremanje i ključa za potpisivanje aplikacije kada se koristi Google Play App Signing?

U okviru Google Play App Signing mehanizma, ključ za otpremanje (upload key) i ključ za potpisivanje aplikacije (app signing key) imaju dve različite bezbednosne uloge. Ključ za otpremanje zadržava programer kako bi potpisao artefakt izgradnje (AAB) pre otpremanja na Google Play Console, čime potvrđuje svoj identitet Google-u. Ključ za potpisivanje aplikacije se bezbedno čuva unutar Google-ove infrastrukture u oblaku i Google Play ga koristi za potpisivanje generisanih APK paketa koji se isporučuju i instaliraju na uređaje krajnjih korisnika, čime se uspostavlja trajni kriptografski identitet aplikacije na Android-u. Glavna operativna prednost je oporavak ključa: ako programer izgubi ili kompromituje svoj ključ za otpremanje, Google Play podrška može da ga resetuje nakon verifikacije identiteta programera, bez narušavanja kontinuiteta ažuriranja aplikacije. Prema ranijem modelu gde su programeri direktno držali ključ za potpisivanje aplikacije, gubitak ključa je značio da se aplikacija više nikada ne može ažurirati.

android {
    signingConfigs {
        create("release") {
            storeFile file("my-upload-key.jks")
            storePassword System.getenv("KEYSTORE_PASSWORD")
            keyAlias "upload-alias"
            keyPassword System.getenv("KEY_PASSWORD")
        }
    }
    buildTypes {
        release {
            signingConfig signingConfigs.getByName("release")
            minifyEnabled true
        }
    }
}
Pokušajte da odgovorite na ovo pitanje sa AI trenerom

Pitanja za srednji nivo

11Ekran gubi podatke koje je korisnik uneo u formu nakon rotacije uređaja. Kako biste sistematski dijagnostikovali da li je problem u nedostatku sačuvanog stanja, nepravilnom opsegu vidljivosti ViewModel komponente ili ponovnom povezivanju prikaza?

Da biste sistematski dijagnostikovali zašto se podaci iz forme gube nakon rotacije uređaja, izolujte tri ključne oblasti: 1. **Opseg i očuvanje ViewModel-a:** Proverite da li se instanca ViewModel-a zaista čuva tokom promene konfiguracije umesto da se ponovo kreira. Pratite heš kod ViewModel-a (`viewModel.hashCode()`) tokom rotacija. Uverite se da se dobija putem delegiranih svojstava kao što su `by viewModels()` / `by activityViewModels()` ili preko `ViewModelProvider(this)`, a ne direktnim pozivom konstruktora (npr. `MyViewModel()`). 2. **Sačuvano stanje i obnova putem identifikatora prikaza:** Proverite da li se forma oslanja isključivo na podrazumevani Android mehanizam za obnovu hijerarhije prikaza. Prikazi moraju imati definisan atribut `android:id` u rasporedu (layout) kako bi učestvovali u automatskom čuvanju i obnavljanju stanja prikaza. Ako koristite prilagođene prikaze ili `SavedStateHandle`, proverite da li `onSaveInstanceState` / `SavedStateHandle` zaista čuvaju i obnavljaju relevantna polja. 3. **Ponovno povezivanje prikaza i logika posmatrača:** Pregledajte `onViewCreated` ili pretplate na posmatrače (`StateFlow`, `LiveData`). Proverite da li se novokreirani prikazi ispravno ponovo pretplaćuju ili kod za podešavanje prikaza slučajno prepisuje obnovljeni tekst podrazumevanim praznim vrednostima. Pored toga, proverite da li postoje petlje kod dvosmernog povezivanja (two-way binding) ili u `TextWatcher`-u gde prazan obnovljeni prikaz odmah šalje prazno ažuriranje nazad u ViewModel.

class FormFragment : Fragment(R.layout.fragment_form) {
    // 1. Ensure correct lifecycle scoping
    private val viewModel: FormViewModel by viewModels()

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        Log.d("FormDiag", "VM instance hash: ${viewModel.hashCode()}")

        // 2. Observe state without clobbering input during view recreation
        viewLifecycleOwner.lifecycleScope.launch {
            viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
                viewModel.formText.collect { text ->
                    if (binding.inputField.text.toString() != text) {
                        binding.inputField.setText(text)
                    }
                }
            }
        }
    }
}
Pokušajte da odgovorite na ovo pitanje sa AI trenerom

12Kako moderni Jetpack Activity Result API (Application Programming Interface) zamenjuje zastareli mehanizam startActivityForResult i kako štiti od gubitka povratnog poziva (callback) tokom ponovnog kreiranja Activity-ja?

Moderni Jetpack Activity Result API zamenjuje zastarele metode `startActivityForResult()` i `onActivityResult()` tipski bezbednim i razdvojenim mehanizmom zasnovanim na ugovorima (contracts). Umesto upravljanja proizvoljnim celobrojnim kodovima zahteva i glomaznim `switch` strukturama unutar `onActivityResult`, pozivaoci definišu `ActivityResultContract<I, O>` (koji specificira parametre ulaznog intent-a i očekivani obrađeni izlaz) i registruju tipizirani povratni poziv pomoću `registerForActivityResult()` kako bi dobili `ActivityResultLauncher`. Ovaj API štiti od gubitka povratnog poziva tokom ponovnog kreiranja Activity-ja ili prekida procesa putem striktne registracije vezane za životni vek komponente: 1. Kada se pokrene spoljni Activity (kao što je kamera ili birač dokumenata), operativni sistem može uništiti pozivajući Activity usled manjka memorije ili promene konfiguracije. 2. Zahtevanjem da se `registerForActivityResult()` pozove bezuslovno pre nego što Activity ili Fragment dospe u stanje `STARTED` (obično kao inicijalizator svojstva ili unutar `onCreate`), povratni poziv rezultata se registruje u `ActivityResultRegistry` pre nego što dođe do restauracije stanja. 3. Kada se pokrenuti Activity završi i matični Activity ponovo kreira, `ActivityResultRegistry` povezuje rezultat koji čeka u sistemu sa novo-registrovanim povratnim pozivom i bezbedno ga isporučuje.

class ProfileActivity : AppCompatActivity() {
    // Registered unconditionally during initialization / before onStart()
    private val takePictureLauncher = registerForActivityResult(
        ActivityResultContracts.TakePicturePreview()
    ) { bitmap: Bitmap? ->
        bitmap?.let {
            findViewById<ImageView>(R.id.avatarImage).setImageBitmap(it)
        }
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_profile)

        findViewById<Button>(R.id.btnCapture).setOnClickListener {
            takePictureLauncher.launch(null)
        }
    }
}
Pokušajte da odgovorite na ovo pitanje sa AI trenerom

13Uporedite MVVM (Model-View-ViewModel) i MVI (Model-View-Intent) arhitekture za kompleksan ekran: kako reduktori stanja i slanje namera u MVI garantuju atomska ažuriranja stanja u poređenju sa MVVM?

U tradicionalnom MVVM-u, ViewModel često izlaže više nezavisnih reaktivnih tokova (kao što su `isLoading`, `userData`, `errorMessage` kao zasebna `StateFlow` ili `LiveData` svojstva) ili više javnih metoda za ažuriranje. Kada se više asinhronih zadataka završi konkurentno, ovi tokovi se mogu ažurirati nezavisno, što dovodi do trkačkih uslova (race conditions), međufaza sa nepotpunim stanjima ili vizuelnog treperenja. U MVI (Model-View-Intent) arhitekturi, upravljanje stanjem prati strogi jednosmerni tok podataka izgrađen oko tri elementa: 1. Jedinstveno nepromenljivo stanje `UiState` koje predstavlja celokupno stanje ekrana. 2. Diskretni tipovi `UiIntent` (ili Action) koji predstavljaju sve interakcije korisnika i sistemske događaje. 3. Čista funkcija reduktora stanja: `(PreviousState, UiIntent) -> NewState`. MVI garantuje atomska ažuriranja stanja jer se svi događaji šalju kao zasebne namere i sekvencijalno usmeravaju kroz reduktor stanja. Reduktor uzima nepromenljivi snimak postojećeg stanja i stvara potpuno novi snimak stanja sa svim povezanim svojstvima ažuriranim istovremeno. Pošto su ažuriranja stanja centralizovana, a prelazi sekvencijalni, korisnički interfejs nikada ne posmatra parcijalan, neusklađen ili kontradiktoran snimak stanja.

data class ScreenState(
    val isLoading: Boolean = false,
    val items: List<Item> = emptyList(),
    val error: String? = null
)

sealed interface ScreenIntent {
    data object Refresh : ScreenIntent
    data class DataLoaded(val items: List<Item>) : ScreenIntent
    data class LoadFailed(val message: String) : ScreenIntent
}

fun reduce(state: ScreenState, intent: ScreenIntent): ScreenState = when (intent) {
    is ScreenIntent.Refresh -> state.copy(isLoading = true, error = null)
    is ScreenIntent.DataLoaded -> state.copy(isLoading = false, items = intent.items, error = null)
    is ScreenIntent.LoadFailed -> state.copy(isLoading = false, error = intent.message)
}
Pokušajte da odgovorite na ovo pitanje sa AI trenerom

14Kako biste dizajnirali tok dozvola u izvršnom okruženju koji obrađuje prvi zahtev, UI (User Interface) sa obrazloženjem, odbijanje, opciju „ne pitaj ponovo” i preusmeravanje na podešavanja bez prisiljavanja korisnika?

Korisnički prilagođen tok dozvola u izvršnom okruženju koji ne prisiljava korisnika prati postepeno otkrivanje, jasno obrazloženje i gracioznu degradaciju kroz sva stanja odbijanja: 1. **Kontekstualni zahtev (prvi put)**: Tražite dozvole isključivo u trenutku kada su zaista potrebne, odnosno kada korisnik stupi u interakciju sa određenom funkcionalnošću (npr. pritiskom na „Skeniraj kod” za dozvolu za kameru), umesto unapred pri pokretanju aplikacije. 2. **UI sa obrazloženjem**: Kada metoda `shouldShowRequestPermissionRationale()` vrati `true`, prikažite jasno objašnjenje unutar aplikacije (kao što je donji panel ili dijalog) koje objašnjava zašto je dozvola neophodna i koju korist donosi pre nego što se pokrene sistemski dijalog. 3. **Graciozna degradacija pri odbijanju**: Ako korisnik odbije zahtev, poštujte taj izbor bez blokiranja nepovezanih funkcionalnosti. Obezbedite alternativni tok rada gde god je to moguće (npr. ručni unos teksta ukoliko je pristup kameri odbijen). 4. **Trajno odbijanje („Ne pitaj ponovo”) i preusmeravanje na podešavanja**: Ako je dozvola odbijena, a `shouldShowRequestPermissionRationale()` vrati `false` (korisnik je izabrao „Ne pitaj ponovo” ili je više puta odbio), obavestite korisnika zašto funkcionalnost nije dostupna i ponudite opciono dugme koje ga vodi u podešavanja aplikacije putem `Settings.ACTION_APPLICATION_DETAILS_SETTINGS`. Obezbedite da korisnik može lako da odustane ili se vrati nazad bez osećaja da je blokiran.

fun onScanButtonClicked() {
    when {
        ContextCompat.checkSelfPermission(context, Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED -> {
            openScanner()
        }
        activity.shouldShowRequestPermissionRationale(Manifest.permission.CAMERA) -> {
            showRationaleDialog(onConfirm = { cameraPermissionLauncher.launch(Manifest.permission.CAMERA) })
        }
        else -> {
            cameraPermissionLauncher.launch(Manifest.permission.CAMERA)
        }
    }
}

fun handlePermissionResult(isGranted: Boolean) {
    if (isGranted) {
        openScanner()
    } else if (!activity.shouldShowRequestPermissionRationale(Manifest.permission.CAMERA)) {
        showSettingsRedirectDialog()
    } else {
        showManualEntryFallback()
    }
}
Pokušajte da odgovorite na ovo pitanje sa AI trenerom

15Kako biste projektovali Android CI/CD (Continuous Integration / Continuous Delivery) pipeline od validacije zahteva za spajanje (pull request) do kreiranja potpisanog kandidata za izdanje (release candidate), internog testiranja, otpremanja na Play stazu i postepenog puštanja u produkciju?

Produkcioni Android CI/CD pipeline počinje u fazi validacije zahteva za spajanje (pull request) sa brzim automatizovanim proverama: statičkom analizom (ktlint, detekt, Android Lint), jediničnim testovima i kompajliranjem debug verzije. Nakon spajanja u glavnu granu ili granu za izdanja, pokreće se pipeline za izdanje koji generiše potpisani Release Candidate (RC) Android App Bundle (`.aab`) koristeći kredencijale za potpisivanje (keystore) kojima upravlja CI sistem, Proguard/R8 optimizaciju i sažimanje koda, kao i automatizovano uvećanje verzije. Potpisani AAB i prateće datoteke za mapiranje deobfuskacije otpremaju se na Google Play stazu za interno testiranje (ili Firebase App Distribution) pomoću alata kao što su Gradle Play Publisher (GPP) ili Fastlane, gde se izvršavaju automatizovani smoke ili instrumentacioni testovi. Kada interni QA tim i stejkholderi odobre verziju, potpuno isti binarni artifakt se promoviše dalje (Internal -> Closed Alpha/Beta -> Production Rollout) bez ponovnog građenja iz izvornog koda. Na kraju, fazno puštanje u produkciju (staged rollout, npr. 5% -> 20% -> 100%) praćeno automatizovanim praćenjem stope rušenja (crash rate) i vitalnih metrika (vitals) osigurava bezbedno uvođenje.

lane :promote_internal_to_beta do
  upload_to_play_store(
    track: 'internal',
    track_promote_to: 'beta',
    skip_upload_aab: true, # Promotes existing binary without rebuilding
    skip_upload_metadata: false,
    skip_upload_changelogs: false
  )
end
Pokušajte da odgovorite na ovo pitanje sa AI trenerom

16Zašto hvatanje opštih tipova Exception ili Throwable bez ponovnog izbacivanja CancellationException prekida mehanizam otkazivanja korutina i kako dijagnostikujete probleme sa otkazivanjem?

Kotlin Coroutines se oslanjaju na kooperativno otkazivanje koje se sprovodi preko izuzetka CancellationException. Kada se Job korutine otkaže, suspendujući pozivi (kao što su delay ili yield) bacaju CancellationException kako bi odmotali stek poziva i prekinuli korutinu. Kada kod uhvati generički Exception ili Throwable bez ponovnog bacanja CancellationException, signal za otkazivanje biva progutan. Korutina ne uspeva da se prekine i nastavlja rad, stvarajući 'zombi korutinu' koja troši procesorske resurse i memoriju, stvara curenje resursa i može izazvati neispravne promene stanja ili rušenje aplikacije pri radu sa uništenim UI komponentama. Za dijagnostikovanje problema sa otkazivanjem: 1. Proverite ispravnost rukovanja izuzecima obezbeđivanjem da se CancellationException eksplicitno ponovo izbacuje ili da se hvataju samo specifični domenski izuzeci umesto Throwable/Exception. 2. Pregledajte stanja korutina u razvojnim verzijama koristeći Kotlin Coroutines Debugger (-Dkotlinx.coroutines.debug) ili Android Studio Coroutines Inspector da biste uočili korutine koje su morale biti završene. 3. Zapisujte stanja životnog veka korutina u logove (npr. job.isActive, job.isCancelled) ili koristite struktuirano logovanje u rukovaocima završetka otkazivanja (job.invokeOnCompletion).

import kotlinx.coroutines.CancellationException

// ANTI-PATTERN: Swallows cancellation, creating a zombie coroutine
try {
    doSuspendingWork()
} catch (e: Exception) {
    logError(e)
}

// CORRECT: Preserves cancellation propagation
try {
    doSuspendingWork()
} catch (e: Exception) {
    if (e is CancellationException) throw e
    logError(e)
}
Pokušajte da odgovorite na ovo pitanje sa AI trenerom

17Kako funkcionišu podizanje stanja (state hoisting) i jednosmerni tok podataka (UDF — Unidirectional Data Flow) u radnom okviru Jetpack Compose, i kako odlučujete da li stanje pripada kompozabilnoj funkciji na listu, roditeljskoj kompozabilnoj funkciji ili ViewModel-u?

Podizanje stanja (state hoisting) je obrazac u radnom okviru Jetpack Compose gde se stanje pomera naviše kroz hijerarhiju kompozicije kako bi kompozabilna funkcija postala bez stanja (stateless), pretvarajući je u čistu UI komponentu. To omogućava jednosmerni tok podataka (UDF), gde stanje teče nadole (od roditelja/ViewModel-a ka podređenim kompozabilnim funkcijama u vidu argumenata), a događaji teku nagore (od podređenih kompozabilnih funkcija ka roditelju/ViewModel-u kao povratni pozivi). Odluka o tome gde stanje pripada prati sledeće smernice: 1. Krajnja kompozabilna funkcija / list (lokalno UI stanje): Ako je stanje čisto prolazno, vizuelno i nije potrebno nijednom roditelju niti srodnoj komponenti (npr. da li se izvršava interna animacija proširivanja/skupljanja ili lokalni efekat talasanja), zadržite ga lokalno u krajnjoj kompozabilnoj funkciji koristeći `remember`. 2. Roditeljska kompozabilna funkcija (podignuto UI stanje): Ako srodne kompozabilne funkcije moraju da dele stanje ili reaguju na njega, ili ako roditelj kontroliše vidljivost/validaciju komponente, podignite stanje u neposrednog zajedničkog roditelja. Komponenta na listu postaje bez stanja (prihvata `value` i `onValueChange`). 3. ViewModel (stanje ekrana / poslovno stanje): Ako stanje predstavlja poslovne podatke, preživljava promene konfiguracije, upravlja navigacijom ili zahteva interakciju sa slojevima domena ili repozitorijuma, ono pripada ViewModel-u izloženom kao stanje koje se može posmatrati (npr. `StateFlow`). ViewModel obrađuje poslovnu logiku i u skladu s tim ažurira stanje korisničkog interfejsa.

// Stateless Leaf Composable
@Composable
fun SearchBar(query: String, onQueryChange: (String) -> Unit) {
    TextField(value = query, onValueChange = onQueryChange)
}

// Stateful Screen / ViewModel Owner
@Composable
fun SearchScreen(viewModel: SearchViewModel = viewModel()) {
    val uiState by viewModel.uiState.collectAsStateWithLifecycle()
    SearchBar(
        query = uiState.query,
        onQueryChange = { newQuery -> viewModel.onQueryChanged(newQuery) }
    )
}
Pokušajte da odgovorite na ovo pitanje sa AI trenerom

Pitanja za napredni nivo

18Kako biste projektovali telemetriju, mrvice za praćenje pada aplikacije (crash breadcrumbs) i arhitektonske zaštitne mehanizme za otkrivanje, dijagnostikovanje i sprečavanje gubitka stanja klase FragmentManager i trkačkih stanja navigacije u produkciji?

Gubitak stanja klase `FragmentManager` (`IllegalStateException: Can not perform this action after onSaveInstanceState`) i asinhrona trkačka stanja pri navigaciji nastaju kada asinhrone operacije—poput mrežnih povratnih poziva ili reaktivnih tokova—pokušaju UI transakcije nakon što je životni vek domaćina prešao `onSaveInstanceState()` ili `onStop()`. Da bismo ovo otkrili i dijagnostikovali u produkciji, implementiramo telemetriju životnog veka i mrvice pomoću `Application.ActivityLifecycleCallbacks` i `FragmentManager.FragmentLifecycleCallbacks`, beležeći prelaze sa vremenskim oznakama, broj stavki na steku za povratak na čekanju i kontekst izvršavanja pre rušenja. Da bi se gubitak stanja arhitektonski sprečio, navigacija i UI transakcije moraju se pokretati isključivo preko posmatrača stanja svesnih životnog veka (kao što su `repeatOnLifecycle(Lifecycle.State.RESUMED)` ili `StateFlow` prikupljen uz vezivanje za životni vek), a ne preko sirovih asinhronih povratnih poziva. Navigacioni događaji treba da budu modelovani kao diskretni prelazi stanja u jednosmernom toku podataka (UDF) ili jednokratni događaji koji se troše samo dok je stanje životnog veka najmanje `STARTED` ili `RESUMED`. Pored toga, arhitektonski zaštitni mehanizmi treba da osiguraju bezbednost korišćenjem `commitStateLoss()` samo u eksplicitnim nepovratnim tranzijentnim kontekstima, ili po mogućstvu migracijom na komponentu Jetpack Navigation sa strogim granicama životnog veka. Statička analiza putem prilagođenih pravila Android Lint i provere u izvršnom okruženju u razvojnim izdanjima (npr. Fragment StrictMode) mogu otkriti prekršaje pre nego što stignu u produkciju.

class NavigationDispatcher @Inject constructor() {
    private val _events = Channel<NavigationCommand>(Channel.BUFFERED)
    val events: Flow<NavigationCommand> = _events.receiveAsFlow()

    fun navigate(command: NavigationCommand) {
        _events.trySend(command)
    }
}

// In Fragment / Activity
viewLifecycleOwner.lifecycleScope.launch {
    viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.RESUMED) {
        navigationDispatcher.events.collect { command ->
            CrashReporting.leaveBreadcrumb("Navigating to ${command.destination} at state ${lifecycle.currentState}")
            command.execute(parentFragmentManager)
        }
    }
}
Pokušajte da odgovorite na ovo pitanje sa AI trenerom

19Kako funkcije upravljanja potrošnjom energije na sistemskom nivou, kao što su Doze Mode, App Standby Buckets i OEM (Original Equipment Manufacturer) programi za prekidanje procesa, utiču na raspoređivanje pozadinskih poslova i kako biste projektovali otporan mehanizam sinhronizacije koristeći WorkManager?

Optimizacije baterije na sistemskom nivou znatno ograničavaju izvršavanje u pozadini. Doze Mode ograničava korišćenje procesora, pristup mreži i pozadinske poslove tokom perioda neaktivnosti uređaja, dozvoljavajući izvršavanje samo tokom periodičnih prozora za održavanje. App Standby Buckets dinamički smanjuju učestalost pozadinskih zadataka i mrežni pristup na osnovu toga koliko je aplikacija skoro korišćena (od Active do Restricted ili Never). Pored toga, agresivni prilagođeni upravljači napajanjem proizvođača opreme (OEM, kao što su MIUI ili OneUI) često prekidaju pozadinske procese, ignorišu standardne alarme i ukidaju dozvole za automatsko pokretanje bez obzira na standardno ponašanje AOSP okruženja. Za izradu otpornog mehanizma sinhronizacije, WorkManager predstavlja standardnu osnovu jer apstrahuje JobScheduler, AlarmManager i BroadcastReceivers, dok se direktno integriše sa ograničenjima operativnog sistema. WorkManager omogućava deklarisanje strogih preduslova za izvršavanje (poput NetworkType.CONNECTED ili requiresBatteryNotLow(true)), automatski odlažući izvršavanje do Doze prozora održavanja ili dok se ponovo ne uspostavi mrežna veza. Da bi se prevazišli neočekivani prekidi procesa, prolazni padovi mreže i OEM prekidanja rada, mehanizam sinhronizacije mora poštovati dva osnovna principa: eksponencijalno odlaganje (exponential backoff) i potpunu idempotentnost sa kraja na kraj. WorkManager treba konfigurisati sa BackoffPolicy.EXPONENTIAL kako bi se izbeglo zagušenje backend servera pri izlasku iz Doze režima. Pored toga, radne jedinice (workers) moraju tretirati operacije sinhronizacije kao atomske i idempotentne — koristeći determinističke identifikatore transakcija, lokalne zastavice stanja i serverske ključeve za deduplikaciju — tako da u slučaju naglog prekida i ponovnog stavljanja u red, ponovni pokušaj ne duplira podatke niti ošteti lokalne baze podataka.

val syncConstraints = Constraints.Builder()
    .setRequiredNetworkType(NetworkType.CONNECTED)
    .setRequiresBatteryNotLow(true)
    .build()

val syncWorkRequest = OneTimeWorkRequestBuilder<SyncWorker>()
    .setConstraints(syncConstraints)
    .setBackoffCriteria(
        BackoffPolicy.EXPONENTIAL,
        WorkRequest.MIN_BACKOFF_MILLIS,
        TimeUnit.MILLISECONDS
    )
    .addTag("data_sync_tag")
    .build()

WorkManager.getInstance(context).enqueueUniqueWork(
    "unique_data_sync",
    ExistingWorkPolicy.KEEP,
    syncWorkRequest
)
Pokušajte da odgovorite na ovo pitanje sa AI trenerom

20Kako projektujete skalabilnu Android arhitekturu za više timova koristeći šablon razdvajanja modula na „API (Application Programming Interface) – implementacija” da biste optimizovali vreme Gradle izgradnje i nametnuli stroge granice ugovora?

U složenom Android kodu na kome radi više timova, šablon API-implementacija (API-Impl) deli svaki modul funkcionalnosti ili domena na dva zasebna Gradle potprojekta: lagani `:feature:api` modul koji sadrži javne interfejse, modele i ugovore za navigaciju, i `:feature:impl` modul koji sadrži internu poslovnu logiku, korisnički interfejs i implementacije repozitorijuma. Moduli koji ih koriste zavise isključivo od `:feature:api` pomoću `implementation project(':feature:api')`, dok korenski modul `:app` ili namenski koreni kompozicije (composition roots) povezuju konkretne implementacije putem ubrizgavanja zavisnosti (Dependency Injection, npr. Dagger/Hilt). Ovaj šablon dramatično optimizuje performanse Gradle izgradnje kroz stabilnost ABI-ja (Application Binary Interface) i izolaciju putanje za kompajliranje (compilation classpath). Kada inženjeri izmene detalje implementacije u `:feature:impl`, javni ABI modula `:feature:api` ostaje nepromenjen. Samim tim, Gradle preskače rekompilaciju svih nizvodnih modula koji zavise samo od `:feature:api`, maksimizujući efikasnost mehanizama Gradle Remote Build Cache i keša konfiguracije (configuration cache). Sa organizacione tačke gledišta i upravljanja projektom, ovaj šablon uspostavlja jasne granice vlasništva među timovima pomoću alata kao što je CODEOWNERS. Timovi mogu bezbedno da razvijaju detalje svoje interne implementacije bez izlaganja privatnih klasa, sprečavajući neželjenu čvrstu spregnutost i kružne zavisnosti u velikim timovima.

// :feature:checkout:api/src/main/java/com/example/checkout/api/CheckoutApi.kt
package com.example.checkout.api

interface CheckoutLauncher {
    fun launchCheckoutFlow(cartId: String)
}

// :feature:checkout:impl/src/main/java/com/example/checkout/impl/CheckoutLauncherImpl.kt
package com.example.checkout.impl

import com.example.checkout.api.CheckoutLauncher
import javax.inject.Inject

internal class CheckoutLauncherImpl @Inject constructor(
    private val paymentGateway: PaymentGateway
) : CheckoutLauncher {
    override fun launchCheckoutFlow(cartId: String) {
        // Concrete checkout implementation
    }
}

// :feature:checkout:impl/src/main/java/com/example/checkout/impl/di/CheckoutModule.kt
package com.example.checkout.impl.di

import com.example.checkout.api.CheckoutLauncher
import com.example.checkout.impl.CheckoutLauncherImpl
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent

@Module
@InstallIn(SingletonComponent::class)
internal abstract class CheckoutBindingModule {
    @Binds
    abstract fun bindCheckoutLauncher(impl: CheckoutLauncherImpl): CheckoutLauncher
}
Pokušajte da odgovorite na ovo pitanje sa AI trenerom