20 ofte stillede Android-interviewspørgsmål. Android-udvikling er et efterspurgt felt med fokus på Kotlin-baserede mobilapps, Android SDK og Jetpack samt lifecycle-, performance-, netværks-, storage-, test- og releaseopgaver. Spørgsmålene dækker flere niveauer, og du kan øve dig i at svare mundtligt i vores interviewtræner.
1Gennemgå de centrale livscyklus-callbacks for en Activity fra oprettelse til destruktion, og forklar forskellene mellem onPause(), onStop() og onDestroy() i forhold til frigivelse af ressourcer.
En Activity skifter mellem seks centrale livscyklus-callbacks: onCreate(), onStart(), onResume(), onPause(), onStop() og onDestroy(). onCreate() udfører engangsinitialisering, såsom view inflation. onStart() gør aktiviteten synlig, og onResume() bringer den i forgrunden, hvor den har interaktivt fokus. Når der navigeres væk, indikerer onPause(), at aktiviteten har mistet fokus, onStop() indikerer, at den ikke længere er synlig på skærmen, og onDestroy() indikerer den endelige nedlæggelse af Activity-instansen. Hvad angår frigivelse af ressourcer: - onPause(): Pausér kun hurtige, forgrundsspecifikke handlinger (såsom at pause UI-animationer eller lette kamera-previews), da kodeafvikling i onPause() blokerer direkte for, at den næste indkommende aktivitet kan starte. - onStop(): Dette er det primære sted at frigive tungere ressourcer knyttet til UI-synlighed (såsom GPS-/lokations-listeners, sensorfeeds, medieafspilning eller netværkspolling). Da aktiviteten er helt usynlig, vil det spilde batteri at holde disse aktive, og systemet kan afslutte baggrundsprocessen uden yderligere varsel. - onDestroy(): Bruges til endelig oprydning af Activity-instansen (såsom at rydde lokale tråde eller referencer). Kritiske ressourcer bør dog ikke vente til onDestroy(), da Android kan dræbe en stoppet app-proces direkte for at genvinde hukommelse uden nogensinde at udføre 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()
}
}
2Hvad er de fire primære Android-applikationskomponenter, og hvad er livscyklus- og trådmodellen for deres standard-callbacks?
De fire primære Android-applikationskomponenter er Activity, Service, BroadcastReceiver og ContentProvider. En Activity leverer en brugergrænseflade til interaktioner på skærmen. En Service udfører baggrundsbehandling uden en dedikeret UI. En BroadcastReceiver lytter efter og reagerer på systemomspændende eller app-interne broadcast-beskeder. En ContentProvider administrerer og udstiller strukturerede data til andre applikationer eller interne moduler. Som standard afvikles de primære livscyklus-callbacks for Activity (f.eks. onCreate, onStart, onResume), Service (f.eks. onCreate, onStartCommand, onBind) og BroadcastReceiver (onReceive) synkront på applikationens hovedtråd (UI-tråden). ContentProvider-metoder (såsom onCreate) initialiseres også på hovedtråden, men query/insert-handlinger, der kaldes på tværs af processer, kører på tråde fra en Binder-trådpulje. Da standard-callbacks kører på hovedtråden, vil tunge, blokerende operationer som netværkskald eller omfattende disk-I/O til databaser blokere UI-løkken og udløse en Application Not Responding (ANR)-fejl.
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
}
}
3Hvad er de primære ansvarsområder for en ViewModel sammenlignet med UI-controllere (User Interface) som Activities og Fragments i Androids MVVM-arkitektur (Model-View-ViewModel), og hvorfor frarådes det at videregive en Context til en ViewModel?
I Androids MVVM-arkitektur er en ViewModel ansvarlig for at opbevare og administrere UI-relateret tilstand, orkestrere præsentationslogik og overleve konfigurationsændringer (såsom skærmrotationer). Til sammenligning er UI-controllere (Activities og Fragments) ansvarlige for at gengive UI-elementer på skærmen, observere tilstandsændringer, håndtere direkte brugerinteraktioner (klik, swipes) samt håndtere Android-livscyklusbegivenheder. Det frarådes kraftigt at videregive en Activity-`Context` eller en reference til et View til en ViewModel, fordi ViewModels generelt overlever UI-controlleres livscyklus på tværs af konfigurationsændringer. Når en Activity destrueres og genoprettes under rotation, vil en ViewModel, der holder på dens Context, forhindre den gamle Activity i at blive fjernet af garbage collectoren, hvilket forårsager en hukommelseslækage. Hvis en Android-`Context` er strengt nødvendig til operationer på systemniveau, bør der i stedet anvendes en Context med applikations-scope (for eksempel via `AndroidViewModel` eller ved at injicere 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))
}
}
}
4Hvordan adskiller normale og farlige tilladelser i Android sig fra hinanden, og hvad sker der, når en app anmoder om en farlig tilladelse ved kørselstid (runtime)?
Normale tilladelser udgør en minimal risiko for brugerens privatliv eller enhedens drift og tildeles automatisk af systemet ved installation af appen, hvis de er deklareret i `AndroidManifest.xml` (såsom `ACCESS_NETWORK_STATE`). Farlige tilladelser (også kaldet runtime-tilladelser) styrer adgangen til private brugerdata eller begrænsede enhedsfunktioner, såsom kamera, mikrofon, lokation og kontakter. Når en app anmoder om en farlig tilladelse under kørslen, viser Android-systemet en systemdialogboks, der beder brugeren om at tillade eller afvise adgangen. Hvis brugeren giver tilladelse, udfører appen den ønskede funktion. Hvis brugeren afviser tilladelsen, modtager appen et afvisnings-callback og skal håndtere det elegant ved at deaktivere eller begrænse den afhængige funktion uden at gå ned, samt eventuelt vise en forklarende brugergrænseflade (UI rationale).
5Hvad er forskellen mellem debug- og release-builds i Android, og hvordan kombineres build types og product flavors til build variants?
I Android-udvikling konfigurerer build types pakke- og runtime-egenskaber til forskellige udviklingsstadier. Debug-builds aktiverer debugging-flag (`isDebuggable = true`), anvender et standard debug-keystore og deaktiverer typisk kodeminificering og -krympning (R8/ProGuard) for at sikre hurtige build-iterationer. Release-builds deaktiverer debugging-flag, aktiverer optimeringer og tilsløring (obfuscation) og kræver en sikker release-signeringsnøgle til distribution. Product flavors repræsenterer forskellige versioner eller målgrupper af appen, som deler den samme kernekodebase, såsom forskellige miljøer (staging vs. produktion) eller produktudgaver (gratis vs. betalt). I Gradle dannes build variants ved det kartesiske produkt af product flavors og build types (Build Variant = Product Flavor × Build Type). For eksempel vil en kombination af flavors som `staging` og `production` med build types som `debug` og `release` give fire build variants: `stagingDebug`, `stagingRelease`, `productionDebug` og `productionRelease`.
6Hvad er en coroutine i Kotlin, og hvordan adskiller suspendering sig fra at blokere en tråd under runtime i Android?
En coroutine i Kotlin er en letvægtsabstraktion til samtidighed, der gør det muligt at skrive asynkron, ikke-blokerende kode på en sekventiel måde. Flere coroutines kan køre samtidigt på en enkelt tråd eller på tværs af trådpuljer uden den tunge hukommelses- og kontekstskift-overhead, som operativsystemtråde har. Den væsentlige forskel mellem suspendering og blokering er trådudnyttelse: - Blokering (f.eks. Thread.sleep() eller synkron disk-/netværks-I/O) standser den underliggende tråd og gør den utilgængelig for andet arbejde. På Androids hovedtråd fastfryser blokering brugergrænsefladen og udløser ANR-fejl (Application Not Responding). - Suspendering (via suspend-funktioner såsom delay()) pauser coroutinen uden at standse den underliggende tråd. Coroutinen gemmer sin udførelsestilstand i en Continuation og frigiver tråden tilbage til kørselsmiljøet, så den kan udføre andet arbejde (såsom at håndtere UI-interaktioner). Når den asynkrone operation er færdig, genoptages coroutinen.
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"
}
7Hvad er forskellen i Jetpack Compose mellem tilstand bevaret med remember versus rememberSaveable, og hvordan driver mutableStateOf UI-opdateringer (User Interface) på tværs af rekompositioner?
I Jetpack Compose gemmer `remember` et objekt i hukommelsen på tværs af rekompositioner, så længe den pågældende composable forbliver i kompositionstræet. Tilstand, der udelukkende fastholdes med `remember`, går dog tabt ved konfigurationsændringer (såsom skærmrotationer) og systeminitieret proceslukning (process death). Til sammenligning bevarer `rememberSaveable` tilstand på tværs af konfigurationsændringer og proceslukning ved automatisk at gemme og gendanne værdier via Androids Saved Instance State bundle (eller gennem brugerdefinerede `Saver`-implementeringer for ikke-understøttede datatyper). `mutableStateOf` indkapsler en værdi i et observerbart `MutableState`-objekt understøttet af Composes Snapshot-tilstandssystem. Når en composable læser værdien af en `MutableState` under kompositionen, registrerer Compose denne læsehandling. Når værdien senere ændres, markerer Compose automatisk alle composables, der læste værdien, som ugyldige og planlægger dem til rekomposition, hvilket sikrer, at brugerfladen forbliver synkroniseret med den opdaterede tilstand.
@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")
}
}
}
8Hvordan hjælper Kotlins null-sikkerhed (null safety) med at forhindre nedbrud i Android-apps, og hvornår bør man bruge en type, der kan være null, frem for en ikke-null type?
Kotlins null-sikkerhed hjælper med at forhindre mange Android-nedbrud ved at gøre null-egenskaben til en del af typesystemet. En værdi erklæret som `String` behandles som ikke-null, så kompileren tillader direkte adgang såsom `name.length`. En værdi erklæret som `String?` kan være null, og Kotlin tvinger dig derfor til at håndtere det tilfælde før brug, hvilket reducerer utilsigtede `NullPointerException`-fejl fra valgfrie Intent-extras, API-svar, Bundle-argumenter eller Java-/platform-API'er. Brug en ikke-null type, når værdien er påkrævet for, at objektet eller funktionen er gyldig. Brug en type, der kan være null, når fravær er en reel, forventet tilstand, såsom en valgfri URL til et profilbillede eller et manglende felt fra serveren. Værdier, der kan være null, håndteres normalt med sikre kald såsom `user?.name`, Elvis-operatoren såsom `user?.name ?: "Guest"` eller eksplicitte null-tjek. `!!`-operatoren fravælger sikkerheden og kaster en undtagelse, hvis værdien er null, så den bør anvendes sjældent og kun, når udvikleren reelt kan bevise, at værdien ikke kan være 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
9Hvad er den mekaniske forskel på at videregive en Activity Context versus en Application Context til objekter med lang levetid, og hvordan fører det forkerte valg til en hukommelseslækage?
En Activity Context er bundet direkte til livscyklussen for en specifik skærm og dens UI-hierarki, hvorimod en Application Context er bundet til hele app-processens livscyklus. Når en Activity afsluttes eller genoprettes (f.eks. ved en konfigurationsændring såsom skærmrotation), slutter dens livscyklus, og dens hukommelse bør frigives af Garbage Collector (GC). Hvis du sender en Activity Context til et objekt med lang levetid – såsom en singleton, et statisk felt eller en langvarig baggrundstråd – fastholder dette langlivede objekt en stærk reference til din Activity. Da objekter med lang levetid fungerer som GC-rødder eller kan nås fra GC-rødder, kan garbage collectoren ikke frigive den destruerede Activity. Dette fastholder ikke kun selve Activity-instansen, men også hele dens tilknyttede View-hierarki, drawables og ressourcer, hvilket skaber en betydelig hukommelseslækage. Til sammenligning er det sikkert at videregive applicationContext til komponenter med lang levetid, da dens livscyklus forventes at matche processens levetid.
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 }
}
}
}
}
10Hvad er forskellen på en upload-nøgle (upload key) og en app-signeringsnøgle (app signing key) ved brug af Google Play App Signing?
I Google Play App Signing tjener upload-nøglen og app-signeringsnøglen to forskellige sikkerhedsmæssige formål. Upload-nøglen opbevares af udvikleren til at signere build-artefaktet (AAB), før det uploades til Google Play Console, hvilket bekræfter udviklerens identitet over for Google. App-signeringsnøglen gemmes sikkert i Googles cloud-infrastruktur og bruges af Google Play til at signere de genererede APK'er, der rent faktisk leveres og installeres på slutbrugernes enheder, hvilket etablerer appens permanente kryptografiske identitet på Android. En væsentlig driftsmæssig fordel er nøglegenoprettelse: Hvis en udvikler mister eller kompromitterer sin upload-nøgle, kan Google Play-support nulstille upload-nøglen efter verifikation af udviklerens identitet uden at afbryde appens opdateringssti. I den ældre model, hvor udviklere selv lå inde med app-signeringsnøglen, betød tab af nøglen, at appen aldrig kunne opdateres igen.
11En skærm mister brugerindtastede formulardata efter enhedens rotation. Hvordan vil du systematisk diagnosticere, om problemet skyldes manglende gemt tilstand (saved state), forkert ViewModel-scoping eller genbinding af visninger (view rebinding)?
For systematisk at diagnosticere, hvorfor formulardata går tabt efter enhedens rotation, isoleres tre kerneområder: 1. **ViewModel-scoping og fastholdelse:** Kontrollér, at ViewModel-instansen rent faktisk bevares på tværs af konfigurationsændringen i stedet for at blive genoprettet. Log ViewModel-instansens hashkode (`viewModel.hashCode()`) på tværs af rotationer. Sørg for, at den hentes via delegerede egenskaber som `by viewModels()` / `by activityViewModels()` eller `ViewModelProvider(this)`, og ikke via direkte oprettelse med en konstruktør (f.eks. `MyViewModel()`). 2. **Gemt tilstand og genoprettelse af view-ID (Identifier):** Undersøg, om formularen udelukkende stolede på Androids standardgenoprettelse af view-hierarkiet. Views skal have en `android:id`-attribut defineret i layoutet for at kunne deltage i automatisk lagring og genoprettelse af view-tilstand. Hvis der anvendes tilpassede views eller `SavedStateHandle`, skal du verificere, at `onSaveInstanceState` / `SavedStateHandle` rent faktisk gemmer og genopretter de relevante felter. 3. **Genbinding af views og observer-logik:** Inspicér `onViewCreated` eller observer-abonnementer (`StateFlow`, `LiveData`). Kontrollér, om nyoprettede views abonnerer korrekt igen, eller om koden til opsætning af views ved en fejl overskriver den genoprettede tekst med tomme standardværdier. Tjek desuden for tovejsbindinger eller `TextWatcher`-løkker, hvor et tomt genoprettet view straks udsender en blank opdatering tilbage til 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)
}
}
}
}
}
}
12Hvordan erstatter det moderne Jetpack Activity Result API (Application Programming Interface) den forældede startActivityForResult, og hvordan sikrer det mod tab af callbacks under genoprettelse af en Activity?
Det moderne Jetpack Activity Result API erstatter forældede `startActivityForResult()` og `onActivityResult()` med en typesikker, afkoblet kontraktbaseret mekanisme. I stedet for at håndtere vilkårlige heltals-request codes og store `onActivityResult`-switch-blokke definerer kaldere en `ActivityResultContract<I, O>` (som angiver input-intent-parametrene og det forventede parsede output) og registrerer et typet callback ved hjælp af `registerForActivityResult()` for at modtage en `ActivityResultLauncher`. API'et sikrer mod tab af callbacks under genoprettelse af en Activity eller procesdød via en streng livscyklusregistrering: 1. Når en ekstern Activity (såsom kameraet eller en dokumentvælger) startes, kan den kaldende Activity blive destrueret af styresystemet på grund af hukommelsesmangel eller konfigurationsændringer. 2. Ved at kræve, at `registerForActivityResult()` kaldes ubetinget, før Activity'en eller Fragment'et når tilstanden `STARTED` (typisk som en egenskabsinitialisering eller inde i `onCreate`), registreres resultat-callbacket i `ActivityResultRegistry`, før tilstandsgenoprettelsen finder sted. 3. Når den startede Activity returnerer, og værts-Activity'en genoprettes, matcher `ActivityResultRegistry` det afventende resultat fra systemet med det netop registrerede callback og leverer resultatet sikkert.
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)
}
}
}
13Sammenlign MVVM- (Model-View-ViewModel) og MVI-arkitekturer (Model-View-Intent) for en kompleks skærm: Hvordan garanterer state reducere og afsendelse af intents i MVI atomare tilstandsopdateringer sammenlignet med MVVM?
I traditionel MVVM eksponerer en ViewModel ofte flere uafhængige reaktive datastrømme (såsom `isLoading`, `userData`, `errorMessage` som separate `StateFlow`- eller `LiveData`-egenskaber) eller flere offentlige opdateringsmetoder. Når flere asynkrone opgaver afsluttes samtidigt, kan disse datastrømme opdateres uafhængigt af hinanden, hvilket kan føre til race conditions, ufuldstændige mellemtilstande eller visuel flimren. I MVI (Model-View-Intent) følger tilstandshåndtering et strengt ensrettet dataflow (UDF, Unidirectional Data Flow) opbygget omkring tre elementer: 1. En enkelt uforanderlig `UiState`, der repræsenterer hele skærmens tilstand. 2. Diskrete `UiIntent`- (eller Action-)typer, der repræsenterer alle brugerinteraktioner og systemhændelser. 3. En ren State Reducer-funktion: `(PreviousState, UiIntent) -> NewState`. MVI garanterer atomare tilstandsopdateringer, fordi alle hændelser afsendes som distinkte intents og dirigeres sekventielt gennem tilstandsreduceren. Reduceren tager et uforanderligt øjebliksbillede af den eksisterende tilstand og producerer et helt nyt tilstandsøjebliksbillede, hvor alle relaterede egenskaber opdateres samtidigt. Da tilstandsopdateringer er centraliserede og overgangene er sekventielle, observerer brugergrænsefladen aldrig et delvist, usynkroniseret eller modstridende tilstandsøjebliksbillede.
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)
}
14Hvordan vil du designe et forløb til kørselstilladelser (runtime permissions), der håndterer den første anmodning, rationale-UI (User Interface), afvisning, 'spørg ikke igen' og en fallback til indstillinger uden at presse brugeren?
Et ikke-påtrængende, brugervenligt forløb til runtime-tilladelser følger principperne om trinvis fremvisning (progressive disclosure), klar begrundelse og kontrolleret degradering på tværs af alle afvisningstilstande:
1. **Kontekstbaseret anmodning (første gang)**: Anmod kun om tilladelser på det tidspunkt, hvor der reelt er brug for dem, når brugeren interagerer med en funktion (f.eks. ved at trykke på 'Scan kode' for at få kameratilladelse), frem for på forhånd ved app-opstart.
2. **Rationale-UI**: Når `shouldShowRequestPermissionRationale()` returnerer `true`, vises en klar forklaring i appen (såsom et bottom sheet eller en dialogboks), der forklarer, hvorfor tilladelsen er nødvendig, og hvilken fordel den giver, før systemets dialogboks udløses.
3. **Kontrolleret degradering ved afvisning**: Hvis brugeren afviser anmodningen, respekteres valget uden at blokere for urelaterede funktioner. Tilbyd en alternativ arbejdsgang, hvor det er muligt (f.eks. manuel tekstindtastning, hvis kameraadgang afvises).
4. **Permanent afvisning ('Spørg ikke igen') og fallback til indstillinger**: Hvis tilladelsen afvises, og `shouldShowRequestPermissionRationale()` returnerer `false` (brugeren har valgt 'Spørg ikke igen' eller afvist gentagne gange), informeres brugeren om, hvorfor funktionen er utilgængelig, og der tilbydes en valgfri knap, der sender vedkommende til appens indstillinger via `Settings.ACTION_APPLICATION_DETAILS_SETTINGS`. Sørg for, at brugeren let kan annullere eller navigere tilbage uden at blive låst fast.
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()
}
}
15Hvordan vil du designe en Android CI/CD-pipeline (Continuous Integration/Continuous Delivery) fra validering af pull requests til oprettelse af signerede release candidates, intern test, upload til Play-spor og udrulningspromovering?
En produktionsklar Android CI/CD-pipeline starter ved valideringstrinnet for pull requests med hurtige automatiserede tjek: statisk analyse (ktlint, detekt, Android Lint), enhedstests og kompilering af et debug-build. Ved merge til main- eller release-branchen udløses release-pipelinen for at generere en signeret Release Candidate (RC) Android App Bundle (`.aab`) ved hjælp af CI-administrerede release-keystore-legitimationsoplysninger, Proguard/R8-krympning og automatiseret versionsopdatering. Den signerede AAB og dens tilhørende deobfuskerings-mappingfiler uploades til Google Plays interne testspor (eller Firebase App Distribution) ved hjælp af værktøjer som Gradle Play Publisher (GPP) eller Fastlane, hvor automatiserede smoke-tests eller instrumenteringstests køres. Når intern QA og interessenter godkender buildet, promoveres nøjagtig samme binære artefakt videre (Internt -> Lukket Alpha/Beta -> Produktionsudrulning) uden at genkompilere fra kildekoden. Til sidst sikrer trinvise udrulninger (f.eks. 5 % -> 20 % -> 100 %) kombineret med automatiseret overvågning af crash-rater og vitale målinger en sikker udrulning.
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
16Hvorfor ødelægger det coroutine-annullering at fange generiske Exception eller Throwable uden at kaste CancellationException videre, og hvordan diagnosticerer man annulleringsproblemer?
Kotlin Coroutines er baseret på kooperativ annullering, der er implementeret via CancellationException. Når en coroutines Job annulleres, kaster suspenderende kald (såsom delay eller yield) en CancellationException for at afvikle kaldstakken og afslutte coroutinen. Når kode fanger en generisk Exception eller Throwable uden at kaste CancellationException videre, sluges annulleringssignalet. Coroutinen formår ikke at afbryde og fortsætter sin kørsel, hvilket skaber en 'zombie-coroutine', som spilder CPU/hukommelse, lækker ressourcer og kan udløse ugyldige tilstandsovergange eller nedbrud mod destruerede UI-komponenter. Sådan diagnosticeres annulleringsproblemer: 1. Verificer korrekt undtagelseshåndtering ved at sikre, at CancellationException eksplicit kastes videre, eller at der kun fanges domænespecifikke undtagelser i stedet for generiske Throwable/Exception. 2. Inspicer coroutine-tilstande i debug-builds ved hjælp af Kotlin Coroutines Debugger (-Dkotlinx.coroutines.debug) eller Android Studio Coroutines Inspector for at identificere kørende coroutines, der burde være afsluttet. 3. Log coroutine-livscyklustilstande (f.eks. job.isActive, job.isCancelled), eller benyt struktureret logning i completions-handlere for annullering (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)
}
17Hvordan fungerer state hoisting og UDF (Unidirectional Data Flow) i Jetpack Compose, og hvordan beslutter du, om tilstand hører til i en leaf-composable, en overordnet composable eller en ViewModel?
State hoisting er et mønster i Jetpack Compose, hvor tilstand flyttes op i kompositionshierarkiet for at gøre en composable tilstandsløs og omdanne den til en ren UI-komponent. Det muliggør UDF (Unidirectional Data Flow), hvor tilstand flyder nedad (fra overordnet komponent/ViewModel til underordnede composables som argumenter), og hændelser flyder opad (fra underordnede composables til overordnet komponent/ViewModel som callbacks). Beslutningen om, hvor tilstand hører hjemme, følger disse retningslinjer: 1. Leaf Composable (Lokal UI-tilstand): Hvis tilstanden udelukkende er midlertidig, visuel og ikke behøves af overordnede eller sideløbende komponenter (f.eks. om en intern fold ud/ind-animation kører, eller en lokal ripple-effekt), holdes den lokal i leaf-composablen ved hjælp af `remember`. 2. Overordnet Composable (Hoisted UI-tilstand): Hvis sideløbende composables har brug for at dele eller reagere på tilstanden, eller hvis den overordnede komponent styrer komponentens synlighed/validering, løftes tilstanden op til den nærmeste fælles overordnede composable. Leaf-komponenten bliver tilstandsløs (modtager `value` og `onValueChange`). 3. ViewModel (Skærm- / forretningstilstand): Hvis tilstanden repræsenterer forretningsdata, overlever konfigurationsændringer, styrer navigation eller kræver interaktion med domæne-/repository-lagene, hører den hjemme i en ViewModel eksponeret som observerbar tilstand (f.eks. `StateFlow`). ViewModel håndterer forretningslogik og opdaterer UI-tilstanden derefter.
18Hvordan ville du designe telemetri, crash breadcrumbs og arkitektoniske guardrails til at opdage, diagnosticere og forhindre tilstandstab i FragmentManager samt race conditions i navigation i produktion?
Tilstandstab i FragmentManager (`IllegalStateException: Can not perform this action after onSaveInstanceState`) og asynkrone race conditions i navigation opstår, når asynkrone handlinger — såsom netværks-callbacks eller reaktive datastrømme — forsøger at udføre UI-transaktioner, efter at værtens livscyklus har passeret `onSaveInstanceState()` eller `onStop()`. For at opdage og diagnosticere disse i produktion implementerer vi livscyklustelemetri og breadcrumbs via `Application.ActivityLifecycleCallbacks` og `FragmentManager.FragmentLifecycleCallbacks`, som registrerer tidsstemplede overgange, antallet af ventende elementer på backstacken samt eksekveringskonteksten forud for nedbruddet. For at forhindre tilstandstab arkitektonisk skal navigation og UI-transaktioner udelukkende drives af livscyklusbevidste tilstandsobservatører (såsom `repeatOnLifecycle(Lifecycle.State.RESUMED)` eller `StateFlow`, der opsamles med binding til livscyklussen) frem for direkte asynkrone callbacks. Navigationshændelser bør modelleres som diskrete tilstandsovergange i et ensrettet dataflow (UDF, Unidirectional Data Flow) eller enkeltstående hændelser (single-shot events), der kun forbruges, mens livscyklustilstanden er mindst `STARTED` eller `RESUMED`. Desuden bør arkitektoniske guardrails håndhæve sikkerheden ved kun at tillade `commitStateLoss()` i eksplicitte, midlertidige kontekster, som ikke kan gendannes, eller helst ved at migrere til Jetpack Navigation-komponenten med strenge livscyklusgrænser. Statisk analyse via brugerdefinerede Android Lint-regler og runtime-håndhævelse i debug-builds (f.eks. Fragment StrictMode) kan fange overtrædelser, før de når produktion.
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)
}
}
}
19Hvordan påvirker strømstyringsfunktioner på systemniveau såsom Doze Mode, App Standby Buckets og OEM-opgavedræbere (Original Equipment Manufacturer) baggrundsplanlægning, og hvordan ville du designe en robust synkroniseringsmotor ved hjælp af WorkManager?
Batterioptimeringer på systemniveau begrænser baggrundskørsel markant. Doze Mode begrænser CPU, netværksadgang og baggrundsjobs i perioder med inaktivitet og tillader kun afvikling under periodiske vedligeholdelsesvinduer. App Standby Buckets drosler dynamisk hyppigheden af baggrundsjobs og netværksadgang baseret på, hvor nyligt appen har været brugt (fra Active ned til Restricted eller Never). Desuden dræber aggressive, tilpassede OEM-strømstyringsværktøjer (såsom MIUI eller OneUI) ofte baggrundsprocesser, ignorerer standardalarmer og fjerner tilladelser til automatisk start uanset standard AOSP-adfærd (Android Open Source Project). For at bygge en robust synkroniseringsmotor er WorkManager standardfundamentet, fordi det abstraherer JobScheduler, AlarmManager og BroadcastReceivers, samtidig med at det integrerer direkte med operativsystemets begrænsninger. WorkManager gør det muligt at deklarere strenge forudsætninger for kørsel (såsom NetworkType.CONNECTED, requiresBatteryNotLow(true)) og udskyder automatisk kørslen til Doze-vedligeholdelsesvinduer, eller indtil forbindelsen er genoprettet. For at modstå uventet procesafslutning, forbigående netværksudfald og OEM-nedlukninger skal synkroniseringsmotoren overholde to grundlæggende principper: eksponentiel backoff og fuldstændig idempotens. WorkManager bør konfigureres med BackoffPolicy.EXPONENTIAL for at undgå thundering herd-problemer på backendservere ved genetablering efter Doze. Derudover skal workers behandle synkroniseringshandlinger som atomare og idempotente — ved hjælp af deterministiske transaktions-ID'er, lokale tilstandsflag og dedupliceringsnøgler på serversiden — så hvis en worker pludseligt afsluttes midtvejs og sættes i kø igen, vil et nyt forsøg hverken duplikere data eller ødelægge lokale databaser.
20Hvordan designer du en skalerbar Android-arkitektur til flere teams ved hjælp af 'API-Implementation'-moduladskillelsesmønstret for at optimere Gradle-byggetider og håndhæve stringente kontraktgrænser?
I en enterprise-Android-kodebase med flere teams opdeler API-Implementation-mønstret (API-Impl) hvert funktions- eller domænemodul i to separate Gradle-underprojekter: et letvægts `:feature:api`-modul, der indeholder offentlige grænseflader, modeller og navigationskontrakter, og et `:feature:impl`-modul, der indeholder intern forretningslogik, UI og repository-implementationer. Forbrugende moduler afhænger udelukkende af `:feature:api` ved hjælp af `implementation project(':feature:api')`, mens rodmodulet `:app` eller dedikerede sammensætningsrødder forbinder konkrete implementationer via Dependency Injection (f.eks. Dagger/Hilt). Dette mønster optimerer Gradle-byggeydelsen markant gennem ABI-stabilitet (Application Binary Interface) og isolering af kompilerings-classpathen. Når udviklere ændrer implementationsdetaljer i `:feature:impl`, forbliver det offentlige ABI for `:feature:api` uændret. Som følge heraf springer Gradle genkompilering over for alle nedstrømsmoduler, der kun afhænger af `:feature:api`, hvilket maksimerer effektiviteten af Gradle Remote Build Cache og konfigurationscachen. Fra et organisatorisk og styringsmæssigt perspektiv etablerer dette mønster klare ejerskabsskillelinjer på tværs af teams ved hjælp af værktøjer som CODEOWNERS. Teams kan trygt videreudvikle deres interne implementationsdetaljer uden at eksponere private klasser, hvilket forhindrer uønsket tæt kobling og cirkulære afhængigheder på tværs af store teams.