20 ofte stillede interviewspørgsmål til Mobile Developer. Mobiludvikling er et efterspurgt felt med fokus på stabile iOS-, Android- og cross-platform-apps samt produkt-, performance-, release-, offline- og enhedsintegrationsopgaver. Spørgsmålene dækker flere niveauer, og du kan øve dig i at svare mundtligt i vores interviewtræner.
1Hvad er forskellen på at gemme et autentificeringstoken i standard nøgle-værdi-lagring som SharedPreferences eller UserDefaults versus platformssikker lagring som Android Keystore eller iOS Keychain?
Standardlagringsmekanismer som Androids SharedPreferences og iOS' UserDefaults er designet til lette, ikke-følsomme indstillinger. De gemmer data i ukrypterede klartekstfiler (XML eller plist) i applikationens sandkassemappe. Enhver med fysisk adgang til en rootet eller jailbroken enhed, en ukrypteret enhedssikkerhedskopi eller direkte filsystemadgang kan læse disse tokens direkte. I modsætning hertil giver platformssikker lagring — specifikt iOS Keychain og Android Keystore (ofte brugt via EncryptedSharedPreferences) — kryptering af data i hvile (data-at-rest). iOS Keychain krypterer gemte elementer ved hjælp af nøgler knyttet til enhedens hardware (såsom Secure Enclave) og tillader finkornede adgangspolitikker. Android Keystore genererer og gemmer kryptografiske nøgler inde i hardware-isolerede sikkerhedsmoduler (Trusted Execution Environment eller StrongBox), hvilket sikrer, at kryptografiske nøgler aldrig eksponeres i applikationens hukommelse og ikke kan udtrækkes fra filsystemet.
// --- ANDROID ---
// INSECURE (Plain SharedPreferences):
val prefs = context.getSharedPreferences("app_prefs", Context.MODE_PRIVATE)
prefs.edit().putString("auth_token", token).apply() // Plaintext XML on disk
// SECURE (EncryptedSharedPreferences backed by Android Keystore):
val masterKey = MasterKey.Builder(context)
.setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
.build()
val securePrefs = EncryptedSharedPreferences.create(
context,
"secure_prefs",
masterKey,
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
)
securePrefs.edit().putString("auth_token", token).apply()
// --- IOS (Swift) ---
// INSECURE:
UserDefaults.standard.set(token, forKey: "auth_token") // Plain plist on disk
// SECURE (Keychain Services API):
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: "user_auth_token",
kSecValueData as String: token.data(using: .utf8)!,
kSecAttrAccessible as String: kSecAttrAccessibleWhenUnlocked
]
SecItemAdd(query as CFDictionary, nil)
2Hvad er det primære formål med en mobil CI/CD-pipeline (Continuous Integration/Continuous Deployment), og hvilke unikke trin og begrænsninger adskiller den fra web- eller backend-deployment-pipelines?
Det primære formål med en mobil CI/CD-pipeline er at automatisere build, test, signering og distribution af mobile klientapplikationer for at opretholde ensartet kvalitet og reproducerbare udgivelser. Continuous Integration (CI) validerer kodeændringer gennem automatiserede builds, statisk analyse og enhedstest. Continuous Delivery/Deployment (CD) pakker, signerer og distribuerer binære artefakter til testkanaler (såsom TestFlight eller Firebase App Distribution) eller app-butikker. Mobil CI/CD adskiller sig fra web- og backend-pipelines på flere væsentlige punkter: 1. Artefakttype: Builds producerer kompilerede klientbinærer (.ipa, .apk, .aab) i stedet for at køre direkte på servere eller i container-images. 2. Hardwarebegrænsninger for runners: iOS-kompilering kræver macOS-hardware og Xcode-kommandolinjeværktøjer, hvorimod backend-pipelines typisk kører på letvægts-Linux-containere. 3. Udgivelseslatens og godkendelser: Publicering til slutbrugere involverer godkendelsesprocesser hos tredjeparts app-butikker (Apple App Store / Google Play Store), hvilket betyder, at udrulninger ikke kan rulles tilbage øjeblikkeligt via en genudrulning på serveren. Fejlrettelser kræver indsendelse af en ny, signeret binær fil eller brug af feature flags i runtime.
name: Mobile CI/CD Workflow
on:
pull_request:
branches: [main]
push:
tags:
- 'v*.*.*'
jobs:
ci_validation:
name: Lint & Unit Tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run Linter
run: ./gradlew lint
- name: Run Unit Tests
run: ./gradlew testDebugUnitTest
cd_release_ios:
name: Build & Distribute iOS
needs: ci_validation
if: startsWith(github.ref, 'refs/tags/v')
runs-on: macos-14
steps:
- uses: actions/checkout@v4
- name: Install Signing Certificates
run: fastlane match appstore --readonly
- name: Build & Upload to TestFlight
run: fastlane ios beta
3Hvad er formålet med at opdele en tværplatformsmobilapplikation i præsentations-, domæne- og datalag, og hvordan gør denne struktur det lettere at dele kode på tværs af iOS og Android?
Opdelingen af en applikation i præsentations-, domæne- og datalag etablerer klare ansvarsområder: 1. **Præsentationslag:** Håndterer UI-rendering, brugerinput og tilstand på skærmniveau (f.eks. Views, Widgets, ViewModels eller Presenters). 2. **Domænelag:** Indkapsler ren forretningslogik, domænemodeller/entiteter og use cases. Det forbliver uafhængigt af UI og platformsspecifikke frameworks. 3. **Datalag:** Håndterer datahentning og persistens fra eksterne API'er, lokale databaser eller enhedscache via repositories og datakilder. I tværplatforms mobiludvikling gør denne struktur det lettere at dele kode, fordi domæne- og datalagene er platformsagnostiske. Kerneforretningsregler, datatransformationer og netværkshåndtering kan deles 100 % på tværs af iOS og Android, hvilket gør det muligt for teams enten at dele hele stakken (som i Flutter eller React Native) eller dele forretnings- og datalogik, mens man bevarer platformsspecifikke native præsentationslag (som i Kotlin Multiplatform).
[Presentation Layer] (UI, Screens, ViewModels)
│
▼ calls
[Domain Layer] (Use Cases, Business Rules, Entities)
▲
│ implements interfaces
[Data Layer] (Repositories, API Clients, Local Storage)
4Hvad er de grundlæggende forskelle mellem det imperative og det deklarative paradigme for UI-udvikling (User Interface) inden for mobiludvikling, og hvordan udtrykker Flutter-widgets og React Native-komponenter den deklarative model?
I imperativ UI-udvikling skriver udviklere eksplicitte, trinvise instruktioner til at oprette, ændre og fjerne UI-elementer (såsom at finde et view via id og kalde metoder som `setText` eller `setVisibility` direkte). Omvendt beskriver deklarative UI-modeller, hvordan brugergrænsefladen skal se ud for en given tilstand (ofte udtrykt som UI = f(state)). Når tilstanden ændrer sig, afgør frameworket, hvordan den visuelle repræsentation opdateres mest effektivt. Både Flutter og React Native er baseret på dette deklarative paradigme. I Flutter er widgets uforanderlige konfigurationsbeskrivelser; når dynamiske data ændrer sig, planlægger et kald til `setState()` i en `StatefulWidget` en genopbygning, hvor `build()`-metoden returnerer en ny beskrivelse af widget-træet, som Flutter afstemmer mod sine Element- og RenderObject-træer. I React Native er komponenter funktioner eller klasser, der returnerer JSX; opdatering af state eller props udløser en genoptegning, hvor React afstemmer det virtuelle elementtræ og anvender de minimale, nødvendige native opdateringer via broen eller sit native kørselsmiljø.
5Hvad er den driftsmæssige forskel mellem en kold start (cold start), en varm start (warm start) og en hot start i mobilapps, og hvorfor er koldstart den mest kritiske metrik for ydelsesbudgetter (performance budgets)?
I mobilappers livscyklus adskiller opstartsstadierne sig afhængigt af, om appens proces og hukommelsestilstand allerede findes i styresystemet (OS, Operating System).
- Kold start (Cold Start): Appen starter helt forfra, fordi styresystemet endnu ikke har oprettet dens proces, eller processen tidligere er blevet afsluttet. Styresystemet skal allokere en ny proces, indlæse binære filer/kørselsmiljøer, initialisere applikations-/runtime-konteksten og rendere det første view. Det medfører den højeste latenstid.
- Varm start (Warm Start): Appens proces ligger normalt allerede i hukommelsen, men aktiviteten/view-hierarkiet blev destrueret eller fjernet (f.eks. på grund af genoprettelse i baggrunden eller tilbage-navigation). Styresystemet genskaber brugergrænsefladen/aktiviteten uden at skulle forgrene eller starte en helt ny proces fra bunden.
- Hot Start: Appen og dens UI-tilstand ligger stadig fuldt ud i hukommelsen (f.eks. hvis brugeren trykkede Hjem og straks vendte tilbage). Styresystemet bringer blot det eksisterende view-hierarki i forgrunden med næsten ingen initialiseringsomkostninger.
Kold start er den mest kritiske metrik for performance-budgetter, fordi den repræsenterer den langsomste førstegangsoplevelse for brugeren. Lange koldstarter hænger direkte sammen med umiddelbart brugerfrafald, lavere fastholdelse og dårligere placeringer i appbutikker (såsom grænseværdierne i Android Vitals).
6Hvad er en native bridge i en cross-platform mobilapp, og hvornår vil du bruge en i stedet for at skrive funktionaliteten udelukkende i Flutter- eller React Native-kode?
En native bridge er det kommunikationslag, der gør det muligt for cross-platform-kode at kalde platformsspecifik iOS- eller Android-kode, og lader native kode returnere resultater eller sende hændelser tilbage. I Flutter gøres dette typisk med platform channels eller plugins. I React Native gøres det typisk med native moduler, TurboModules eller native UI-komponenter. Man bruger en bridge, når en funktion kræver noget, som ren Flutter- eller React Native-kode ikke har tilstrækkelig adgang til, såsom et platforms-API (Application Programming Interface), enhedsegenskaber, et native SDK (Software Development Kit), en ydelseskritisk native implementering eller en native UI-komponent. Almindelige eksempler omfatter kamerafunktioner, Bluetooth, push-meddelelser, betalinger, sikker lagring, baggrundstjenester, sundheds-API'er eller SDK'er, der kun tilbyder Swift/Objective-C- eller Kotlin/Java-integrationer. En god bridge eksponerer normalt et lille, overskueligt API for cross-platform-laget, såsom getBatteryLevel, startBluetoothScan eller openNativePaymentSheet, mens den platformsspecifikke implementering håndterer de faktiske iOS- og Android-detaljer.
7Hvad betyder en offline-first-arkitektur i mobiludvikling, og hvordan adskiller den sig fundamentalt fra standard HTTP-responscaching (Hypertext Transfer Protocol)?
I mobiludvikling behandler en offline-first-arkitektur lokalt lager som den primære sandhedskilde (source of truth) for både læse- og skriveoperationer. I stedet for at vente på, at netværksforespørgsler fuldføres, før der renderes eller tillades brugerinteraktioner, interagerer appen direkte med den lokale database eller det lokale lagringslag, mens synkroniseringsprocesser i baggrunden håndterer afstemning af lokale ændringer med fjernserveren, når der er forbindelse. Dette adskiller sig fundamentalt fra standard HTTP-responscaching på flere måder: 1. **Datamodel**: HTTP-caching gemmer rå netværkssvar (f.eks. JSON-nyttelaster) indekseret efter forespørgsels-URL'er og -headere. Offline-first-arkitektur gemmer strukturerede domæneentiteter i en lokal database (såsom Room, SQLite eller SwiftData/Core Data). 2. **Skrivning vs. læsning**: HTTP-caching er primært en mekanisme til optimering af læsninger og understøtter ikke nativt offline-skrivetransaktioner eller -mutationer. Offline-first tillader fulde lokale skrivninger og sætter mutationer i kø til senere serversynkronisering. 3. **Forespørgselsmuligheder og levetid**: Cachede HTTP-svar er underlagt automatiske cache-udsmidningspolitikker og kan ikke vilkårligt forespørges, filtreres eller sammensættes via joins. Persistering i offline-first giver fuld understøttelse af forespørgsler, indeksering og deterministisk levetidsstyring uafhængigt af netværksstatus.
/* Network-First with HTTP Caching */
UI -> Network Request -> [Cache Hit ? Return Cached JSON : Fetch from API -> Save to HTTP Cache] -> UI
// Limitation: Read-only optimization; offline writes fail immediately.
/* Offline-First with Local Source of Truth */
UI <-> Observes Local DB (e.g., Room / Core Data / SQLite)
User Write -> Write to Local DB -> Queue Sync Task
Sync Worker (Background) -> Send queued mutations to Server -> Update Local DB with Server Response
8Hvad er forskellen mellem en remote push-notifikation og en lokal notifikation i en mobilapp?
Den grundlæggende forskel mellem lokale og remote push-notifikationer ligger i, hvor de har deres oprindelse, og hvordan de udløses: 1. Lokale notifikationer oprettes, planlægges og udløses udelukkende på enheden af applikationen ved hjælp af operativsystemets API'er (såsom UserNotifications på iOS eller AlarmManager/WorkManager/NotificationManager på Android). De udløses baseret på betingelser på enheden, såsom en bestemt dato/tid, en nedtællingstimer eller en geografisk grænse (geofencing). Da de kører lokalt via operativsystemets dæmon, kræver de ikke en backend-server eller en aktiv internetforbindelse på leveringstidspunktet. 2. Remote push-notifikationer stammer fra en ekstern applikationsserver og overføres over internettet via platformens push-gateways (APNs for Apple-enheder, FCM for Android). Gatewayen leverer dataene (payload) til enhedens dæmon på OS-niveau, som vækker appen eller viser et banner. Remote-notifikationer er nødvendige ved eksterne hændelser i realtid, såsom indgående chatbeskeder, venneanmodninger eller breaking news.
let content = UNMutableNotificationContent()
content.title = "Workout Reminder"
content.body = "Time for your daily exercise routine."
content.sound = .default
// Triggers locally after 1 hour (3600 seconds) without any server interaction
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 3600, repeats: false)
let request = UNNotificationRequest(identifier: "dailyWorkout", content: content, trigger: trigger)
UNUserNotificationCenter.current().add(request) { error in
if let error = error {
print("Failed to schedule notification: \(error)")
}
}
9Hvad er den strukturelle og driftsmæssige forskel mellem et Android App Bundle (.aab), en APK (Android Package) og et iOS-IPA-arkiv (iOS App Store Package) ved distribution af apps via app-butikker?
En APK (Android Package) er et eksekverbart pakkeformat, der indeholder kompileret DEX-bytekode, ressourcer, assets og native biblioteker, som kan installeres og afvikles direkte på en Android-enhed. En universel APK samler ressourcer til alle skærmtætheder, CPU-arkitekturer og sprog, hvilket fører til større downloadstørrelser. Et Android App Bundle (.aab) er et upload- og udgivelsesformat, som Google Play kræver til butiksdistribution. En AAB kan ikke installeres direkte på en enhed; i stedet bruger Google Play bundlet og Play App Signing til at generere optimerede split-APK'er, der dynamisk er skræddersyet til en specifik enheds arkitektur, skærmtæthed og sprog (Dynamic Delivery). En iOS-IPA (.ipa) er en applikationsarkivfil (en zip-container, der indeholder Payload-mappen, `.app`-bundlet, signeringsaktiver og metadata). Når den uploades til App Store Connect, udfører Apple App Thinning (såsom App Slicing) for kun at levere de assets og binære filer, som den downloadende enhed har brug for. Operationelt kan APK'er og IPA'er installeres direkte på fysiske testenheder (forudsat korrekt signering og provisionering), hvorimod en AAB først skal konverteres til split-APK'er (f.eks. ved hjælp af `bundletool`) før lokal installation på en enhed.
# 1. Standalone APK installs directly via ADB
adb install myapp-universal.apk
# 2. AAB requires bundletool to generate device-tailored split APKs
bundletool build-apks --bundle=myapp.aab --output=myapp.apks --connected-device
# 3. Install the generated split APK set onto the connected device
bundletool install-apks --apks=myapp.apks
10Hvilken beskyttelse giver TLS (Transport Layer Security) til en mobilapplikations netværkskald, og hvordan håndhæver moderne mobile styresystemer sikre transportstandarder?
TLS (Transport Layer Security) beskytter mobile netværkskald ved at give tre essentielle garantier: fortrolighed (kryptering af data under overførsel, så aflyttere ikke kan læse payloads eller headers), dataintegritet (registrering af enhver manipulation eller ændring af forespørgsler og svar under overførsel) og serverautentificering (validering af serverens digitale certifikat mod betroede certifikatmyndigheder (Certificate Authorities) for at forhindre manden-i-midten-angreb (Man-in-the-Middle)). Moderne mobile styresystemer håndhæver som standard sikker transport ved at blokere ukrypteret HTTP-klarteksttrafik: 1. iOS håndhæver App Transport Security (ATS), som kræver, at netværksforbindelser (såsom dem via URLSession) anvender HTTPS med TLS 1.2+, medmindre der eksplicit er defineret domæneundtagelser i Info.plist. 2. Android (API 28+) deaktiverer som standard HTTP-klarteksttrafik (`cleartextTrafficPermitted=false`). Tilladelse af ukrypteret HTTP kræver eksplicitte undtagelser via Network Security Configuration (`network_security_config.xml`) eller manifest-attributten `android:usesCleartextTraffic`.
11Hvordan implementerer du biometrisk godkendelse (Face ID / fingeraftryk) med platformens sikre lagring for at sikre, at kryptografiske nøgler kun låses op efter vellykket biometrisk verifikation?
For sikkert at beskytte kryptografiske nøgler bag biometri må en app ikke udelukkende stole på et boolsk UI-callback (User Interface) fra en biometrisk prompt. I stedet skal kryptografiske nøgler genereres og lagres i hardwarebeskyttet lagring (Secure Enclave på iOS, Android Keystore / StrongBox på Android), konfigureret med adgangskontrolpolitikker, der håndhæver biometrisk godkendelse forud for brug af nøglen. På iOS konfigureres nøgler eller Keychain-elementer ved hjælp af `SecAccessControlCreateWithFlags` med flag som `.biometryCurrentSet` (eller `.userPresence`). Når den private nøgle anmodes til signering eller dekryptering, beder styresystemet automatisk brugeren om Face ID / Touch ID. På Android genereres nøgler ved hjælp af `KeyGenParameterSpec.Builder` med `.setUserAuthenticationRequired(true)`. En kryptografisk operation (såsom en `Cipher` eller `Signature`) initialiseres og overføres som et `BiometricPrompt.CryptoObject` til `BiometricPrompt.authenticate()`. Nøglen låses kun op og er anvendelig i `onAuthenticationSucceeded` via dette godkendte `CryptoObject`. Brugen af `.biometryCurrentSet` (iOS) eller `setInvalidatedByBiometricEnrollment(true)` (Android) sikrer, at hvis et nyt fingeraftryk eller biometrisk kendetegn registreres på enheden, bliver de eksisterende nøgler permanent ugyldige, hvilket modvirker uautoriseret adgang, hvis enhedens adgangskode kompromitteres.
val keyGen = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore")
val spec = KeyGenParameterSpec.Builder(
"biometric_auth_key",
KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT
)
.setBlockModes(KeyProperties.BLOCK_MODE_GCM)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
.setUserAuthenticationRequired(true)
.setInvalidatedByBiometricEnrollment(true)
.build()
keyGen.init(spec)
keyGen.generateKey()
12Hvilke caching-strategier og pipeline-optimeringer kan du implementere for markant at reducere build-tider for pull requests på cross-platform mobile CI-runnere (Continuous Integration)?
For drastisk at reducere build-tider for pull requests på mobile CI-runnere skal optimeringer målrettes mod resolving af afhængigheder, caching af kompilering og selektiv eksekvering. For det første: implementér effektiv caching af afhængigheder med nøgler baseret på hashes af lock-filer (f.eks. `package-lock.json`, `Podfile.lock`, `gradle/verification-metadata.xml` eller SPM package resolved-filer). Dette forhindrer download eller gentagen resolving af eksterne pakker på nye runner-instanser. For det andet: konfigurer native build-caches. For Android aktiveres Gradle Build Cache (`--build-cache`) med ekstern HTTP-caching eller CI-native persistent caching for `~/.gradle/caches` og build-mapper. For iOS caches Xcode `DerivedData` og kompileringsartefakter fra Swift Package / CocoaPods omhyggeligt, eller man kan udnytte moderne værktøjer til remote caching såsom Tuist eller Bazel. For React Native/Flutter caches JS-bundle-outputtet, `node_modules` og Flutter engine SDK-cachen. For det tredje: anvend selektiv jobafvikling (ændringsregistrering og test impact analysis). Brug Git-stifiltre til at springe iOS-builds over, når kun Android- eller backend-filer er ændret, kør hurtige lint- og enhedstest-trin før ressourcekrævende UI-tests eller kompileringsfaser, og undgå at generere komplette release-binærer (såsom AAB'er eller universelle IPA'er) på PR-kørsler, hvor kun et simulator-/debug-build eller en enhedstestkørsel er nødvendig.
name: PR Fast Check
on:
pull_request:
paths:
- 'android/**'
- 'shared/**'
jobs:
android-unit-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with:
distribution: 'zulu'
java-version: '17'
# Gradle build caching for dependencies & task outputs
- uses: gradle/actions/setup-gradle@v3
with:
cache-read-only: false
- name: Run Unit Tests & Lint Only
run: ./gradlew testDebugUnitTest lintDebug --build-cache --parallel
13Hvordan vil du strukturere en delt funktion, så præsentation, domæne-use cases og dataadgang forbliver uafhængigt testbare og genanvendelige på tværs af iOS og Android?
For at strukturere en delt funktion med henblik på uafhængig testbarhed og genanvendelighed på tværs af iOS og Android anvendes en ren, lagdelt arkitektur opdelt i præsentations-, domæne- og datalag: 1. **Domænelag (ren delt logik):** Indeholder rene forretningsentiteter, repository-interfaces (kontrakter) og use cases/interactors (f.eks. `GetCartUseCase`, `ApplyPromoCodeUseCase`). Dette lag har nul afhængigheder til UI-frameworks (UIKit, SwiftUI, Android Views, Jetpack Compose, Flutter-widgets) eller platformsspecifikke API'er. Hver use case indkapsler en enkelt forretningsoperation, hvilket gør den 100 % enhedstestbar med almindelige mocks. 2. **Datalag (dataadgang og indkapsling):** Implementerer domænets repository-interfaces. Det interagerer med eksterne datakilder (REST/GraphQL) og lokal persistens (SQLite/Key-Value). Netværks-DTO'er og databaseentiteter mappes strengt til rene domæneentiteter, inden de forlader dette lag, hvilket forhindrer eksterne serialiseringsskemaer eller databaseændringer i at sive ind i domæne- eller UI-lagene. 3. **Præsentationslag (UI og tilstandshåndtering):** Består af tilstandsholdere (ViewModels, Blocs eller Presenters) og UI-views/widgets. Tilstandsholderen kalder domæne-use cases, håndterer UI-tilstand (loading, succes, fejl) og udstiller observerbar tilstand til viewet. Viewet observerer blot denne tilstand og renderer den. Denne struktur gør det muligt at enhedsteste use cases isoleret uden en brugergrænseflade, mocke repositories for at teste view models og udskifte præsentationsimplementeringer mellem platforme, mens hele domæne- og datalogikken genbruges.
[ UI View / Compose / SwiftUI ]
|
v (Observes State / Dispatches Events)
[ ViewModel / Presenter / BLoC ] (Presentation Layer)
|
v (Executes pure business operations)
[ Use Case / Interactor ] (Domain Layer - Pure Shared Logic)
|
v (Calls interface contract)
[ Repository Interface ] (Domain Layer Contract)
^
| (Implements)
[ Repository Implementation ] (Data Layer)
|---- Maps NetworkDTO -> DomainEntity
|---- Maps DatabaseEntity -> DomainEntity
[ API Client / Local DB / Storage ]
14Hvordan vurderer du, hvornår lokal komponenttilstand ikke længere er tilstrækkelig, og vælger en passende arkitektonisk tilgang til tilstandshåndtering i en tværplatformskodebase med flere udviklere?
I tværplatforms mobiludvikling drives overgangen fra lokal komponenttilstand (`useState`/`StatefulWidget`) til en arkitektonisk tilstandshåndteringsløsning af tilstandens synlighedsområde (scope), livscykluskrav og testbarhed: 1. **Hvornår lokal tilstand er utilstrækkelig**: - **Deling på tværs af komponenter og prop-drilling**: Når tilstand skal tilgås eller ændres på tværs af separate navigationsruter eller fjerntliggende grene i widget-/komponenttræet. - **Overlevelse på tværs af livscyklus**: Når data skal overleve nedlæggelse af skærmvisninger, ruteovergange eller nulstilling af navigation (f.eks. brugersession, indkøbskurv, cachelagrede feeds). - **Adskillelse af ansvarsområder og testbarhed**: Når forretningslogik, validering og sideeffekter bliver sammenflettet med UI-komponenter, hvilket gør automatiseret, headless enhedstest svær. 2. **Valg af arkitektur til kodebaser med flere udviklere**: - **Ensrettet dataflow / forudsigelige containere (f.eks. BLoC, Redux, Riverpod)**: Håndhæver en streng adskillelse, hvor brugergrænsefladen afsender eksplicitte hændelser/handlinger og renderer uforanderlig tilstand leveret af dedikerede enheder til forretningslogik. Dette skaber klare kontrakter, reducerer sideeffekter og muliggør uafhængig enhedstest af forretningslogik. - **Atomare / afgrænsede reaktive lagre (f.eks. Zustand, MobX, Provider)**: Tilbyder mindre boilerplate og fleksible selektor-abonnementer (selector subscriptions), hvilket er velegnet, når teams ønsker letvægtsafkobling og målrettede gen-renderinger af komponenter. I et teammiljø er det primære arkitektoniske mål at isolere ren forretningslogik fra UI-renderingslag og anvende selektive abonnementer for at forhindre unødvendig gen-rendering af hele komponenttræet.
15Hvordan vil du undersøge og reducere hakken (jank) på en skærm i en cross-platform mobilapp, der hakker under scrolling gennem en lang liste med billeder og dynamisk indhold?
Jeg ville først reproducere hakkeriet på fysiske enheder og profilere det, da scrolling-jank kan stamme fra flere steder: overskredet frame-budget, JS- eller Dart-arbejde, arbejde på hoved-/UI-tråden, GPU/raster-arbejde, layoutberegninger, afkodning af billeder, hukommelsespres eller netværksindlæsning. Ved 60 Hz har appen cirka 16,7 ms pr. frame, og ved 120 Hz cirka 8,3 ms, så tung rendering, afkodning, layout eller synkrone beregninger under scrolling kan forårsage droppede frames. Jeg ville bruge værktøjer som Flutter DevTools, React Native performance tools eller Flipper, Android Studio Profiler, Xcode Instruments og visninger af frame-tidslinjer til at identificere den reelle flaskehals. For den lange liste ville jeg sikre, at den er lazy-loaded eller virtualiseret: for eksempel `FlatList`, `FlashList` eller `RecyclerListView` i React Native, eller `ListView.builder`, `SliverList` eller lignende builders i Flutter. Jeg ville bruge stabile nøgler, undgå at genopbygge eller re-rendre hver række, når forældertilstanden ændrer sig, memoize rækkekomponenter eller selektorer, hvor det er relevant, holde `build`/`renderItem` lette og flytte sortering, filtrering, JSON-parsing, formatering eller billedbehandling væk fra scroll-stien. Hvis elementstørrelser er forudsigelige, ville jeg give layout-hints såsom `getItemLayout` i React Native eller faste/prototype-elementudstrækninger i Flutter. For billeder ville jeg levere miniaturebilleder i passende størrelse, cache dem, undgå at afkode billeder i fuld opløsning til små celler, bruge placeholders og lazy loading samt holde øje med hukommelsesbelastning fra for mange store bitmaps. Jeg ville også forenkle alt for komplekse rækkelayouts, reducere tunge skygger/clipping/overdraw/ugennemsigtighed, hvor det er relevant, batch-opdele eller paginere data og derefter profilere igen for at bekræfte, at droppede frames og frame-tider er forbedret.
1. Record a trace while scrolling on a real device.
2. Check frame timeline: are frames exceeding 16.7 ms / 8.3 ms?
3. Identify bottleneck: JS/Dart, main/UI thread, raster/GPU, image decode, memory, or network.
4. Fix the largest measured bottleneck: virtualization, image resizing/caching, row memoization, layout simplification, moving work off scroll path.
5. Re-test on low-end and target-refresh-rate devices.
16Hvordan fungerer trådgrænser på tværs af native bridge-kald, og hvordan forhindrer man pauser ved trådskift (thread-hopping) eller UI-hakken (UI (User Interface) jank), når native moduler udfører tungt baggrundsarbejde?
Cross-platform-frameworks benytter specifikke trådkonventioner til bridge-kald: Standardmæssige Flutter `MethodChannel`-kald ankommer for eksempel på platformens primære UI-tråd, mens traditionelle React Native-bridge-kald kører på en dedikeret JavaScript-tråd og fordeles asynkront til native tråde. Når en native bridge-metode udføres på den native UI-/hovedtråd, vil CPU-tunge operationer, langvarig synkron disk-I/O eller blokerende netværksopgaver blokere hovedhændelsesløkken (main run loop). Dette medfører dropped frames, synlig UI-hakken og på Android ANR-fejl (Application Not Responding) eller nedlukning via iOS-watchdog. For at forhindre dette bør native bridge-handlere uddelegere tungt eller blokerende arbejde til baggrundstrådpuljer (f.eks. Kotlin Coroutines med `Dispatchers.IO`/`Dispatchers.Default`, Android `ThreadPoolExecutor` eller Swift `Task.detached` / GCD `DispatchQueue.global()`). Når arbejdet er færdigt, skal resultatet sendes tilbage til den bridge-tråd, som frameworket forventer (f.eks. ved at returnere resultater på UI-tråden for standard-MethodChannels eller bruge baggrundsopgavekøer), hvilket sikrer, at cross-platform-tilstandsopdateringer ikke blokerer UI-renderingcyklussen.
class ImageProcessorPlugin : MethodChannel.MethodCallHandler {
private val scope = CoroutineScope(Dispatchers.Default + SupervisorJob())
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
if (call.method == "applyFilter") {
val imageBytes = call.argument<ByteArray>("image") ?: return result.error("INVALID_ARG", "Image null", null)
// Dispatch heavy computation off the main UI thread
scope.launch {
try {
val processed = processBitmapBytes(imageBytes) // CPU heavy work
withContext(Dispatchers.Main) {
result.success(processed)
}
} catch (e: Exception) {
withContext(Dispatchers.Main) {
result.error("PROCESSING_FAILED", e.localizedMessage, null)
}
}
}
} else {
result.notImplemented()
}
}
}
17Hvordan ville du designe en caching- og ugyldiggørelsesstrategi for liste- og feed-skærme ved hjælp af Stale-While-Revalidate og betingede HTTP (Hypertext Transfer Protocol)-headere?
En robust caching- og ugyldiggørelsesstrategi for feed- og listeskærme kombinerer Stale-While-Revalidate-mønsteret (SWR) med betingede HTTP-valideringsheadere (såsom `ETag` / `If-None-Match` eller `Last-Modified` / `If-Modified-Since`). Når skærmen åbnes, læser og renderer den straks cachede data fra en lokal database (Single Source of Truth, f.eks. Room eller Core Data/SQLite), hvilket giver brugeren en øjeblikkelig UI-visning. Samtidig afsendes en baggrundsnetværksforespørgsel med den cachede `If-None-Match: <etag>`-header. Hvis serveren svarer med `304 Not Modified`, overføres der intet payload, hvilket validerer den lokale cache, mens der spares båndbredde og batteri. Hvis serveren svarer med `200 OK`, skrives de nye data og det opdaterede `ETag` til den lokale database i en transaktion, og reaktive database-observers udsender automatisk den opdaterede liste til brugergrænsefladen. Til paginering gemmes sidetokens eller offsets sammen med cachede poster. Ugyldiggørelse (invalidation) udløses af TTL-udløb (Time-To-Live), brugerhandlinger (såsom pull-to-refresh, der omgår betingede headere) eller lokale mutationsbivirkninger (f.eks. når oprettelse eller sletning af et element lokalt opdaterer databasen optimistisk og markerer cachede sidemarkører til revalidering).
fun getFeed(): Flow<List<FeedItem>> = flow {
val cached = feedDao.getFeedItems()
if (cached.isNotEmpty()) emit(cached)
val lastEtag = feedDao.getFeedEtag()
try {
val response = api.fetchFeed(ifNoneMatch = lastEtag)
if (response.code() == 200 && response.body() != null) {
feedDao.updateFeedTransaction(response.body()!!, response.headers()["ETag"])
emit(feedDao.getFeedItems())
}
} catch (e: Exception) {
if (cached.isEmpty()) throw e
}
}
18En finansiel mobilapplikation skal opbevare refresh-tokens sikkert på enheden, understøtte biometrisk oplåsning, overleve OS-opgraderinger (Operating System) og fungere sikkert under midlertidige offline-sessioner. Hvordan ville du designe denne arkitektur til tokenlagring og -adgang?
En robust mobil tokenlagringsarkitektur anvender hardware-baserede sikkerhedsmoduler: iOS Keychain understøttet af Secure Enclave og Android Keystore understøttet af StrongBox eller et TEE (Trusted Execution Environment). Refresh-tokens skal krypteres under hvile (at rest) ved hjælp af nøgler beskyttet af biometrisk kryptografisk kontrol (gating) (såsom `kSecAccessControlBiometryCurrentSet` eller `kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly` på iOS og `setUserAuthenticationRequired(true)` med `AUTH_BIOMETRIC_STRONG` på Android). Kryptografisk gating sikrer, at den kryptografiske nøgle kun udpakkes af hardwaren ved vellykket biometrisk godkendelse, frem for at stole på et boolean-tjek i applikationskoden, der kan omgås. For at overleve OS-opgraderinger og samtidig forhindre uautoriseret adgang bindes nøgler til enhedens hardware, mens der håndhæves politikker for registreringsændringer, som ugyldiggør eller kræver fornyet godkendelse, hvis der registreres ny biometri (f.eks. `setInvalidatedByBiometricEnrollment(true)` på Android). Til midlertidige offline-sessioner kan kortlivede krypterede adgangstokens og begrænsede lokale offline-sessionstilstande fungere inden for definerede lokale TTL'er (Time To Live) og afgrænsede rettigheder, mens privilegerede refresh-handlinger udskydes, indtil netværksforbindelsen vender tilbage. Ved genoprettelse af forbindelsen validerer refresh-token-rotation med replay-detektering af engangsbrug på backend sessionen; hvis genbrug af tokens eller tilbagekaldelse af en konto registreres, returnerer backenden et ugyldiggørelsessignal, der får klienten til at slette hardware-baserede nøgler og rydde det lokale offline-lager.
val keyStore = KeyStore.getInstance("AndroidKeyStore").apply { load(null) }
val keyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore")
val spec = KeyGenParameterSpec.Builder(
"refreshTokenKeyAlias",
KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT
)
.setBlockModes(KeyProperties.BLOCK_MODE_GCM)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
.setUserAuthenticationRequired(true)
.setUserAuthenticationParameters(0, KeyProperties.AUTH_BIOMETRIC_STRONG)
.setInvalidatedByBiometricEnrollment(true)
.build()
keyGenerator.init(spec)
keyGenerator.generateKey()
19Hvordan ville du designe en skalerbar CI/CD-infrastruktur (Continuous Integration/Continuous Deployment) til et platformsuafhængigt mobil-monorepo, der understøtter flere teams, samtidig med at du håndterer begrænset og dyr kapacitet på macOS-runners?
For at designe en skalerbar CI/CD-infrastruktur til et cross-platform mobil-monorepo og samtidig optimere knap og dyr macOS-kapacitet skal arkitekturen baseres på grafbevidst detektering af berørte builds (affected builds), streng opdeling af arbejdsbyrder på tværs af hybride runner-flåder samt virtualisering og flygtig (ephemeral) orkestrering til macOS. Først implementeres et værktøj til monorepo-buildgrafer (såsom Bazel, Nx eller Turborepo) med distribueret fjerncaching. Ved hver pull request beregner CI-pipelinen differencen i den rettede asykliske graf (DAG) i forhold til målbranchen for kun at bygge og teste berørte pakker og deres downstream-afhængigheder, hvilket fuldstændigt forbigår uændrede moduler. Dernæst implementeres en strategi for hybrid runner-allokering: aflast alle opgaver, der ikke strengt kræver Xcode (såsom TypeScript/Dart-linting og -kompilering, enhedstest, statisk kodeanalyse, sikkerhedsscanning og Android Gradle-builds), til omkostningseffektive, skalerbare Linux/Kubernetes-runners. Reserver macOS-runners udelukkende til endelig iOS-samling, Swift/Objective-C-kompilering, kodesignering og afvikling af iOS Simulator-tests. For det tredje administreres macOS-runners ved hjælp af virtualiseringsinfrastruktur (såsom Tart, Anka eller AWS/MacStadium bare-metal-noder orkestreret via Nomad eller Kubernetes). Hvert iOS-build kører i en ren, flygtig virtuel maskine med forvarmede toolchains og derived-data-caches, hvilket forhindrer runner-drift og muliggør hurtig autoskalering baseret på pipelinens kødybde.
20Hvordan vil du designe en mobilarkitektur på tværs af platforme (cross-platform) til et produkt, der forventes at dele det meste af forretningslogikken mellem iOS og Android, mens platforms-specifik UI, navigation og native integrationer stadig skal kunne udvikle sig uafhængigt?
Jeg ville designe appen opbygget omkring en delt kerne, der ejer stabil forretningsadfærd: domænemodeller, validering, use cases, forretningsregler og kontrakter for dataadgang. Platforms-specifik UI, navigation, native SDK'er, rettigheder, livscyklushåndtering og anliggender vedrørende app-skallen (app shell) bør holdes uden for denne kerne. Den delte kode bør ikke importere UIKit, SwiftUI, Jetpack, Android-framework-API'er, React Native-navigation, Flutter-navigation eller detaljer om native SDK'er. I stedet bør den afhænge af snævre grænseflader såsom AuthRepository, SecureStorage, Analytics, CameraService eller PaymentProvider, som hver platform implementerer. Jeg ville organisere koden efter funktion (feature) så vidt muligt frem for at skabe ét stort delt lag. For eksempel kan betaling, søgning, konto og beskeder hver have delt domæne-/use case-kode, tilstandskontrakter og repository-grænseflader. iOS- og Android-skallerne ejer derefter skærmsammensætning, native UI-mønstre, navigationsstakke, rettigheder og adapter-sammenkobling. Præsentationslogik kan deles, når den er fuldstændig uafhængig af UI-frameworket, såsom reducere, tilstandsmaskiner eller view-model-kontrakter, der udsender simpel tilstand og handlinger, men faktiske visninger og navigation bør forblive platformsejede, så hver platform kan udvikle sig uafhængigt. Det primære kompromis består i aggressivt at dele fælles, stabil forretningslogik, mens man undgår abstraktioner, der skjuler reelle platformsforskelle. Jeg ville håndhæve grænser med regler for modulafhængigheder, dependency injection, offentlige API'er, kontrakttests, arkitekturkontroller og klart ejerskab. Native integrationer bør anvende porte/adaptere, så den delte funktionskode ser en fælles funktionalitet, mens hver platform håndterer SDK-adfærd, livscyklus, rettigheder, fejl og UX-forskelle i sit eget lag.