20 veelgestelde Android-interviewvragen. Android-ontwikkeling is een veelgevraagd vakgebied voor Kotlin-gebaseerde mobiele apps, Android SDK en Jetpack, met lifecycle-, performance-, netwerk-, opslag-, test- en releasetaken. De vragen behandelen verschillende niveaus, en je kunt hardop oefenen met antwoorden in onze interviewtrainer.
1Doorloop de belangrijkste levenscyclus-callbacks van een Activity van aanmaak tot vernietiging, en leg de verschillen uit tussen onPause(), onStop() en onDestroy() wat betreft het vrijgeven van bronnen.
Een Activity doorloopt zes kern-callbacks in de levenscyclus: onCreate(), onStart(), onResume(), onPause(), onStop() en onDestroy(). onCreate() voert eenmalige initialisatie uit, zoals view inflation. onStart() maakt de activity zichtbaar en onResume() brengt deze naar de voorgrond waar de gebruiker interactie kan hebben. Bij het weg navigeren geeft onPause() aan dat de activity de focus heeft verloren, onStop() dat deze niet langer zichtbaar is op het scherm, en onDestroy() dat de activity-instantie definitief wordt opgeruimd. Wat betreft het vrijgeven van bronnen: - onPause(): Pauzeer alleen snelle, voorgrondspecifieke bewerkingen (zoals het pauzeren van UI-animaties of lichte camera-previews), omdat code-uitvoering in onPause() het starten van de volgende binnenkomende activity direct blokkeert. - onStop(): Dit is de primaire plek om zwaardere bronnen vrij te geven die gekoppeld zijn aan de zichtbaarheid van de UI (zoals GPS-/locatielisteners, sensorfeeds, het afspelen van media of periodieke netwerkverzoeken). Omdat de activity volledig onzichtbaar is, verspilt het actief houden hiervan batterij, en kan het systeem het achtergrondproces zonder verdere waarschuwing beëindigen. - onDestroy(): Wordt gebruikt voor het definitief opruimen van de Activity-instantie (zoals het opschonen van lokale threads of referenties). Kritieke bronnen moeten echter niet wachten tot onDestroy(), omdat Android een gestopt app-proces direct kan beëindigen om geheugen vrij te maken zonder ooit onDestroy() uit te voeren.
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()
}
}
2Wat zijn de vier primaire Android-applicatiecomponenten en wat is het levenscyclus- en threadingmodel van hun standaardcallbacks?
De vier primaire Android-applicatiecomponenten zijn Activity, Service, BroadcastReceiver en ContentProvider. Een Activity biedt een gebruikersinterface voor scherminteracties. Een Service voert achtergrondtaken uit zonder een eigen UI (User Interface). Een BroadcastReceiver luistert naar en reageert op systeembrede of app-specifieke broadcastberichten. Een ContentProvider beheert en ontsluit gestructureerde gegevens naar andere applicaties of interne modules.
Standaard worden de primaire levenscycluscallbacks voor Activity (zoals `onCreate`, `onStart`, `onResume`), Service (zoals `onCreate`, `onStartCommand`, `onBind`) en BroadcastReceiver (`onReceive`) synchroon uitgevoerd op de hoofdthread (UI-thread) van de applicatie. ContentProvider-methoden (zoals `onCreate`) initialiseren ook op de hoofdthread, hoewel query- of insert-operaties die tussen processen worden aangeroepen op threads van de Binder-threadpool draaien. Omdat standaardcallbacks op de hoofdthread worden uitgevoerd, leidt het uitvoeren van zware blokkerende bewerkingen zoals netwerkverzoeken of grote database-I/O direct binnen deze callbacks tot het blokkeren van de UI-loop en veroorzaakt dit een ANR-fout (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
}
}
3Wat zijn in de MVVM-architectuur (Model-View-ViewModel) van Android de belangrijkste taken van een ViewModel vergeleken met UI-controllers (User Interface) zoals Activities en Fragments, en waarom wordt het doorgeven van een Context aan een ViewModel afgeraden?
In de MVVM-architectuur van Android is de ViewModel verantwoordelijk voor het beheren en vasthouden van UI-gerelateerde status, het orkestreren van de presentatielogica en het behouden blijven tijdens configuratiewijzigingen (zoals schermrotaties). UI-controllers (Activities en Fragments) zijn daarentegen verantwoordelijk voor het renderen van UI-elementen op het scherm, het observeren van statuswijzigingen, het afhandelen van directe gebruikersinteracties (zoals klikken en veegbewegingen) en het beheren van levenscyclusgebeurtenissen van Android. Het doorgeven van een Activity-`Context` of View-referentie aan een ViewModel wordt sterk afgeraden omdat ViewModels bij configuratiewijzigingen over het algemeen langer blijven bestaan dan de levenscyclus van UI-controllers. Wanneer een Activity wordt vernietigd en opnieuw wordt aangemaakt tijdens een rotatie, zorgt een ViewModel die de `Context` vasthoudt ervoor dat de oude Activity niet kan worden opgeruimd door de Garbage Collector, wat leidt tot een geheugenlek. Als een Android-`Context` strikt noodzakelijk is voor bewerkingen op systeemniveau, moet in plaats daarvan een `Context` met het bereik van de applicatie worden gebruikt (zoals via `AndroidViewModel` of het injecteren van `ApplicationContext`).
// 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))
}
}
}
4Waarin verschillen normale en gevaarlijke Android-machtigingen, en wat gebeurt er wanneer een app tijdens uitvoering om een gevaarlijke machtiging vraagt?
Normale machtigingen vormen een minimaal risico voor de privacy van de gebruiker of de werking van het apparaat en worden bij de installatie van de app automatisch door het systeem verleend wanneer ze in AndroidManifest.xml zijn gedeclareerd (zoals ACCESS_NETWORK_STATE). Gevaarlijke machtigingen (ook wel runtimemachtigingen genoemd) beheren de toegang tot privégegevens van gebruikers of gevoelige apparaatfuncties, zoals de camera, microfoon, locatie en contacten. Wanneer een app tijdens uitvoering om een gevaarlijke machtiging vraagt, toont het Android-systeem een dialoogvenster waarin de gebruiker wordt gevraagd de toegang toe te staan of te weigeren. Als de gebruiker de machtiging verleent, voert de app de gevraagde functionaliteit uit. Als de gebruiker de machtiging weigert, ontvangt de app een weigeringscallback en moet deze hier op een nette manier mee omgaan door de afhankelijke functionaliteit uit te schakelen of te beperken zonder te crashen, en eventueel een toelichting in de gebruikersinterface te tonen.
5Wat is het verschil tussen debug- en release-builds in Android, en hoe worden build types en product flavors gecombineerd tot build variants?
Bij Android-ontwikkeling configureren build types de verpakkings- en runtime-eigenschappen voor verschillende stadia van het ontwikkelproces. Debug-builds schakelen foutopsporingsvlaggen in (`isDebuggable = true`), gebruiken een standaard debug-keystore en schakelen doorgaans codeminificatie en optimalisatie (R8/ProGuard) uit voor snelle build-iteraties. Release-builds schakelen foutopsporingsvlaggen uit, schakelen optimalisaties en code-obfuscatie in, en vereisen een beveiligde release-ondertekeningssleutel voor distributie. Product flavors vertegenwoordigen verschillende versies of doelen van de app die dezelfde kerncodebase delen, zoals verschillende omgevingen (staging versus productie) of productvarianten (gratis versus betaald). In Gradle worden build variants gevormd door het cartesisch product van product flavors en build types (Build Variant = Product Flavor × Build Type). Het combineren van de flavors `staging` en `production` met de build types `debug` en `release` levert bijvoorbeeld vier build variants op: `stagingDebug`, `stagingRelease`, `productionDebug` en `productionRelease`.
6Wat is een coroutine in Kotlin, en hoe verschilt opschorting (suspension) van het blokkeren van een thread tijdens de uitvoering in Android?
Een coroutine in Kotlin is een lichte abstractie voor gelijktijdigheid waarmee je asynchrone, niet-blokkerende code op een sequentiële manier kunt schrijven. Meerdere coroutines kunnen gelijktijdig draaien op een enkele thread of over threadpools worden verdeeld zonder de aanzienlijke geheugen- en contextwisselings-overhead van threads op besturingssysteemniveau. Het essentiële verschil tussen opschorting en blokkeren zit in het benutten van threads: - Blokkeren (bijvoorbeeld via `Thread.sleep()` of synchrone schijf-/netwerk-I/O) legt de onderliggende thread stil, waardoor deze niet beschikbaar is voor ander werk. Op de main-thread van Android zorgt blokkeren ervoor dat de gebruikersinterface vastloopt en worden ANR-fouten (Application Not Responding) veroorzaakt. - Opschorting (via `suspend`-functies zoals `delay()`) pauzeert de coroutine zonder de onderliggende thread stil te leggen. De coroutine legt zijn uitvoeringsstatus vast in een `Continuation` en geeft de thread terug aan de uitvoeringsomgeving zodat deze ander werk kan verrichten (zoals het verwerken van UI-interacties). Zodra de asynchrone bewerking is voltooid, wordt de coroutine hervat.
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"
}
7Wat is in Jetpack Compose het verschil tussen status die behouden blijft met remember versus rememberSaveable, en hoe stuurt mutableStateOf UI-updates (User Interface) aan over recomposities heen?
In Jetpack Compose bewaart `remember` een object in het geheugen gedurende recomposities zolang de composable in de compositieboom blijft. Status die uitsluitend met `remember` wordt bewaard, gaat echter verloren tijdens configuratiewijzigingen (zoals schermrotaties) en door het systeem geïnitieerde procesbeëindiging. Daarentegen behoudt `rememberSaveable` de status over configuratiewijzigingen en procesbeëindiging heen door waarden automatisch op te slaan en te herstellen via de Android Saved Instance State bundle (of via aangepaste `Saver`-implementaties voor niet-ondersteunde gegevenstypen). `mutableStateOf` verpakt een waarde in een observeerbaar `MutableState`-object dat wordt ondersteund door het Snapshot-statussysteem van Compose. Wanneer een composable de waarde van een `MutableState` leest tijdens de compositie, registreert Compose die leesbewerking. Wanneer de waarde later verandert, markeert Compose automatisch alle composables die die waarde hebben gelezen als ongeldig en plant het deze in voor recompositie, waardoor de UI gesynchroniseerd blijft met de bijgewerkte status.
@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")
}
}
}
8Hoe helpt null-veiligheid in Kotlin crashes in Android-apps te voorkomen, en wanneer gebruik je een nullable type versus een non-null type?
Null-veiligheid in Kotlin helpt veel Android-crashes te voorkomen door nullability onderdeel te maken van het typesysteem. Een waarde die is gedeclareerd als `String` wordt behandeld als non-null, waardoor de compiler directe toegang toestaat zoals `name.length`. Een waarde die is gedeclareerd als `String?` kan null zijn, waardoor Kotlin je verplicht om die situatie af te handelen voordat je de waarde gebruikt. Dit vermindert onbedoelde `NullPointerException`s door optionele Intent-extras, API-antwoorden, Bundle-argumenten of Java-/platform-API's. Gebruik een non-null type wanneer de waarde vereist is om het object of de functie geldig te laten zijn. Gebruik een nullable type wanneer de afwezigheid van een waarde een reële, verwachte toestand is, zoals een optionele URL voor een profielafbeelding of een ontbrekend veld van de server. Nullable waarden worden gewoonlijk afgehandeld met safe calls zoals `user?.name`, de Elvis-operator zoals `user?.name ?: "Guest"`, of expliciete null-controles. De `!!`-operator schakelt deze veiligheid uit en genereert een uitzondering als de waarde null is; deze dient daarom uiterst zelden te worden gebruikt en alleen wanneer de ontwikkelaar daadwerkelijk kan bewijzen dat de waarde niet null kan zijn.
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
9Wat is het mechanische verschil tussen het doorgeven van een Activity Context versus een Application Context aan langdurig levende objecten, en hoe leidt een verkeerde keuze tot een geheugenlek?
Een `Activity`-`Context` is direct gekoppeld aan de levenscyclus van een specifiek scherm en de bijbehorende UI-hiërarchie, terwijl een `Application`-`Context` is gekoppeld aan de levenscyclus van het gehele app-proces.
Wanneer een `Activity` wordt afgesloten of opnieuw wordt opgebouwd (bijvoorbeeld bij een configuratiewijziging zoals schermrotatie), eindigt de levenscyclus en hoort het bijbehorende geheugen te worden opgeruimd door de GC (Garbage Collector). Als je een `Activity`-`Context` doorgeeft aan een langdurig levend object—zoals een singleton, een statisch veld of een langlopende achtergrondthread—behoudt dat object een sterke referentie naar de `Activity`. Omdat langdurig levende objecten fungeren als GC-roots of bereikbaar zijn vanaf GC-roots, kan de garbage collector de vernietigde `Activity` niet opruimen. Hierdoor blijft niet alleen de `Activity`-instantie zelf in het geheugen aanwezig, maar ook de volledige gekoppelde `View`-hiërarchie, drawables en resources, wat resulteert in een aanzienlijk geheugenlek. Het doorgeven van `applicationContext` aan langlevende componenten is daarentegen veilig, omdat de levensduur daarvan overeenkomt met die van het proces.
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 }
}
}
}
}
10Wat is het onderscheid tussen een uploadsleutel en een app-ondertekeningssleutel bij het gebruik van Google Play App Signing?
Onder Google Play App Signing vervullen de uploadsleutel en de app-ondertekeningssleutel twee verschillende beveiligingsrollen. De uploadsleutel wordt door de ontwikkelaar bewaard om het build-artefact (AAB) te ondertekenen voordat het wordt geüpload naar de Google Play Console, waarmee de identiteit van de ontwikkelaar aan Google wordt bevestigd. De app-ondertekeningssleutel wordt veilig opgeslagen binnen de cloudinfrastructuur van Google en wordt door Google Play gebruikt om de gegenereerde APK's te ondertekenen die daadwerkelijk aan eindgebruikers worden geleverd en op apparaten worden geïnstalleerd. Dit vestigt de permanente cryptografische identiteit van de app op Android. Een groot operationeel voordeel is sleutelherstel: als een ontwikkelaar de uploadsleutel verliest of als deze gecompromitteerd raakt, kan Google Play Support de uploadsleutel na identiteitsverificatie van de ontwikkelaar opnieuw instellen zonder het updatetraject van de app te verbreken. In het traditionele model waarbij ontwikkelaars de app-ondertekeningssleutel zelf beheerden, betekende het verlies van de sleutel dat de app nooit meer kon worden bijgewerkt.
11Een scherm verliest door de gebruiker ingevoerde formuliergegevens na het draaien van het apparaat. Hoe zou je systematisch diagnosticeren of het probleem ligt aan een ontbrekende opgeslagen status (saved state), een onjuiste scoping van het ViewModel of het opnieuw binden van de view?
Om systematisch te diagnosticeren waarom formuliergegevens verloren gaan na het draaien van het apparaat, isoleer je drie kerngebieden: 1. **ViewModel-scoping en -behoud:** Controleer of de ViewModel-instantie daadwerkelijk behouden blijft tijdens de configuratiewijziging en niet opnieuw wordt aangemaakt. Log de hashcode van het ViewModel (`viewModel.hashCode()`) voor en na de rotatie. Zorg dat het verkregen wordt via delegated properties zoals `by viewModels()` / `by activityViewModels()` of `ViewModelProvider(this)`, en niet via directe constructoraanroepen (zoals `MyViewModel()`). 2. **Opgeslagen status en herstel van View-ID's:** Controleer of het formulier uitsluitend leunde op het standaard herstelmechanisme van de viewhiërarchie in Android. Views moeten een `android:id`-attribuut hebben gedefinieerd in de layout om deel te nemen aan het automatische opslaan en herstellen van de viewstatus. Bij gebruik van custom views of `SavedStateHandle`, verifieer dat `onSaveInstanceState` / `SavedStateHandle` daadwerkelijk de relevante velden opslaat en herstelt. 3. **Opnieuw binden van views en observer-logica:** Inspecteer `onViewCreated` of subscriptions op observers (`StateFlow`, `LiveData`). Controleer of nieuw aangemaakte views zich correct opnieuw abonneren, of dat initialisatiecode van de view per ongeluk herstelde tekst overschrijft met lege standaardwaarden. Let daarnaast op two-way binding of `TextWatcher`-lussen waarbij een lege, herstelde view direct een lege update terugstuurt naar het 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)
}
}
}
}
}
}
12Hoe vervangt de moderne Jetpack Activity Result API de verouderde startActivityForResult, en hoe beschermt dit tegen het verlies van callbacks tijdens de recreatie van een Activity?
De moderne Jetpack Activity Result API vervangt verouderde methoden zoals `startActivityForResult()` en `onActivityResult()` door een typeveilig, ontkoppeld mechanisme op basis van contracten. In plaats van willekeurige integer-requestcodes en grote `onActivityResult`-switch-statements te beheren, definiëren aanroepers een `ActivityResultContract<I, O>` (waarin de invoer-intentparameters en de verwachte geparseerde uitvoer worden gespecificeerd) en registreren ze een getypeerde callback via `registerForActivityResult()` om een `ActivityResultLauncher` te verkrijgen. De API beschermt tegen het verlies van callbacks tijdens de recreatie van een Activity of procesbeëindiging via strikte lifecycle-registratie: 1. Wanneer een externe Activity (zoals een camera of documentkiezer) wordt gestart, kan de aanroepende Activity door het besturingssysteem worden vernietigd vanwege geheugendruk of configuratiewijzigingen. 2. Door te vereisen dat `registerForActivityResult()` onvoorwaardelijk wordt aangeroepen voordat de Activity of Fragment de status `STARTED` bereikt (doorgaans als property-initialisator of binnen `onCreate`), wordt de result-callback geregistreerd in de `ActivityResultRegistry` voordat het herstel van de toestand plaatsvindt. 3. Wanneer de gestarte activity terugkeert en de host-activity opnieuw wordt aangemaakt, koppelt de `ActivityResultRegistry` het openstaande resultaat van het systeem aan de nieuw geregistreerde callback en wordt het resultaat veilig afgehandeld.
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)
}
}
}
13Vergelijk MVVM (Model-View-ViewModel)- en MVI (Model-View-Intent)-architecturen voor een complex scherm: hoe garanderen state reducers en het verzenden van intents (intent dispatching) in MVI atomaire statusupdates vergeleken met MVVM?
In traditioneel MVVM stelt een ViewModel vaak meerdere onafhankelijke reactieve streams beschikbaar (zoals `isLoading`, `userData`, `errorMessage` als afzonderlijke `StateFlow`- of `LiveData`-eigenschappen) of meerdere publieke update-methoden. Wanneer meerdere asynchrone taken gelijktijdig voltooien, kunnen deze streams onafhankelijk worden bijgewerkt, wat kan leiden tot race conditions, tussenliggende onvolledige toestanden of visuele flikkeringen. In MVI (Model-View-Intent) volgt het statusbeheer een strikte Unidirectional Data Flow (UDF) die is opgebouwd rond drie elementen: 1. Eén enkele onveranderlijke `UiState` die de volledige schermstatus vertegenwoordigt. 2. Discrete `UiIntent`- (of actie-)typen die alle gebruikersinteracties en systeemgebeurtenissen representeren. 3. Een pure State Reducer-functie: `(PreviousState, UiIntent) -> NewState`. MVI garandeert atomaire statusupdates doordat alle gebeurtenissen als afzonderlijke intents worden verzonden en sequentieel door de state reducer worden geleid. De reducer neemt een onveranderlijke momentopname van de bestaande toestand en produceert een volledig nieuwe status-snapshot waarin alle gerelateerde eigenschappen gelijktijdig zijn bijgewerkt. Omdat statusupdates gecentraliseerd zijn en overgangen sequentieel verlopen, observeert de UI nooit een gedeeltelijke, niet-gesynchroniseerde of tegenstrijdige toestand.
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)
}
14Hoe zou je een flow voor runtime-permissies ontwerpen die de eerste aanvraag, uitleg-UI (User Interface), weigering, 'niet opnieuw vragen' en een terugvaloptie naar instellingen afhandelt zonder de gebruiker te dwingen?
Een gebruiksvriendelijke permissieflow zonder dwang volgt de principes van stapsgewijze toelichting (progressive disclosure), een duidelijke onderbouwing en een gecontroleerde terugval (graceful degradation) bij elke vorm van weigering:
1. **Aanvraag in context (eerste keer)**: Vraag permissies pas aan op het moment dat ze daadwerkelijk nodig zijn wanneer de gebruiker interactie heeft met een functionaliteit (bijvoorbeeld door op 'Code scannen' te tikken voor cameratoegang), in plaats van direct bij het opstarten van de app.
2. **Uitleg-UI**: Wanneer `shouldShowRequestPermissionRationale()` de waarde `true` retourneert, toon je eerst een duidelijke uitleg in de app (zoals een bottom sheet of dialoogvenster) over waarom de permissie vereist is en welk voordeel dit biedt, voordat je de systeemprompt activeert.
3. **Gecontroleerde terugval bij weigering**: Als de gebruiker het verzoek weigert, respecteer je die keuze zonder niet-gerelateerde functionaliteiten te blokkeren. Bied waar mogelijk een alternatieve workflow aan (bijvoorbeeld handmatige tekstinvoer als cameratoegang is geweigerd).
4. **Permanente weigering ('Niet opnieuw vragen') & terugvaloptie naar instellingen**: Als de permissie is geweigerd en `shouldShowRequestPermissionRationale()` retourneert `false` (de gebruiker heeft gekozen voor 'Niet opnieuw vragen' of herhaaldelijk geweigerd), leg dan uit waarom de functie niet beschikbaar is en bied een optionele knop aan die verwijst naar de app-instellingen via `Settings.ACTION_APPLICATION_DETAILS_SETTINGS`. Zorg ervoor dat de gebruiker eenvoudig kan annuleren of terugkeren zonder vast te komen zitten.
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()
}
}
15Hoe zou je een Android CI/CD-pipeline (Continuous Integration/Continuous Deployment) ontwerpen vanaf pull-request-validatie tot het maken van een ondertekende release candidate, interne tests, upload naar een Play-track en doorgifte naar verdere uitrol?
Een productieklare Android CI/CD-pipeline begint bij de validatiefase van pull-requests met snelle geautomatiseerde controles: statische codeanalyse (ktlint, detekt, Android Lint), unittests en het compileren van een debug-build. Zodra code wordt gemerged naar de main- of release-branch, start de release-pipeline om een ondertekende Release Candidate (RC) Android App Bundle (`.aab`) te genereren met behulp van door CI beheerde keystore-inloggegevens, Proguard/R8-optimalisatie/shrinking en geautomatiseerde versie-ophoging. De ondertekende AAB en de bijbehorende deobfuscatie-mappingsbestanden worden geüpload naar de interne testtrack van Google Play (of Firebase App Distribution) met tools zoals Gradle Play Publisher (GPP) of Fastlane, waar geautomatiseerde smoke- of instrumentatietests worden uitgevoerd. Zodra interne QA en stakeholders de build goedkeuren, wordt exact hetzelfde binaire artefact gepromoveerd naar volgende fases (Intern -> Gesloten Alfa/Bèta -> Productie-uitrol) zonder de broncode opnieuw te compileren. Ten slotte zorgen gefaseerde uitrollen (bijvoorbeeld 5% -> 20% -> 100%) in combinatie met geautomatiseerde monitoring van crashpercentages en vitals voor een veilige implementatie.
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
16Waarom verbreekt het afvangen van een generieke Exception of Throwable zonder CancellationException opnieuw te gooien de annulering van coroutines, en hoe diagnosticeer je annuleringsproblemen?
Kotlin Coroutines vertrouwen op coöperatieve annulering die is geïmplementeerd via `CancellationException`. Wanneer de `Job` van een coroutine wordt geannuleerd, gooien onderbrekende aanroepen (zoals `delay` of `yield`) een `CancellationException` om de aanroepstack af te wikkelen en de coroutine te beëindigen. Wanneer code een generieke `Exception` of `Throwable` afvangt zonder `CancellationException` opnieuw te gooien, wordt het annuleringssignaal genegeerd. De coroutine stopt niet en blijft doorgaan met uitvoeren, wat leidt tot een 'zombie-coroutine' die CPU en geheugen verspilt, resources lekt en ongeldige statusovergangen of crashes kan veroorzaken bij vernietigde UI-componenten. Om annuleringsproblemen te diagnosticeren: 1. Controleer de discipline rond foutafhandeling door ervoor te zorgen dat `CancellationException` expliciet opnieuw wordt gegooid of dat domeinspecifieke uitzonderingen worden afgevangen in plaats van generieke `Throwable`/`Exception`. 2. Inspecteer coroutinestatussen in debug-builds met behulp van de Kotlin Coroutines Debugger (`-Dkotlinx.coroutines.debug`) of de Android Studio Coroutines Inspector om actieve coroutines te identificeren die beëindigd hadden moeten zijn. 3. Log levenscyclusstatussen van coroutines (bijvoorbeeld `job.isActive`, `job.isCancelled`) of gebruik gestructureerde logging in handlers voor voltooiing na annulering (`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)
}
17Hoe werken state hoisting en UDF (Unidirectional Data Flow) in Jetpack Compose, en hoe bepaal je of state thuishoort in een leaf-composable, een parent-composable of een ViewModel?
State hoisting is een ontwerppatroon in Jetpack Compose waarbij de state naar boven in de compositiehiërarchie wordt verplaatst om een composable stateless te maken, waardoor deze verandert in een pure UI-component. Dit maakt Unidirectional Data Flow (UDF) mogelijk, waarbij state omlaag stroomt (van parent/ViewModel naar onderliggende composables via parameters) en gebeurtenissen omhoog stromen (van onderliggende composables naar parent/ViewModel via callbacks).
Het bepalen van de juiste plek voor state volgt deze richtlijnen:
1. **Leaf-composable (lokale UI-state):** Als de state puur tijdelijk en visueel is en niet nodig is voor een parent of nevenliggende component (bijv. of een interne uitklapanimatie draait of een lokaal 'ripple'-effect), houd deze dan lokaal in de leaf-composable met behulp van `remember`.
2. **Parent-composable (gehoiste UI-state):** Als nevenliggende composables de state moeten delen of erop moeten reageren, of als de parent de zichtbaarheid/validatie van de component bestuurt, hoist de state dan naar de directe gemeenschappelijke parent. De leaf-composable wordt hierdoor stateless (accepteert `value` en `onValueChange`).
3. **ViewModel (scherm-/business-state):** Als state bedrijfsgegevens representeert, configuratiewijzigingen moet overleven, navigatie aanstuurt of interactie met domein-/repositorylagen vereist, hoort deze thuis in een ViewModel en wordt deze ontsloten als observeerbare state (bijv. `StateFlow`). De ViewModel verwerkt de bedrijfslogica en werkt de UI-state dienovereenkomstig bij.
18Hoe zou je telemetrie, crash-breadcrumbs en architecturale vangrails ontwerpen om state loss in FragmentManager en navigation race conditions in productie te detecteren, diagnosticeren en voorkomen?
State loss in FragmentManager (`IllegalStateException: Can not perform this action after onSaveInstanceState`) en asynchrone navigatie-raceconditions treden op wanneer asynchrone operaties — zoals netwerk-callbacks of reactieve stromen — UI-transacties proberen uit te voeren nadat de levenscyclus van de host voorbij `onSaveInstanceState()` of `onStop()` is gegaan.
Om deze problemen in productie te detecteren en diagnosticeren, implementeren we levenscyclustelemetrie en breadcrumbs via `Application.ActivityLifecycleCallbacks` en `FragmentManager.FragmentLifecycleCallbacks`. Hiermee leggen we tijdstempels van overgangen, aantallen wachtende backstack-transacties en de uitvoeringscontext vast voorafgaand aan crashes.
Om state loss architecturaal te voorkomen, moeten navigatie- en UI-transacties uitsluitend worden aangestuurd door levenscyclusbewuste state-observers (zoals `repeatOnLifecycle(Lifecycle.State.RESUMED)` of `StateFlow` verzameld met levenscyclusbinding) in plaats van directe asynchrone callbacks. Navigatiegebeurtenissen moeten worden gemodelleerd als discrete UDF-toestandsveranderingen (Unidirectional Data Flow) of eenmalige events die alleen worden afgehandeld wanneer de levenscyclustoestand minimaal `STARTED` of `RESUMED` is.
Daarnaast moeten architecturale vangrails veiligheid afdwingen door `commitStateLoss()` uitsluitend te gebruiken in expliciete, niet-herstelbare tijdelijke contexten, of bij voorkeur door te migreren naar de Jetpack Navigation-component met strikte levenscyclusgrenzen. Statische analyse via aangepaste Android Lint-regels en runtime-handhaving in debug-builds (bijv. Fragment StrictMode) kunnen schendingen opsporen voordat ze in productie belanden.
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)
}
}
}
19Welke invloed hebben energiebeheerfuncties op systeemniveau, zoals Doze Mode, App Standby Buckets en OEM-taakbeëindigers (Original Equipment Manufacturer), op achtergrondplanning, en hoe zou je een veerkrachtige synchronisatie-engine ontwerpen met WorkManager?
Batterij-optimalisaties op systeemniveau leggen zware beperkingen op aan achtergronduitvoering. Doze Mode beperkt CPU-gebruik, netwerktoegang en achtergrondtaken tijdens perioden van inactiviteit en staat uitvoering alleen toe tijdens periodieke onderhoudsvensters. App Standby Buckets beperken dynamisch de frequentie van achtergrondtaken en netwerktoegang op basis van hoe recent de app is gebruikt (van Active tot Restricted of Never). Daarnaast beëindigen agressieve aangepaste energiemanagers van OEM's (zoals MIUI of OneUI) regelmatig achtergrondprocessen, negeren ze standaard alarmen en trekken ze machtigingen voor automatisch opstarten in, ongeacht het standaard AOSP-gedrag (Android Open Source Project). Om een veerkrachtige synchronisatie-engine te bouwen, is WorkManager de standaardbasis, omdat het `JobScheduler`, `AlarmManager` en `BroadcastReceiver`s abstraheert en direct integreert met randvoorwaarden van het besturingssysteem. Met WorkManager kunnen strikte uitvoeringsvoorwaarden worden gedefinieerd (zoals `NetworkType.CONNECTED`, `requiresBatteryNotLow(true)`), waardoor de uitvoering automatisch wordt uitgesteld tot onderhoudsvensters van Doze of totdat de netwerkverbinding is hersteld. Om onverwachte procesbeëindiging, tijdelijke netwerkuitval en OEM-beëindigingen te weerstaan, moet de synchronisatie-engine twee kernprincipes volgen: exponentiële back-off en end-to-end idempotentie. WorkManager moet worden geconfigureerd met `BackoffPolicy.EXPONENTIAL` om 'thundering herd'-problemen op backendservers te voorkomen bij het herstellen vanuit Doze. Bovendien moeten workers synchronisatiebewerkingen als atomair en idempotent behandelen — met behulp van deterministische transactie-ID's, lokale statusvlaggen en deduplicatiesleutels aan de serverzijde — zodat wanneer een worker halverwege abrupt wordt beëindigd en opnieuw in de wachtrij wordt geplaatst, een nieuwe poging geen gegevens dupliceert of lokale databases beschadigt.
20Hoe ontwerp je een schaalbare Android-architectuur voor meerdere teams met behulp van het 'API-Implementation'-modulescheidingspatroon om Gradle-bouwtijden te optimaliseren en strikte contractgrenzen af te dwingen?
In een enterprise Android-codebase voor meerdere teams splitst het API-Implementation-patroon (API-Impl) elke feature- of domeinmodule op in twee afzonderlijke Gradle-subprojecten: een lichte `:feature:api`-module met daarin publieke interfaces, modellen en navigatiecontracten, en een `:feature:impl`-module met de interne bedrijfslogica, UI en repository-implementaties. Afhankelijke modules verwijzen uitsluitend naar `:feature:api` via `implementation project(':feature:api')`, terwijl de hoofdmodule `:app` of speciale composition roots de concrete implementaties samenbrengen via dependency injection (bijvoorbeeld Dagger/Hilt).
Dit patroon optimaliseert de bouwprestaties van Gradle aanzienlijk door ABI-stabiliteit (Application Binary Interface) en isolatie van het compilatie-classpath. Wanneer ontwikkelaars implementatiedetails in `:feature:impl` wijzigen, blijft de publieke ABI van `:feature:api` ongewijzigd. Hierdoor slaat Gradle de hercompilatie over van alle downstream modules die alleen van `:feature:api` afhangen, wat de effectiviteit van de Gradle Remote Build Cache en de configuratiecache maximaliseert.
Vanuit organisatorisch en governance-perspectief stelt dit patroon duidelijke eigenaarschapsgrenzen tussen teams vast met behulp van tools zoals CODEOWNERS. Teams kunnen hun interne implementatiedetails veilig doorontwikkelen zonder private klassen bloot te stellen, wat ongewenste strakke koppeling en circulaire afhankelijkheden binnen grote teams voorkomt.