20 veelgestelde interviewvragen voor Mobile Developer. Mobiele ontwikkeling is een veelgevraagd vakgebied voor betrouwbare iOS-, Android- en cross-platformapps, met product-, performance-, release-, offline- en device-integratietaken. De vragen behandelen verschillende niveaus, en je kunt hardop oefenen met antwoorden in onze interviewtrainer.
1Wat is het verschil tussen het opslaan van een authenticatietoken in standaard sleutel-waarde-opslag zoals SharedPreferences of UserDefaults versus beveiligde platformopslag zoals Android Keystore of iOS Keychain?
Standaard opslagmechanismen zoals SharedPreferences in Android en UserDefaults in iOS zijn ontworpen voor lichte, niet-gevoelige voorkeuren. Ze slaan gegevens op in niet-versleutelde tekstbestanden (XML of plist) binnen de sandbox-directory van de applicatie. Iedereen met fysieke toegang tot een geroot of gejailbreakt apparaat, een niet-versleutelde back-up van het apparaat of toegang tot het bestandssysteem kan deze tokens direct uitlezen. Daarentegen biedt beveiligde platformopslag — specifiek iOS Keychain en Android Keystore (vaak gebruikt via EncryptedSharedPreferences) — versleuteling van data-in-rust. iOS Keychain versleutelt opgeslagen items met behulp van sleutels die gekoppeld zijn aan de hardware van het apparaat (zoals de Secure Enclave) en biedt fijnmazig toegangsbeleid. Android Keystore genereert en bewaart cryptografische sleutels binnen hardwarematig geïsoleerde beveiligingsmodules (Trusted Execution Environment of StrongBox), waardoor cryptografische sleutels nooit worden blootgesteld in het applicatiegeheugen en niet uit het bestandssysteem kunnen worden gehaald.
// --- 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)
2Wat is het primaire doel van een mobiele CI/CD-pipeline (Continuous Integration/Continuous Delivery) en welke unieke fasen en beperkingen onderscheiden deze van deployment-pipelines voor het web of de backend?
Het primaire doel van een mobiele CI/CD-pipeline is het automatiseren van het bouwen, testen, ondertekenen en distribueren van mobiele clientapplicaties om een consistente kwaliteit en herhaalbare releases te waarborgen. Continuous Integration (CI) valideert codewijzigingen via geautomatiseerde builds, statische codeanalyse en unittesten. Continuous Delivery/Deployment (CD) verpakt, ondertekent en distribueert binaire artefacten naar testkanalen (zoals TestFlight of Firebase App Distribution) of appstores.
Mobiele CI/CD verschilt op verschillende belangrijke punten van web- en backend-pipelines:
1. Type artefact: Builds produceren gecompileerde binaire clientbestanden (.ipa, .apk, .aab) in plaats van code die direct op servers of in container-images draait.
2. Hardwarebeperkingen van runners: Compilatie voor iOS vereist macOS-hardware en Xcode-opdrachtregeltools, terwijl backend-pipelines doorgaans op lichtgewicht Linux-containers draaien.
3. Releaselatentie en goedkeuringen: Publiceren naar eindgebruikers omvat reviewprocessen van externe appstores (Apple App Store / Google Play Store), wat betekent dat deployments niet direct kunnen worden teruggedraaid via een nieuwe serverimplementatie. Oplossingen vereisen het indienen van een nieuw ondertekend binair bestand of het gebruik van runtime-featureflags.
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
3Wat is in een cross-platform mobiele applicatie het doel van het scheiden van presentatie-, domein- en datalagen, en hoe vergemakkelijkt deze structuur het delen van code tussen iOS en Android?
Het opdelen van een applicatie in presentatie-, domein- en datalagen zorgt voor duidelijke scheiding van verantwoordelijkheden:
1. **Presentatielaag:** Verwerkt de UI-rendering, gebruikersinvoer en de status op schermniveau (bijvoorbeeld Views, Widgets, ViewModels of Presenters).
2. **Domeinlaag:** Kapselt pure bedrijfslogica, domeinmodellen/entiteiten en use cases in. Deze laag blijft onafhankelijk van UI- en platformframeworks.
3. **Datalaag:** Beheert het ophalen en persisteren van gegevens via externe API's, lokale databases of de apparaatcache met behulp van repositories en databronnen.
In cross-platform mobiele ontwikkeling vergemakkelijkt deze structuur het delen van code doordat de domein- en datalagen platformonafhankelijk zijn. Kernbedrijfsregels, datatransformaties en netwerkcommunicatie kunnen 100% worden gedeeld tussen iOS en Android. Hierdoor kunnen teams ervoor kiezen om de volledige stack te delen (zoals in Flutter of React Native) of bedrijfs- en datalogica te delen met behoud van platformspecifieke native presentatielagen (zoals in Kotlin Multiplatform).
[Presentation Layer] (UI, Screens, ViewModels)
│
▼ calls
[Domain Layer] (Use Cases, Business Rules, Entities)
▲
│ implements interfaces
[Data Layer] (Repositories, API Clients, Local Storage)
4Wat zijn de belangrijkste verschillen tussen imperatieve en declaratieve UI-ontwikkelparadigma's (User Interface) in mobiele softwareontwikkeling, en hoe belichamen Flutter-widgets en React Native-componenten het declaratieve model?
Bij imperatieve UI-ontwikkeling schrijven ontwikkelaars expliciete, stapsgewijze instructies om UI-elementen aan te maken, aan te passen en te verwijderen (zoals het zoeken van een view op ID en het direct aanroepen van methoden zoals `setText` of `setVisibility`). Declaratieve UI-modellen beschrijven daarentegen hoe de UI eruit moet zien voor een bepaalde status (vaak uitgedrukt als UI = f(state)). Wanneer de status verandert, bepaalt het framework hoe de visuele weergave efficiënt wordt bijgewerkt. Zowel Flutter als React Native belichamen dit declaratieve paradigma. In Flutter zijn widgets onveranderlijke configuratiebeschrijvingen; wanneer dynamische gegevens veranderen, plant het aanroepen van `setState()` in een `StatefulWidget` een herbouw, waarbij de `build()`-methode een nieuwe widgetstructuurbeschrijving retourneert die Flutter afstemt op zijn Element- en RenderObject-structuren. In React Native zijn componenten functies of klassen die JSX retourneren; het bijwerken van de status (`state`) of eigenschappen (`props`) activeert een her-rendering waarin React de virtuele elementstructuur vergelijkt en de minimaal noodzakelijke native updates toepast via de bridge of native runtime.
5Wat is het operationele verschil tussen een cold start, een warm start en een hot start in mobiele applicaties, en waarom is de cold start de meest kritieke metriek voor prestatiebudgetten?
In de levenscyclus van mobiele apps verschillen de opstarttoestanden op basis van de vraag of het proces en de geheugentoestand van de app al aanwezig zijn in het besturingssysteem (OS, Operating System).
- Cold start: De app start vanaf nul op omdat het OS het proces nog niet heeft aangemaakt, of omdat het proces eerder is beëindigd. Het OS moet een nieuw proces toewijzen, binaries en runtime laden, de Application- en runtimecontext initialiseren en de eerste view opbouwen. Dit heeft de hoogste latentie.
- Warm start: Het proces van de app bevindt zich meestal al in het geheugen, maar de activity- of viewhiërarchie is vernietigd of vrijgegeven (bijvoorbeeld door heraanmaak op de achtergrond of terugnavigatie). Het OS maakt de UI of activity opnieuw aan zonder dat er een volledig nieuw proces hoeft te worden gestart.
- Hot start: De app en de UI-toestand bevinden zich nog volledig in het geheugen (bijvoorbeeld wanneer de gebruiker op Home heeft gedrukt en direct terugkeert). Het OS brengt de bestaande viewhiërarchie enkel naar de voorgrond met nagenoeg verwaarloosbare initialisatie overhead.
De cold start is de meest kritieke metriek voor prestatiebudgetten omdat deze de traagste eerste indruk voor gebruikers vormt. Lange cold starts correleren direct met het vroegtijdig afhaken van gebruikers, lagere retentie en slechtere beoordelingen in appwinkels (zoals drempelwaarden in Android Vitals).
6Wat is een native bridge in een cross-platform mobiele app, en wanneer gebruik je deze in plaats van de functionaliteit volledig in Flutter- of React Native-code te schrijven?
Een native bridge is de communicatielaag waarmee cross-platform code platformspecifieke iOS- of Android-code kan aanroepen, en waarmee native code resultaten of gebeurtenissen kan terugsturen. In Flutter gebeurt dit doorgaans via platformkanalen of plugins. In React Native gebeurt dit meestal met native modules, TurboModules of native UI-componenten. Je gebruikt een bridge wanneer een functionaliteit iets vereist waar pure Flutter- of React Native-code onvoldoende toegang toe heeft, zoals een platform-API, apparaatmogelijkheid, native SDK (Software Development Kit), prestatiekritieke native implementatie of native UI-component. Veelvoorkomende voorbeelden zijn camerafunctionaliteiten, Bluetooth, pushnotificaties, betalingen, beveiligde opslag, achtergrondservices, gezondheids-API's of SDK's die uitsluitend Swift/Objective-C- of Kotlin/Java-integraties bieden. Een goede bridge biedt meestal een compacte, duidelijke API aan de cross-platform laag (zoals `getBatteryLevel`, `startBluetoothScan` of `openNativePaymentSheet`), terwijl de platformspecifieke implementatie de werkelijke iOS- en Android-details afhandelt.
7Wat houdt een offline-first architectuur in binnen mobiele ontwikkeling, en hoe verschilt dit fundamenteel van standaard HTTP-responsecaching (Hypertext Transfer Protocol)?
Bij mobiele ontwikkeling beschouwt een offline-first architectuur de lokale opslag als de primaire bron van waarheid voor zowel lees- als schrijfbewerkingen. In plaats van te wachten tot netwerkverzoeken zijn voltooid voordat er gerenderd wordt of gebruikersinteracties worden toegestaan, communiceert de app rechtstreeks met de lokale database of opslaglaag, terwijl synchronisatieprocessen op de achtergrond lokale wijzigingen afstemmen met de externe server zodra er verbinding is. Dit verschilt fundamenteel van standaard HTTP-responsecaching op een aantal punten: 1. **Datamodel**: HTTP-caching slaat onbewerkte netwerkantwoorden op (zoals JSON-payloads) met request-URL's en headers als sleutel. Een offline-first architectuur slaat gestructureerde domeinentiteiten op in een lokale database (zoals Room, SQLite of SwiftData/Core Data). 2. **Schrijfbewerkingen versus leesbewerkingen**: HTTP-caching is voornamelijk een mechanisme voor leesoptimalisatie en ondersteunt standaard geen offline schrijftransacties of mutaties. Offline-first staat volledige lokale schrijfbewerkingen toe en plaatst mutaties in een wachtrij voor latere synchronisatie met de server. 3. **Opvraagbaarheid en levenscyclus**: Gecachte HTTP-antwoorden zijn onderhevig aan automatische beleidsregels voor cache-verwijdering en kunnen niet willekeurig worden bevraagd, gefilterd of samengevoegd. Offline-first persistentie biedt volledige ondersteuning voor query's, indexering en deterministisch beheer van de levenscyclus, onafhankelijk van de netwerkstatus.
/* 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
8Wat is het verschil tussen een externe pushmelding en een lokale melding in een mobiele app?
Het kernverschil tussen lokale en externe pushmeldingen zit in de herkomst en de manier waarop ze worden geactiveerd:
1. Lokale meldingen worden volledig op het apparaat zelf aangemaakt, gepland en getriggerd door de applicatie via OS-API's (zoals `UserNotifications` op iOS of `AlarmManager`/`WorkManager`/`NotificationManager` op Android). Ze worden geactiveerd op basis van condities op het apparaat zelf, zoals een specifieke datum/tijd, een afteltimer of een geografische grens (geofencing). Omdat ze lokaal door de daemon van het besturingssysteem worden uitgevoerd, hebben ze op het aflevermoment geen backendserver of actieve internetverbinding nodig.
2. Externe pushmeldingen zijn afkomstig van een externe applicatieserver en worden via het internet verstuurd met pushgateways van het platform (APNs voor Apple-apparaten, FCM voor Android). De gateway levert de payload af aan de OS-daemon van het apparaat, die vervolgens de app activeert of een melding toont. Externe meldingen zijn noodzakelijk voor realtime gebeurtenissen van buitenaf, zoals inkomende chatberichten, vriendschapsverzoeken of belangrijk nieuws.
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)")
}
}
9Wat is het structurele en operationele verschil tussen een Android App Bundle (.aab), een APK (Android Package) en een iOS IPA-archief (.ipa) bij de distributie van apps via app stores?
Een APK (Android Package) is een uitvoerbaar pakketformaat met gecompileerde DEX-bytecode, resources, assets en native bibliotheken dat direct kan worden geïnstalleerd en uitgevoerd op een Android-apparaat. Een universele APK bundelt resources voor alle schermdichtheden, CPU-architecturen en talen, wat leidt tot grotere downloadformaten. Een Android App Bundle (.aab) is een upload- en publicatieformaat dat door Google Play wordt vereist voor distributie via de store. Een AAB kan niet rechtstreeks op een apparaat worden geïnstalleerd; in plaats daarvan gebruikt Google Play de bundle en Play App Signing om geoptimaliseerde split-APK's te genereren die dynamisch zijn afgestemd op de architectuur, schermdichtheid en taal van een specifiek apparaat (Dynamic Delivery). Een iOS IPA (.ipa) is een applicatie-archiefbestand (een zip-container met daarin de Payload-map, de `.app`-bundle, ondertekeningsmiddelen en metadata). Bij het uploaden naar App Store Connect past Apple App Thinning toe (zoals App Slicing) om alleen de assets en binaries af te leveren die vereist zijn voor het ontvangende apparaat. Operationeel kunnen APK's en IPA's direct worden geïnstalleerd op fysieke testapparaten (afhankelijk van ondertekening en provisioning), terwijl een AAB vóór lokale apparaatinstallatie eerst moet worden omgezet in split-APK's (bijvoorbeeld met behulp van `bundletool`).
# 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
10Welke bescherming biedt Transport Layer Security (TLS) voor de netwerkaanroepen van een mobiele applicatie, en hoe dwingen moderne mobiele besturingssystemen standaard veilige transportprotocollen af?
Transport Layer Security (TLS) beschermt netwerkaanroepen van mobiele applicaties door drie essentiële garanties te bieden: vertrouwelijkheid (het versleutelen van gegevens tijdens de overdracht zodat afluisteraars geen payloads of headers kunnen lezen), gegevensintegriteit (het detecteren van manipulatie of wijzigingen in requests en responses onderweg) en serverauthenticatie (het valideren van het digitale certificaat van de server tegen vertrouwde Certificate Authorities om Man-in-the-Middle-aanvallen te voorkomen). Moderne mobiele besturingssystemen dwingen standaard veilig transport af door niet-versleuteld HTTP-verkeer in platte tekst te blokkeren:
1. iOS handhaaft App Transport Security (ATS), wat vereist dat netwerkverbindingen (zoals die via URLSession) HTTPS met TLS 1.2+ gebruiken, tenzij domeinuitzonderingen expliciet zijn gedefinieerd in Info.plist.
2. Android (API 28+) schakelt HTTP-verkeer in platte tekst standaard uit (`cleartextTrafficPermitted=false`). Het toestaan van niet-versleutelde HTTP vereist expliciete uitzonderingen via Network Security Configuration (`network_security_config.xml`) of het manifest-attribuut `android:usesCleartextTraffic`.
11Hoe implementeer je biometrische authenticatie (Face ID / vingerafdruk) met beveiligde platformopslag om te garanderen dat cryptografische sleutels pas worden ontgrendeld na succesvolle biometrische verificatie?
Om cryptografische sleutels veilig af te schermen achter biometrie mag een app niet enkel vertrouwen op een boolean UI-callback vanuit een biometrische prompt. In plaats daarvan moeten cryptografische sleutels worden gegenereerd en opgeslagen in hardwarematig beveiligde opslag (Secure Enclave op iOS, Android Keystore / StrongBox op Android), geconfigureerd met toegangscontrolebeleid dat biometrische authenticatie afdwingt vóór sleutelgebruik. Op iOS worden sleutels of Keychain-items geconfigureerd met `SecAccessControlCreateWithFlags` en flags zoals `.biometryCurrentSet` (of `.userPresence`). Wanneer de privésleutel wordt opgevraagd voor ondertekening of ontsleuteling, vraagt het besturingssysteem de gebruiker automatisch om Face ID / Touch ID. Op Android worden sleutels gegenereerd met `KeyGenParameterSpec.Builder` en `.setUserAuthenticationRequired(true)`. Een cryptografische operatie (zoals een `Cipher` of `Signature`) wordt geïnitialiseerd en meegegeven als een `BiometricPrompt.CryptoObject` aan `BiometricPrompt.authenticate()`. De sleutel wordt pas ontgrendeld en bruikbaar binnen `onAuthenticationSucceeded` via dat geauthenticeerde `CryptoObject`. Het gebruik van `.biometryCurrentSet` (iOS) of `setInvalidatedByBiometricEnrollment(true)` (Android) zorgt ervoor dat wanneer er een nieuwe vingerafdruk of biometrisch profiel op het apparaat wordt geregistreerd, de bestaande sleutels permanent ongeldig worden, wat ongeautoriseerde toegang voorkomt als de toegangscode van het apparaat is gecompromitteerd.
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()
12Welke cachingstrategieën en pipeline-optimalisaties kun je implementeren om de buildtijden van pull requests aanzienlijk te verkorten op cross-platform mobiele CI-runners (Continuous Integration)?
Om de buildtijden van pull requests op mobiele CI-runners drastisch te verkorten, moeten optimalisaties gericht zijn op dependency-resolutie, compilatiecaching en selectieve uitvoering. Ten eerste: implementeer effectieve dependency-caching op basis van lockfile-hashes (zoals `package-lock.json`, `Podfile.lock`, `gradle/verification-metadata.xml` of SPM-package-resolved-bestanden). Dit voorkomt het downloaden of opnieuw resolven van externe packages op schone runner-instances. Ten tweede: configureer native build-caches. Schakel voor Android de Gradle Build Cache (`--build-cache`) in met remote HTTP-caching of CI-native persistente caching voor `~/.gradle/caches` en build-mappen. Cache voor iOS Xcode `DerivedData` en Swift Package- / CocoaPods-compilatie-artefacten zorgvuldig, of gebruik moderne remote-cachingtools zoals Tuist of Bazel. Cache voor React Native/Flutter de JS-bundle-uitvoer, node_modules en de Flutter engine SDK-cache. Ten derde: pas selectieve job-uitvoering toe (wijzigingsdetectie en test-impactanalyse). Gebruik Git-padfilters om iOS-builds over te slaan wanneer alleen Android- of backend-bestanden zijn gewijzigd, draai snelle linting- en unit-testfasen vóór dure UI-tests of compilatiestappen, en vermijd het genereren van volledige release-binaries (zoals AAB's of universele IPA's) bij PR-runs wanneer een simulator-/debug-build of unit-testrun volstaat.
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
13Hoe zou je een gedeelde feature structureren zodat presentatie, domein-use-cases en datatoegang onafhankelijk testbaar en herbruikbaar blijven voor zowel iOS als Android?
Om een gedeelde feature te structureren voor onafhankelijke testbaarheid en herbruikbaarheid op zowel iOS als Android, pas je een clean, gelaagde architectuur toe die is opgedeeld in presentatie-, domein- en datalagen: 1. **Domeinlaag (Zuivere gedeelde logica):** Bevat zuivere bedrijfsentiteiten, repository-interfaces (contracten) en use cases/interactors (bijvoorbeeld `GetCartUseCase`, `ApplyPromoCodeUseCase`). Deze laag heeft geen enkele afhankelijkheid van UI-frameworks (UIKit, SwiftUI, Android Views, Jetpack Compose, Flutter-widgets) of platform-API's. Elke use case kapselt één enkele bedrijfsoperatie in, waardoor deze 100% unit-testbaar is met eenvoudige mocks. 2. **Datalaag (Datatoegang & Inkapseling):** Implementeert repository-interfaces uit het domein. Deze communiceert met externe databronnen (REST/GraphQL) en lokale opslag (SQLite/Key-Value). Netwerk-DTO's (Data Transfer Objects) en database-entiteiten worden strikt gemapt naar zuivere domeinentiteiten voordat ze deze laag verlaten. Dit voorkomt dat externe serialisatieschema's of databasewijzigingen weglekken naar domein- of UI-lagen. 3. **Presentatielaag (UI & Statusbeheer):** Bestaat uit toestandshouders (ViewModels, Blocs of Presenters) en UI-views/widgets. De toestandshouder roept domein-use-cases aan, beheert de UI-status (laden, succes, fout) en stelt observeerbare status beschikbaar aan de view. De view observeert deze status enkel en rendert deze. Deze structuur maakt het mogelijk om use cases geïsoleerd zonder UI te testen met unit tests, repositories te mocken om viewmodels te testen, en presentatie-implementaties tussen platforms uit te wisselen terwijl de volledige domein- en datalogica wordt hergebruikt.
[ 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 ]
14Hoe beoordeel je wanneer lokale componentstatus niet langer volstaat en kies je een geschikte architecturale aanpak voor statusbeheer (state management) in een cross-platform codebase met meerdere ontwikkelaars?
In cross-platform mobiele ontwikkeling wordt de overstap van lokale componentstatus (`useState`/`StatefulWidget`) naar een architecturale oplossing voor statusbeheer gedreven door het bereik van de toestand, levenscyclusvereisten en testbaarheid: 1. **Wanneer lokale status niet langer volstaat**: - **Delen over componenten en prop drilling**: wanneer status moet worden benaderd of gewijzigd over verschillende navigatieroutes of ver uit elkaar liggende takken van de widget-/componentboom. - **Levenscycluspersistentie**: wanneer gegevens schermverwijderingen, routetransities of navigatieresets moeten overleven (zoals gebruikerssessies, winkelwageninhoud of gecachte feeds). - **Scheiding van verantwoordelijkheden en testbaarheid**: wanneer bedrijfslogica, validatie en neveneffecten verstrengeld raken met UI-componenten, wat geautomatiseerde headless unittesten bemoeilijkt. 2. **Een architectuur kiezen voor codebases met meerdere ontwikkelaars**: - **Unidirectional Data Flow / voorspelbare containers (zoals BLoC, Redux, Riverpod)**: dwingt een strikte scheiding af waarbij de UI expliciete events/acties verzendt en een onveranderlijke toestand rendert die wordt uitgezonden door specifieke bedrijfslogica-eenheden. Dit creëert duidelijke contracten, vermindert neveneffecten en maakt onafhankelijke unittesten van de bedrijfslogica mogelijk. - **Atomaire / scoped reactieve stores (zoals Zustand, MobX, Provider)**: bieden minder boilerplate en flexibele selector-abonnementen, ideaal wanneer teams een lichte ontkoppeling en gerichte her-renders van componenten willen. In teamverband is het primaire architecturale doel om pure bedrijfslogica te isoleren van de UI-weergavelagen en selectieve abonnementen te gebruiken om onnodige her-renders van de volledige boomstructuur te voorkomen.
15Hoe zou je haperingen (jank) onderzoeken en verminderen in een cross-platform mobiel scherm dat hapert tijdens het scrollen door een lange lijst met afbeeldingen en dynamische inhoud?
Ik zou de hapering eerst reproduceren op fysieke testapparaten en een profiler gebruiken, omdat scroll-jank verschillende oorzaken kan hebben: het overschrijden van het framebudget, JS- of Dart-verwerking, werk op de main/UI-thread, GPU/raster-taken, layoutberekeningen, het decoderen van afbeeldingen, geheugendruk of netwerkverzoeken. Bij 60 Hz heeft de app ongeveer 16,7 ms per frame en bij 120 Hz ongeveer 8,3 ms, waardoor zware rendering, decodering, layoutberekeningen of synchrone taken tijdens het scrollen kunnen leiden tot overgeslagen frames (dropped frames). Ik zou tools zoals Flutter DevTools, React Native performance tools of Flipper, Android Studio Profiler, Xcode Instruments en frametimelines gebruiken om de daadwerkelijke bottleneck vast te stellen. Voor de lange lijst zou ik zorgen voor lazy loading of virtualisatie: bijvoorbeeld `FlatList`, `FlashList` of `RecyclerListView` in React Native, of `ListView.builder`, `SliverList` of vergelijkbare builders in Flutter. Ik zou stabiele keys gebruiken, het opnieuw bouwen of renderen van elke rij bij wijzigingen in de bovenliggende status vermijden, rijcomponenten of selectors memoizen waar passend, `build`/`renderItem` licht houden en sorteren, filteren, JSON-parsing, formattering of beeldbewerking weghalen uit het scrollpad. Als itemafmetingen voorspelbaar zijn, zou ik layouthints meegeven zoals `getItemLayout` in React Native of vaste/prototype item-extents in Flutter. Voor afbeeldingen zou ik miniaturen (thumbnails) met passende afmetingen serveren, deze cachen, vermijden dat afbeeldingen in volledige resolutie worden gedecodeerd voor kleine cellen, placeholders en lazy loading toepassen en waken voor geheugendruk door te veel grote bitmaps. Ik zou ook te complexe rij-layouts vereenvoudigen, zware schaduwen, clipping, overdraw en transparantie (opacity) waar nodig verminderen, gegevens in batches of gepagineerd inladen en vervolgens opnieuw profilen om te bevestigen dat dropped frames en frametijden zijn verbeterd.
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.
16Hoe werken threadgrenzen bij native bridge-aanroepen en hoe voorkom je vertragingen door thread-hopping of haperingen in de UI (User Interface) wanneer native modules zwaar achtergrondwerk verrichten?
Crossplatform-frameworks hanteren specifieke threading-conventies voor bridge-aanroepen: standaardaanroepen via `MethodChannel` in Flutter komen bijvoorbeeld binnen op de main UI-thread van het platform, terwijl traditionele React Native-bridge-aanroepen worden uitgevoerd op een specifieke JavaScript-thread en asynchroon worden doorgestuurd naar native threads. Wanneer een native bridge-methode wordt uitgevoerd op de native UI-/main-thread, blokkeert het uitvoeren van CPU-intensieve bewerkingen, langdurige synchrone schijf-I/O of blokkerende netwerktaken de main runloop. Dit leidt tot overgeslagen frames, zichtbare haperingen in de UI en op Android tot 'Application Not Responding' (ANR)-fouten of beëindiging door de iOS-watchdog. Om dit te voorkomen, moeten native bridge-handlers zwaar of blokkerend werk uitbesteden aan achtergrond-executiegroepen (zoals Kotlin Coroutines met `Dispatchers.IO`/`Dispatchers.Default`, Android `ThreadPoolExecutor`, of Swift `Task.detached` / GCD `DispatchQueue.global()`). Zodra het werk is voltooid, moet het resultaat worden teruggestuurd naar de door het framework verwachte bridge-thread (bijvoorbeeld door resultaten terug te sturen op de UI-thread voor standaard MethodChannels of via achtergrondtaakwachtrijen), zodat updates van de crossplatform-status de UI-renderingcyclus niet blokkeren.
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()
}
}
}
17Hoe ontwerp je een caching- en ongeldigmakingsstrategie voor lijst- en feedschermen met behulp van Stale-While-Revalidate en conditionele HTTP (Hypertext Transfer Protocol)-headers?
Een robuuste caching- en ongeldigmakingsstrategie voor feed- en lijstschermen combineert het Stale-While-Revalidate (SWR)-patroon met conditionele HTTP-validatieheaders (zoals `ETag` / `If-None-Match` of `Last-Modified` / `If-Modified-Since`). Wanneer het scherm wordt geopend, leest en rendert het onmiddellijk gecachte gegevens uit een lokale database (de Single Source of Truth, zoals Room of Core Data/SQLite), waardoor de gebruiker direct een gerenderde UI te zien krijgt. Tegelijkertijd wordt op de achtergrond een netwerkverzoek verstuurd met de gecachte `If-None-Match: <etag>`-header. Als de server reageert met `304 Not Modified`, wordt er geen payload verzonden, waardoor de lokale cache wordt gevalideerd en bandbreedte en batterij worden bespaard. Als de server reageert met `200 OK`, worden de nieuwe gegevens en de bijgewerkte `ETag` in een transactie naar de lokale database geschreven, en sturen reactieve database-observers de bijgewerkte lijst automatisch door naar de UI. Voor paginering worden paginatokens of offsets samen met de gecachte records opgeslagen. Ongeldigmaking wordt getriggerd door het verstrijken van de TTL, gebruikersacties (zoals pull-to-refresh die conditionele headers omzeilt) of neveneffecten van lokale mutaties (bijvoorbeeld het lokaal aanmaken of verwijderen van een item werkt de database optimistisch bij en markeert gecachte paginacursors voor hernieuwde validatie).
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
}
}
18Een financiële mobiele applicatie moet refresh tokens veilig op het apparaat opslaan, biometrische ontgrendeling ondersteunen, OS-upgrades (Operating System) overleven en veilig functioneren tijdens tijdelijke offline sessies. Hoe zou je deze architectuur voor tokenopslag en -toegang ontwerpen?
Een robuuste architectuur voor mobiele tokenopslag maakt gebruik van hardware-beveiligde modules: de iOS Keychain ondersteund door de Secure Enclave en de Android Keystore ondersteund door StrongBox of een TEE (Trusted Execution Environment). Refresh tokens moeten in rusttoestand worden versleuteld met sleutels die zijn beschermd door biometrische cryptografische gating (zoals `kSecAccessControlBiometryCurrentSet` of `kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly` op iOS, en `setUserAuthenticationRequired(true)` met `AUTH_BIOMETRIC_STRONG` op Android). Cryptografische gating zorgt ervoor dat de cryptografische sleutel alleen door de hardware wordt ontgrendeld na een succesvolle biometrische authenticatie, in plaats van te vertrouwen op een te omzeilen booleaanse controle in de applicatiecode. Om OS-upgrades te overleven en tegelijkertijd ongeautoriseerde toegang te voorkomen, worden sleutels gekoppeld aan de apparaathardware, waarbij beleid voor registratiewijzigingen wordt afgedwongen dat sleutels ongeldig maakt of om herauthenticatie vraagt als er nieuwe biometrische gegevens worden geregistreerd (bijvoorbeeld via `setInvalidatedByBiometricEnrollment(true)` op Android). Voor tijdelijke offline sessies kunnen kortlevende versleutelde access tokens en beperkte lokale offline sessiestatussen functioneren binnen gedefinieerde lokale TTL's en begrensde rechten, terwijl bevoorrechte vernieuwingsacties worden uitgesteld totdat er weer een netwerkverbinding is. Bij het herstellen van de verbinding valideert de backend de sessie via rotatie van refresh tokens met detectie van herhaald gebruik voor eenmalig gebruik; als hergebruik van tokens of intrekking van het account wordt gedetecteerd, retourneert de backend een invalidatiesignaal dat de client instrueert om hardware-gebonden sleutels te verwijderen en de offline opslag te wissen.
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()
19Hoe zou je een schaalbare CI/CD-infrastructuur (Continuous Integration/Continuous Deployment) ontwerpen voor een cross-platform mobiele monorepo die meerdere teams ondersteunt, terwijl je de beperkte en kostbare macOS-runnercapaciteit beheert?
Om een schaalbare CI/CD-infrastructuur voor een cross-platform mobiele monorepo te ontwerpen en tegelijkertijd schaarse en dure macOS-capaciteit te optimaliseren, moet de architectuur steunen op graafbewuste detectie van gewijzigde builds, strikte taakverdeling over hybride runner-pools en virtualisatie/tijdelijke orkestratie voor macOS. Ten eerste implementeer je een monorepo-bouwgraaftool (zoals Bazel, Nx of Turborepo) met gedistribueerde externe caching. Bij elke pull request berekent de CI-pijplijn het verschil in de gerichte acyclische graaf (DAG, Directed Acyclic Graph) ten opzichte van de doelbranch om alleen gewijzigde packages en hun downstream-afhankelijkheden te bouwen en te testen, waardoor ongewijzigde modules volledig worden overgeslagen. Ten tweede pas je een hybride runner-toewijzingsstrategie toe: verplaats alle taken die niet strikt Xcode vereisen (zoals TypeScript/Dart-linting en -compilatie, unittests, statische codeanalyse, beveiligingsscans en Android Gradle-builds) naar kostenefficiënte, schaalbare Linux/Kubernetes-runners. Reserveer macOS-runners uitsluitend voor de definitieve iOS-assemblage, Swift/Objective-C-compilatie, code-signing en uitvoering van iOS Simulator-tests. Ten derde beheer je macOS-runners met behulp van virtualisatie-infrastructuur (zoals Tart, Anka of AWS/MacStadium bare-metal nodes georkestreerd via Nomad of Kubernetes). Elke iOS-build draait in een schone, tijdelijke virtuele machine met vooraf ingerichte toolchains en caches voor afgeleide data, wat runner-drift voorkomt en snelle automatische schaalvergroting mogelijk maakt op basis van de wachtrijdiepte van de pijplijn.
20Hoe ontwerp je een platformonafhankelijke mobiele architectuur voor een product waarbij het merendeel van de bedrijfslogica wordt gedeeld tussen iOS en Android, terwijl platformspecifieke UI, navigatie en native integraties zich toch onafhankelijk kunnen ontwikkelen?
Ik zou de app ontwerpen rond een gedeelde kern die verantwoordelijk is voor stabiel zakelijk gedrag: domeinmodellen, validatie, use-cases, bedrijfsregels en contracten voor datatoegang. Platformspecifieke UI, navigatie, native SDK's, permissies, levenscyclusafhandeling en app-shell-aspecten moeten buiten die kern blijven.
De gedeelde code mag geen UIKit, SwiftUI, Jetpack, Android-framework-API's, React Native-navigatie, Flutter-navigatie of specifieke details van native SDK's importeren. In plaats daarvan moet deze afhankelijk zijn van gerichte interfaces zoals AuthRepository, SecureStorage, Analytics, CameraService of PaymentProvider, die door elk platform afzonderlijk worden geïmplementeerd. Ik zou code zoveel mogelijk per functionaliteit organiseren in plaats van één grote gedeelde laag te maken. Zo kunnen afrekenen, zoeken, accountbeheer en berichten elk hun eigen gedeelde domein-/use-casecode, statuscontracten en repository-interfaces hebben.
De iOS- en Android-shells zijn vervolgens verantwoordelijk voor schermopbouw, native UI-patronen, navigatiestacks, permissies en adapterkoppelingen. Presentatielogica kan worden gedeeld wanneer deze echt onafhankelijk is van het UI-framework, zoals reducers, toestandsautomaten of viewmodel-contracten die eenvoudige status en acties uitzenden. De daadwerkelijke weergaven en navigatie moeten echter eigendom van het platform blijven, zodat elk platform onafhankelijk kan evolueren.
Het belangrijkste afwegingspunt is om stabiele bedrijfslogica doortastend te delen zonder abstracties te introduceren die reële platformverschillen verhullen. Ik zou grenzen bewaken met module-afhankelijkheidsregels, dependency injection, publieke API's, contracttests, architectuurcontroles en helder eigenaarschap. Native integraties moeten poorten en adapters (ports and adapters) gebruiken, zodat de gedeelde functionaliteitscode een uniforme capaciteit ziet, terwijl elk platform het specifieke SDK-gedrag, de levenscyclus, permissies, foutafhandeling en gebruikerservaring in zijn eigen laag afhandelt.