20 častých otázek k pohovoru pro Mobile Developer. Mobilní vývoj je žádaný obor zaměřený na spolehlivé iOS, Android a multiplatformní aplikace, produktové požadavky, výkon, releasy, offline režim a integraci se zařízením. Otázky pokrývají různé úrovně a odpovědi si můžeš nahlas procvičit v našem pohovorovém trenéru.
1Jaký je rozdíl mezi ukládáním autentizačního tokenu do standardního úložiště typu klíč-hodnota (jako jsou SharedPreferences nebo UserDefaults) a do zabezpečeného úložiště platformy (jako je Android Keystore nebo iOS Keychain)?
Standardní úložné mechanismy, jako jsou SharedPreferences na Androidu a UserDefaults na iOS, jsou navrženy pro nenáročné, necitlivé předvolby. Ukládají data v nešifrovaných textových souborech (XML nebo plist) v sandboxovém adresáři aplikace. Kdokoli s fyzickým přístupem k rootnutému zařízení nebo zařízení po jailbreaku, k nešifrované záloze zařízení nebo k souborovému systému může tyto tokeny přímo přečíst.
Naproti tomu zabezpečená úložiště platformy – konkrétně iOS Keychain a Android Keystore (často využívaný prostřednictvím EncryptedSharedPreferences) – poskytují šifrování uložených dat (data-at-rest). iOS Keychain šifruje uložené položky pomocí klíčů vázaných na hardware zařízení (například Secure Enclave) a umožňuje definovat jemnozrnné zásady přístupu. Android Keystore generuje a uchovává kryptografické klíče uvnitř hardwarově izolovaných bezpečnostních modulů (TEE – Trusted Execution Environment nebo StrongBox), což zajišťuje, že kryptografické klíče nejsou nikdy vystaveny v paměti aplikace a nelze je extrahovat ze souborového systému.
// --- 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)
2Jaký je hlavní účel mobilní CI/CD (Continuous Integration / Continuous Deployment) pipeline a jaké specifické fáze a omezení ji odlišují od webových nebo backendových nasazovacích procesů?
Hlavním účelem mobilní CI/CD pipeline je automatizovat sestavování, testování, podepisování a distribuci mobilních klientských aplikací za účelem udržení konzistentní kvality a opakovatelných vydání. Continuous Integration (CI) ověřuje změny kódu prostřednictvím automatizovaných sestavení, statické analýzy a jednotkových testů. Continuous Delivery/Deployment (CD) balíčkují, podepisují a distribuují binární artefakty do testovacích kanálů (jako je TestFlight nebo Firebase App Distribution) nebo do obchodů s aplikacemi. Mobilní CI/CD se od webových a backendových pipeline liší v několika klíčových ohledech:
1. Typ artefaktu: Sestavení produkují zkompilované klientské binární soubory (.ipa, .apk, .aab), namísto toho, aby kód běžel přímo na serverech nebo uvnitř kontejnerových obrazů.
2. Hardwarová omezení runnerů: Kompilace pro iOS vyžaduje hardware s macOS a nástroje příkazového řádku Xcode, zatímco backendové pipeline obvykle běží na lehkých linuxových kontejnerech.
3. Latence vydání a schvalování: Publikování koncovým uživatelům zahrnuje schvalovací procesy externích obchodů s aplikacemi (Apple App Store / Google Play Store), což znamená, že nasazení nelze okamžitě vrátit zpět opětovným nasazením na server. Opravy vyžadují odeslání nového podepsaného binárního souboru nebo použití runtime přepínačů funkcí (feature flags).
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
3Jaký je v multiplatformní mobilní aplikaci účel oddělení prezentační, doménové a datové vrstvy a jak tato struktura usnadňuje sdílení kódu mezi platformami iOS a Android?
Rozdělení aplikace do prezentační, doménové a datové vrstvy stanovuje jasné hranice zodpovědnosti:
1. **Prezentační vrstva:** Řeší vykreslování UI, uživatelský vstup a stav na úrovni obrazovky (např. Views, Widgets, ViewModels nebo Presenters).
2. **Doménová vrstva:** Zapouzdřuje čistou obchodní logiku, doménové modely/entity a případy užití (use cases). Zůstává nezávislá na UI a platformních frameworcích.
3. **Datová vrstva:** Spravuje načítání a ukládání dat ze vzdálených API (Application Programming Interface), lokálních databází nebo mezipaměti zařízení prostřednictvím repozitářů a datových zdrojů.
V multiplatformním mobilním vývoji tato struktura usnadňuje sdílení kódu, protože doménová a datová vrstva jsou nezávislé na platformě. Klíčová obchodní pravidla, transformace dat a síťová komunikace mohou být ze 100 % sdíleny mezi iOS a Androidem, což týmům umožňuje buď sdílet celý stack (jako ve Flutteru nebo React Native), nebo sdílet obchodní/datovou logiku při zachování nativních prezentačních vrstev specifických pro danou platformu (jako v Kotlin Multiplatform).
[Presentation Layer] (UI, Screens, ViewModels)
│
▼ calls
[Domain Layer] (Use Cases, Business Rules, Entities)
▲
│ implements interfaces
[Data Layer] (Repositories, API Clients, Local Storage)
4Jaké jsou hlavní rozdíly mezi imperativním a deklarativním paradigmatem vývoje UI (User Interface) v mobilním vývoji a jak widgety ve Flutteru a komponenty v React Native toto deklarativní paradigma naplňují?
Při imperativním vývoji UI vývojáři píší explicitní instrukce krok za krokem k vytváření, úpravě a rušení prvků rozhraní (například vyhledání prvku podle ID a přímé volání metod jako `setText` nebo `setVisibility`). V deklarativním modelu se naopak popisuje, jak má UI vypadat pro daný stav (často vyjádřeno vzorcem `UI = f(state)`). Při změně stavu framework sám určí, jak efektivně aktualizovat vizuální podobu. Flutter i React Native toto deklarativní paradigma ztělesňují. Ve Flutteru jsou widgety neměnnými (immutable) popisy konfigurace; při změně dynamických dat vyvolá volání `setState()` ve `StatefulWidget` přeplánování vykreslení, kde metoda `build()` vrátí nový popis stromu widgetů, který Flutter následně porovná (reconciles) s interními stromy Element a RenderObject. V React Native jsou komponenty funkce nebo třídy vracející JSX; aktualizace stavu nebo parametrů (props) spustí opětovné vykreslení, kde React porovná virtuální strom elementů a aplikuje pouze nezbytné minimální nativní změny přes bridge nebo nativní runtime.
5Jaký je provozní rozdíl mezi studeným (cold start), teplým (warm start) a horkým startem (hot start) v mobilních aplikacích a proč je studený start nejkritičtější metrikou pro výkonnostní rozpočty?
V životním cyklu mobilních aplikací se stavy spuštění liší podle toho, zda proces aplikace a její stav v paměti již v operačním systému existují.
- Studený start (Cold Start): Aplikace startuje od nuly, protože operační systém ještě nevytvořil její proces, nebo byl proces dříve ukončen. Systém musí alokovat nový proces, načíst binární soubory / běhové prostředí, inicializovat kontext aplikace a běhového prostředí a vytvořit první zobrazení (inflate view). Má nejvyšší latenci.
- Teplý start (Warm Start): Proces aplikace je obvykle stále v paměti, ale hierarchie aktivit/pohledů byla zničena nebo uvolněna (např. z důvodu znovuvytvoření na pozadí nebo navigace zpět). Systém znovu vytvoří uživatelské rozhraní / aktivitu, aniž by musel vytvářet nový proces od začátku.
- Horký start (Hot Start): Aplikace i její stav uživatelského rozhraní jsou stále plně v paměti (např. uživatel stiskl tlačítko Domů a okamžitě se vrátil). Systém pouze přesune existující hierarchii pohledů do popředí s téměř nulovou režií na inicializaci.
Studený start je nejkritičtější metrikou pro výkonnostní rozpočty, protože představuje nejpomalejší první dojem pro uživatele. Dlouhý studený start přímo koreluje s okamžitým odchodem uživatelů, nižší mírou retence a horším hodnocením v obchodech s aplikacemi (například podle prahových hodnot v Android Vitals).
6Co je to nativní můstek v multiplatformní mobilní aplikaci a kdy byste jej použili namísto implementace dané funkce výhradně v kódu Flutteru nebo React Native?
Nativní můstek (native bridge) je komunikační vrstva, která umožňuje multiplatformnímu kódu volat specifický kód pro iOS nebo Android a nativnímu kódu vracet výsledky nebo posílat zpět události. Ve Flutteru se to běžně řeší pomocí platformních kanálů (platform channels) nebo pluginů. V React Native se to obvykle provádí pomocí nativních modulů (native modules), TurboModules nebo nativních komponent UI (User Interface). Můstek použijete v případě, kdy funkce vyžaduje něco, k čemu čistý kód ve Flutteru nebo React Native nemá dostatečný přístup – například platformní API (Application Programming Interface), schopnosti zařízení, nativní SDK (Software Development Kit), výkonnostně kritickou nativní implementaci nebo nativní komponenty UI. Běžné příklady zahrnují funkce fotoaparátu, Bluetooth, push notifikace, platby, zabezpečené úložiště, služby na pozadí, zdravotní API nebo SDK, která nabízejí pouze integrace v jazycích Swift/Objective-C nebo Kotlin/Java. Dobrý můstek obvykle vystavuje multiplatformní vrstvě malé a přehledné API, jako je getBatteryLevel, startBluetoothScan nebo openNativePaymentSheet, zatímco specifická implementace pro danou platformu řeší skutečné detaily pro iOS a Android.
7Co znamená architektura offline-first při vývoji mobilních aplikací a v čem se zásadně liší od standardního cachování odpovědí HTTP (Hypertext Transfer Protocol)?
Při vývoji mobilních aplikací přistupuje architektura offline-first k lokálnímu úložišti jako k primárnímu zdroji pravdy pro operace čtení i zápisu. Místo čekání na dokončení síťových požadavků před vykreslením nebo umožněním interakcí uživatele aplikace komunikuje přímo s lokální databází nebo vrstvou úložiště, zatímco synchronizační procesy na pozadí zajišťují sladění lokálních změn se vzdáleným serverem, jakmile je dostupné připojení.
Od standardního cachování odpovědí protokolu HTTP se zásadně liší v několika ohledech:
1. **Datový model**: Cachování HTTP ukládá surové síťové odpovědi (např. JSON data) klíčované podle URL a hlaviček požadavku. Architektura offline-first ukládá strukturované doménové entity do lokální databáze (jako je Room, SQLite nebo SwiftData/Core Data).
2. **Zápisy vs. čtení**: Cachování HTTP slouží primárně jako mechanismus pro optimalizaci čtení a nativně nepodporuje offline transakce zápisu ani mutace. Offline-first umožňuje plnohodnotné lokální zápisy a řadí změny do fronty pro pozdější synchronizaci se serverem.
3. **Dotazovatelnost a životní cyklus**: Uložené odpovědi HTTP podléhají zásadám automatického vyřazování z mezipaměti a nelze se na ně libovolně dotazovat, filtrovat je ani je spojovat. Persistence v architektuře offline-first poskytuje plnou podporu dotazování, indexování a deterministickou správu životního cyklu nezávisle na stavu sítě.
/* 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
8Jaký je rozdíl mezi vzdáleným push oznámením (remote push notification) a lokálním oznámením (local notification) v mobilní aplikaci?
Základní rozdíl mezi lokálními a vzdálenými push oznámeními spočívá v místě jejich vzniku a způsobu jejich spouštění: 1. Lokální oznámení (local notifications) jsou vytvářena, plánována a spouštěna výhradně na zařízení samotnou aplikací pomocí rozhraní API operačního systému (jako je UserNotifications v iOS nebo AlarmManager/WorkManager/NotificationManager v Androidu). Spouštějí se na základě podmínek na straně zařízení, jako je konkrétní datum a čas, odpočet nebo geografická hranice (geofencing). Vzhledem k tomu, že běží lokálně na úrovni systémového démona (daemon), nevyžadují v okamžiku doručení backendový server ani aktivní připojení k internetu. 2. Vzdálená push oznámení (remote push notifications) pocházejí z externího aplikačního serveru a jsou přenášena přes internet prostřednictvím push bran dané platformy (APNs pro zařízení Apple, FCM pro Android). Brána doručí datový obsah (payload) systémovému démonu na úrovni operačního systému, což aplikaci probudí nebo zobrazí banner. Vzdálená oznámení jsou nezbytná pro externí události v reálném čase, jako jsou příchozí chatové zprávy, žádosti o přátelství nebo mimořádné zprávy.
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)")
}
}
9Jaký je strukturální a provozní rozdíl mezi balíčkem Android App Bundle (.aab), APK (Android Package) a archivem IPA (iOS App Store Package) při distribuci aplikací prostřednictvím obchodů s aplikacemi?
APK (Android Package) je formát spustitelného balíčku obsahující zkompilovaný bajtkód DEX, prostředky (resources), assety a nativní knihovny, který lze přímo nainstalovat a spustit na zařízení se systémem Android. Univerzální balíček APK sdružuje prostředky pro všechny hustoty obrazovky, architektury CPU a jazyky, což vede k větší velikosti stahování. Android App Bundle (.aab) je formát pro nahrávání a publikování vyžadovaný službou Google Play pro distribuci v obchodě. Soubor AAB nelze nainstalovat přímo do zařízení; místo toho Google Play využívá tento balíček a službu Play App Signing k dynamickému generování optimalizovaných dílčích balíčků APK (split APKs) přizpůsobených konkrétní architektuře zařízení, hustotě obrazovky a jazyku (Dynamic Delivery). Soubor iOS IPA (.ipa) je archiv aplikace (kontejner ZIP obsahující adresář Payload, svazek `.app`, podepisovací data a metadata). Při nahrání do App Store Connect provádí Apple optimalizaci App Thinning (například App Slicing), aby dodal pouze assety a binární soubory potřebné pro stahující zařízení. Z provozního hlediska lze balíčky APK a archivy IPA přímo instalovat na fyzická testovací zařízení (v závislosti na podepsání a provisioning profilu), zatímco formát AAB musí být před lokální instalací do zařízení nejprve převeden na dílčí APK (např. pomocí nástroje `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
10Jakou ochranu poskytuje protokol TLS (Transport Layer Security) pro síťová volání mobilní aplikace a jak moderní mobilní operační systémy vynucují výchozí bezpečný přenos?
Protokol TLS (Transport Layer Security) chrání síťová volání mobilních aplikací tím, že poskytuje tři základní záruky: důvěrnost (šifrování přenášených dat, takže odposlouchávající strany nemohou číst obsah ani hlavičky), integritu dat (detekci jakékoliv neoprávněné manipulace nebo modifikace požadavků a odpovědí během přenosu) a autentizaci serveru (ověření digitálního certifikátu serveru vůči důvěryhodným certifikačním autoritám pro zabránění útokům Man-in-the-Middle). Moderní mobilní operační systémy ve výchozím stavu vynucují bezpečný přenos blokováním nešifrovaného textového provozu protokolu HTTP: 1. iOS vynucuje funkci ATS (App Transport Security), která vyžaduje, aby síťová spojení (např. přes URLSession) používala HTTPS s protokolem TLS 1.2+, pokud nejsou v souboru Info.plist explicitně definovány výjimky pro domény. 2. Android (API 28+) ve výchozím nastavení zakazuje nešifrovaný HTTP provoz (`cleartextTrafficPermitted=false`). Povolení nešifrovaného HTTP vyžaduje explicitní výjimky v konfiguraci zabezpečení sítě (`network_security_config.xml`) nebo atribut manifestu `android:usesCleartextTraffic`.
11Jak implementujete biometrické ověřování (Face ID / otisk prstu) s bezpečným úložištěm platformy, abyste zajistili, že se kryptografické klíče odemknou pouze po úspěšném biometrickém ověření?
Aby byly kryptografické klíče bezpečně vázány na biometrii, aplikace nesmí spoléhat pouze na návratovou hodnotu boolean z UI (User Interface) biometrické výzvy. Místo toho musí být kryptografické klíče generovány a uloženy v hardwarově zabezpečeném úložišti (Secure Enclave na iOS, Android Keystore / StrongBox na Androidu) nakonfigurovaném s pravidly řízení přístupu, která vynucují biometrické ověření před použitím klíče.
Na platformě iOS se klíče nebo položky v Keychain konfigurují pomocí `SecAccessControlCreateWithFlags` s příznaky jako `.biometryCurrentSet` (nebo `.userPresence`). Pokud je privátní klíč vyžádán k podpisu nebo dešifrování, OS (Operating System) automaticky vyzve uživatele k ověření přes Face ID / Touch ID.
Na Androidu se klíče generují pomocí `KeyGenParameterSpec.Builder` s `.setUserAuthenticationRequired(true)`. Kryptografická operace (jako `Cipher` nebo `Signature`) je inicializována a předána jako `BiometricPrompt.CryptoObject` do metody `BiometricPrompt.authenticate()`. Klíč je odemčen a použitelný pouze uvnitř `onAuthenticationSucceeded` prostřednictvím tohoto ověřeného objektu `CryptoObject`.
Použití `.biometryCurrentSet` (iOS) nebo `setInvalidatedByBiometricEnrollment(true)` (Android) zajistí, že pokud je na zařízení zaregistrován nový otisk prstu nebo biometrický údaj, stávající klíče se stanou trvale neplatnými, což zmírňuje riziko neoprávněného přístupu v případě prolomení přístupového kódu zařízení.
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()
12Jaké strategie ukládání do mezipaměti (caching) a optimalizace pipeline můžete implementovat, abyste výrazně zkrátili dobu sestavování pull requestů na multiplatformních CI (Continuous Integration) runnerech?
Pro radikální zkrácení doby sestavování pull requestů na mobilních CI runnerech je nutné optimalizovat řešení závislostí, ukládání kompilace do mezipaměti a selektivní spouštění úloh. Zaprvé implementujte efektivní mezipaměť závislostí klíčovanou podle kontrolních součtů (hashů) zamykacích souborů (např. `package-lock.json`, `Podfile.lock`, `gradle/verification-metadata.xml` nebo souborů vyřešených balíčků v SPM). Tím se předejde stahování a opakovanému vyhodnocování externích balíčků na čistých instancích runnerů. Zadruhé nakonfigurujte nativní mezipaměti sestavení (build caches). Pro Android povolte Gradle Build Cache (`--build-cache`) se vzdálenou HTTP mezipamětí nebo nativní persistentní mezipamětí v CI pro `~/.gradle/caches` a adresáře sestavení. Pro iOS pečlivě ukládejte do mezipaměti `DerivedData` v Xcode a artefakty kompilace Swift Package / CocoaPods, případně využijte moderní nástroje pro vzdálené ukládání do mezipaměti, jako je Tuist nebo Bazel. Pro React Native/Flutter ukládejte výstup JS bundle, node_modules a mezipaměť SDK jádra Flutteru. Zatřetí aplikujte selektivní spouštění úloh (detekce změn a analýza dopadu testů). Použijte Git path filtry pro přeskočení sestavování iOS, pokud se změnily pouze soubory Androidu nebo backendu, spouštějte rychlé fáze lintování a jednotkových testů před náročnými testy uživatelského rozhraní či kompilací a vyvarujte se generování plných produkčních binárek (jako AAB nebo univerzální IPA) při běhu PR, kde stačí sestavení pro simulátor/debug či jednotkové testy.
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
13Jak byste strukturovali sdílenou funkcionalitu tak, aby prezentační vrstva, doménové případy užití (use cases) a přístup k datům zůstaly nezávisle testovatelné a znovupoužitelné napříč platformami iOS a Android?
Pro strukturování sdílené funkcionality umožňující nezávislou testovatelnost a znovupoužitelnost napříč platformami iOS a Android použijte čistou vícevrstvou architekturu rozdělenou na prezentační, doménovou a datovou vrstvu: 1. **Doménová vrstva (čistá sdílená logika):** Obsahuje čisté byznysové entity, rozhraní repozitářů (kontrakty) a případy užití / interaktory (např. `GetCartUseCase`, `ApplyPromoCodeUseCase`). Tato vrstva nemá žádné závislosti na UI frameworcích (UIKit, SwiftUI, Android Views, Jetpack Compose, widgety ve Flutteru) ani na platformních API. Každý případ užití zapouzdřuje jedinou byznysovou operaci, což umožňuje jeho stoprocentní jednotkovou testovatelnost pomocí běžných mock objektů. 2. **Datová vrstva (přístup k datům a zapouzdření):** Implementuje rozhraní doménových repozitářů. Komunikuje se vzdálenými datovými zdroji (REST/GraphQL) a lokálním perzistentním úložištěm (SQLite/Key-Value). Síťové DTO (Data Transfer Object) a databázové entity jsou před opuštěním této vrstvy striktně mapovány na čisté doménové entity, což zabraňuje úniku externích serializačních schémat nebo změn v databázi do doménové či prezentační vrstvy. 3. **Prezentační vrstva (UI a správa stavu):** Skládá se ze správců stavu (ViewModels, Blocs nebo Presenters) a pohledů/widgetů UI. Správce stavu volá doménové případy užití, spravuje stav UI (načítání, úspěch, chyba) a vystavuje sledovatelný stav pro view. Pohled tento stav pouze sleduje a vykresluje. Tato struktura umožňuje jednotkové testování případů užití v izolaci bez UI, mockování repozitářů při testování view modelů a výměnu implementací prezentační vrstvy mezi platformami při zachování celé doménové a datové logiky.
[ 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 ]
14Jak vyhodnocujete, kdy lokální stav komponenty již nestačí, a jak volíte vhodný přístup k architektonické správě stavu (state management) pro multiplatformní kódovou bázi vyvíjenou více vývojáři?
V multiplatformním mobilním vývoji je přechod od lokálního stavu komponent (`useState`/`StatefulWidget`) k architektonickému řešení správy stavu řízen rozsahem stavu, požadavky na životní cyklus a testovatelností:
1. **Kdy lokální stav přestává stačit**:
- **Sdílení napříč komponentami a prop drilling**: Když je potřeba ke stavu přistupovat nebo jej upravovat napříč nesouvisejícími navigačními trasami nebo vzdálenými větvemi stromu komponent/widgetů.
- **Perzistence životního cyklu**: Když data musí přetrvat zánik obrazovky, přechody mezi trasami nebo reset navigace (např. uživatelská relace, obsah košíku, mezipaměť feedů).
- **Oddělení zodpovědností a testovatelnost**: Když se byznysová logika, validace a vedlejší účinky provážou s komponentami uživatelského rozhraní, což ztěžuje automatizované headless unit testování.
2. **Volba architektury pro kódové báze s více vývojáři**:
- **Jednosměrný tok dat / předvídatelné kontejnery (např. BLoC, Redux, Riverpod)**: Vynucují striktní oddělení, kde UI odesílá explicitní události/akce a vykresluje neměnný stav emitovaný vyhrazenými jednotkami byznysové logiky. To vytváří jasné kontrakty, omezuje vedlejší účinky a umožňuje nezávislé jednotkové testování byznysové logiky.
- **Atomická / scoped reaktivní úložiště (např. Zustand, MobX, Provider)**: Nabízejí méně šablonovitého kódu (boilerplate) a flexibilní selektorové odběry, což je vhodné pro týmy hledající odlehčené oddělení a cílené překreslování konkrétních komponent.
V týmovém prostředí je primárním architektonickým cílem izolovat čistou byznysovou logiku od vykreslovacích vrstev UI a využívat selektivní odběry, které zabrání zbytečnému překreslování celého stromu komponent.
15Jak byste diagnostikovali a snížili zasekávání (jank) na obrazovce multiplatformní mobilní aplikace, která se trhá při posouvání dlouhého seznamu obrázků a dynamického obsahu?
Nejprve bych trhání nasimuloval na reálných zařízeních a provedl profilování, protože zasekávání při posouvání (scrolling jank) může mít řadu příčin: nestíhání časového limitu na snímek (frame budget), zátěž v JavaScriptu či Dartu, práci na hlavním vlákně / vlákně uživatelského rozhraní (main/UI thread), rasterizaci a zátěž na GPU (Graphics Processing Unit), výpočet rozvržení (layout), dekódování obrázků, paměťový tlak nebo načítání ze sítě. Při 60 Hz má aplikace přibližně 16,7 ms na snímek a při 120 Hz zhruba 8,3 ms, takže náročné vykreslování, dekódování, výpočet rozvržení nebo synchronní výpočty během posouvání mohou způsobit výpadky snímků (dropped frames). K identifikaci skutečného úzkého hrdla bych použil nástroje jako Flutter DevTools, nástroje pro měření výkonu v React Native nebo Flipper, Android Studio Profiler, Xcode Instruments a časové osy snímků (frame timeline). U dlouhého seznamu bych zajistil líné načítání (lazy loading) nebo virtualizaci: například `FlatList`, `FlashList` nebo `RecyclerListView` v React Native, případně `ListView.builder`, `SliverList` či podobné buildery ve Flutteru. Použil bych stabilní klíče, vyhnul se přebudování či opětovnému vykreslování každého řádku při změně stavu nadřazené komponenty, vhodně memoizoval komponenty řádků či selektory, udržoval metody `build`/`renderItem` výpočetně nenáročné a přesunul řazení, filtrování, parsování JSON (JavaScript Object Notation), formátování i zpracování obrázků mimo kritickou cestu posouvání. Pokud jsou rozměry položek předvídatelné, poskytl bych nápovědy pro rozvržení, jako je `getItemLayout` v React Native nebo fixní/prototypové rozměry položek ve Flutteru. U obrázků bych načítal náhledy odpovídající velikosti, cachoval je, vyhnul se dekódování obrázků v plném rozlišení pro malé buňky, používal zástupné symboly (placeholders) a líné načítání a hlídal paměťové výkyvy způsobené příliš mnoha velkými bitmapami. Také bych zjednodušil příliš složité rozvržení řádků, omezil náročné stíny, ořezávání (clipping), překreslování (overdraw) a průhlednost (opacity), dávkoval nebo stránkoval data a následně znovu provedl profilování, abych ověřil, že se výpadky snímků a doba zpracování snímku zlepšily.
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.
16Jak fungují hranice mezi vlákny při voláních přes nativní přemostění (native bridge) a jak zabráníte zpožděním při přepínání vláken (thread-hopping stalls) nebo trhání UI (User Interface), když nativní moduly provádějí náročnou práci na pozadí?
Multiplatformní frameworky používají pro volání přes přemostění (bridge) specifické konvence práce s vlákny: například standardní volání `MethodChannel` ve Flutteru přicházejí do hlavního UI vlákna dané platformy, zatímco tradiční volání přes bridge v React Native běží na vyhrazeném JavaScriptovém vlákně a asynchronně se předávají nativním vláknům. Pokud se metoda nativního bridge vykonává na nativním hlavním/UI vlákně, spuštění výpočetně náročných operací, dlouhých synchronních diskových I/O nebo blokujících síťových úloh zablokuje hlavní smyčku událostí (run loop). To vede k vynechávání snímků, viditelnému trhání uživatelského rozhraní (UI jank) a na Androidu k chybám ANR (Application Not Responding) nebo na iOS k vynucenému ukončení procesem watchdog. Aby se tomu předešlo, obslužné rutiny nativního bridge by měly náročnou nebo blokující práci přesměrovat do fondů vláken na pozadí (např. Kotlin Coroutines s `Dispatchers.IO`/`Dispatchers.Default`, Android `ThreadPoolExecutor` nebo Swift `Task.detached` / GCD `DispatchQueue.global()`). Jakmile je práce hotová, výsledek se musí odeslat zpět do vlákna bridge očekávaného frameworkem (např. vrácení výsledků na UI vlákně pro standardní MethodChannels nebo použití front úloh na pozadí), což zajistí, že multiplatformní aktualizace stavu nebudou blokovat cyklus vykreslování UI.
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()
}
}
}
17Jak byste navrhli strategii cachování a invalidace pro obrazovky se seznamy a feedy pomocí vzoru Stale-While-Revalidate a podmíněných hlaviček protokolu HTTP (Hypertext Transfer Protocol)?
Spolehlivá strategie cachování a invalidace pro obrazovky s feedy a seznamy kombinuje vzor Stale-While-Revalidate (SWR) s podmíněnými validačními hlavičkami HTTP (jako jsou `ETag` / `If-None-Match` nebo `Last-Modified` / `If-Modified-Since`). Při otevření obrazovky se okamžitě načtou a vykreslí nacachovaná data z lokální databáze (jediný zdroj pravdy – Single Source of Truth, např. Room nebo Core Data/SQLite), což uživateli poskytne okamžité zobrazení v UI. Současně je na pozadí odeslán síťový požadavek s nacachovanou hlavičkou `If-None-Match: <etag>`. Pokud server odpoví kódem `304 Not Modified`, nepřenáší se žádné tělo zprávy, čímž se potvrdí platnost lokální cache a ušetří se přenosové pásmo i baterie. Pokud server odpoví `200 OK`, nová data a aktualizovaný `ETag` se v transakci zapíší do lokální databáze a reaktivní pozorovatelé databáze automaticky odešlou aktualizovaný seznam do UI. Pro stránkování se ukládají stránkovací tokeny nebo offsety společně s nacachovanými záznamy. Invalidace je vyvolána vypršením TTL (Time to Live), akcemi uživatele (např. gesto pull-to-refresh obcházející podmíněné hlavičky) nebo vedlejšími účinky lokálních mutací (např. vytvoření nebo smazání položky lokálně optimisticky aktualizuje databázi a označí kurzory nacachovaných stránek k opětovné validaci).
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
}
}
18Finanční mobilní aplikace musí bezpečně ukládat obnovovací tokeny (refresh tokens) v zařízení, podporovat biometrické odemykání, zachovat funkčnost i po aktualizaci OS (Operating System) a fungovat bezpečně i během dočasných offline relací. Jak byste navrhli architekturu ukládání těchto tokenů a přístupu k nim?
Robustní architektura pro ukládání tokenů v mobilních zařízeních využívá hardwarově zabezpečené moduly: iOS Keychain postavený na Secure Enclave a Android Keystore podložený StrongBoxem nebo Trusted Execution Environment (TEE). Obnovovací tokeny (refresh tokens) musí být šifrovány v klidovém stavu (at rest) pomocí klíčů chráněných biometrickou kryptografickou kontrolou (například `kSecAccessControlBiometryCurrentSet` nebo `kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly` na iOS a `setUserAuthenticationRequired(true)` s `AUTH_BIOMETRIC_STRONG` na Androidu). Kryptografické podmínění zajišťuje, že hardware dešifruje klíč pouze po úspěšném biometrickém ověření, namísto spoléhání se na snadno obejitelnou booleovskou kontrolu v kódu aplikace. Pro bezproblémové fungování i po aktualizacích OS a současné zabránění neoprávněnému přístupu se klíče vážou na hardware zařízení a vynucují pravidla změn biometrických záznamů, která zneplatní klíče nebo vyžádají opětovné ověření, pokud je zaregistrován nový biometrický údaj (např. `setInvalidatedByBiometricEnrollment(true)` na Androidu). Pro dočasné offline relace mohou krátkodobé šifrované přístupové tokeny (access tokens) a omezené lokální offline stavy fungovat v rámci definovaných lokálních TTL a vymezených oprávnění, zatímco privilegované akce obnovení jsou odloženy, dokud se neobnoví připojení k síti. Po opětovném připojení se relace ověří rotací obnovovacích tokenů s detekcí opakovaného použití na backendu; pokud je zjištěno znovupoužití tokenu nebo zrušení účtu, backend odešle signál k invalidaci, který spustí promazání hardwarově chráněných klíčů a vyčištění lokálního offline úložiště na klientovi.
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()
19Jak byste navrhli škálovatelnou infrastrukturu CI/CD (Continuous Integration/Continuous Deployment) pro multiplatformní mobilní monorepo podporující více týmů při současné optimalizaci omezené a nákladné kapacity macOS runnerů?
Návrh škálovatelné CI/CD infrastruktury pro multiplatformní mobilní monorepo s optimalizací nedostatkové a nákladné kapacity macOS vyžaduje architekturu postavenou na detekci dotčených závislostí v grafu sestavení (affected build detection), striktním rozdělení zátěže mezi hybridní fondy runnerů a virtualizaci/efemérní orchestraci pro macOS. Zaprvé implementujte nástroj pro správu grafu sestavení monorepa (například Bazel, Nx nebo Turborepo) s distribuovaným vzdáleným cachováním. Při každém pull requestu CI pipeline spočítá rozdíl v orientovaném acyklickém grafu (DAG) vůči cílové větvi, aby se sestavovaly a testovaly pouze ovlivněné balíčky a jejich závislé moduly, čímž se zcela přeskočí nezměněné části. Zadruhé zaveďte strategii hybridního přidělování runnerů: přesuňte všechny úlohy, které striktně nevyžadují Xcode (například linting a kompilaci v TypeScriptu/Dartu, jednotkové testy, statickou analýzu kódu, bezpečnostní skenování a sestavování Androidu přes Gradle), na nákladově efektivní a škálovatelné Linux/Kubernetes runnery. macOS runnery vyhraďte výhradně pro finální sestavení pro iOS, kompilaci Swiftu/Objective-C, podepisování kódu a spouštění testů v iOS Simulátoru. Zatřetí spravujte macOS runnery pomocí virtualizační infrastruktury (například Tart, Anka nebo bare-metal uzly AWS/MacStadium orchestrované přes Nomad či Kubernetes). Každé sestavení pro iOS běží v čistém, efemérním virtuálním stroji s předehřátými sadami nástrojů a mezipamětí odvozených dat (derived-data caches), což zabraňuje nežádoucím odchylkám v prostředí runnerů (runner drift) a umožňuje rychlé automatické škálování podle délky fronty v pipeline.
20Jak byste navrhli multiplatformní mobilní architekturu pro produkt, u kterého se očekává sdílení většiny byznys logiky mezi iOS a Androidem, a přitom umožnili nezávislý vývoj specifického UI (User Interface), navigace a nativních integrací pro každou platformu?
Aplikaci bych navrhl kolem sdíleného jádra, které spravuje stabilní chování byznys logiky: doménové modely, validace, případy užití (use cases), byznys pravidla a kontrakty pro přístup k datům. Záležitosti specifické pro danou platformu – UI, navigace, nativní SDK, oprávnění, správa životního cyklu a obalové struktury aplikace (app shell) – by měly zůstat mimo toto jádro.
Sdílený kód by neměl importovat UIKit, SwiftUI, Jetpack, rozhraní frameworku Android, navigaci z React Native, Flutter navigaci ani detaily nativních SDK. Místo toho by měl záviset na úzce vymezených rozhraních, jako jsou AuthRepository, SecureStorage, Analytics, CameraService nebo PaymentProvider, která implementuje každá platforma zvlášť.
Kód bych organizoval co nejvíce podle jednotlivých funkcionalit (features) namísto vytváření jedné velké sdílené vrstvy. Například nákupní košík, vyhledávání, uživatelský účet a zprávy mohou mít každý svůj sdílený doménový/aplikační kód, stavové kontrakty a rozhraní repozitářů. Obalové vrstvy pro iOS a Android pak mají na starosti skládání obrazovek, nativní vzory UI, navigační zásobníky, oprávnění a propojování adaptérů.
Prezentační logiku lze sdílet, pokud je skutečně nezávislá na UI frameworku – například reducery, stavové automaty nebo kontrakty pro view modely, které emitují jednoduchý stav a akce. Samotné zobrazení a navigace by však měly zůstat v režii konkrétní platformy, aby se mohly vyvíjet nezávisle.
Hlavním kompromisem je důsledně sdílet společnou, stabilní byznys logiku a zároveň se vyvarovat abstrakcí, které zastírají reálné rozdíly mezi platformami. Hranice bych vynucoval pomocí pravidel závislostí modulů, dependency injection, veřejných API, testů kontraktů, architektonických kontrol a jasného vlastnictví kódu. Nativní integrace by měly využívat architekturu portů a adaptérů, aby sdílený kód viděl jednotnou funkcionalitu, zatímco každá platforma si ve své vlastní vrstvě řeší chování SDK, životní cyklus, oprávnění, chyby a specifika uživatelského rozhraní.