Příprava na pohovor Android

Otázky k pohovoru pro Android

20 častých otázek k pohovoru pro Android. Android vývoj je žádaný obor zaměřený na mobilní aplikace v Kotlinu, Android SDK a Jetpack a řeší lifecycle, výkon, síť, ukládání dat, testování a releasy. Otázky pokrývají různé úrovně a odpovědi si můžeš nahlas procvičit v našem pohovorovém trenéru.

Spustit AI pohovor pro AndroidNení vyžadována platební karta. K dispozici je 1 bezplatná relace.
Příprava na technické pohovory v angličtiněRežim pro lidi, pro které angličtina není rodným jazykem, určený k procvičování technických pohovorů.

Otázky pro začátečníky

1Popište hlavní zpětná volání (callbacks) životního cyklu komponenty Activity od vytvoření po zničení a vysvětlete rozdíly mezi onPause(), onStop() a onDestroy() z hlediska uvolňování prostředků.

Aktivita (Activity) prochází šesti základními zpětnými voláními životního cyklu: onCreate(), onStart(), onResume(), onPause(), onStop() a onDestroy(). - onCreate(): Provádí jednorázovou inicializaci, například nafouknutí layoutu (view inflation). - onStart(): Činí aktivitu viditelnou pro uživatele. - onResume(): Přesouvá aktivitu do popředí, kde získává interaktivní fokus. Při odchodu z aktivity: - onPause(): Indikuje, že aktivita ztratila fokus. - onStop(): Značí, že aktivita již není na obrazovce viditelná. - onDestroy(): Představuje finální zrušení instance aktivity. Z hlediska uvolňování prostředků: - onPause(): Zde pozastavujte pouze rychlé operace specifické pro popředí (např. pozastavení animací UI nebo nenáročných náhledů fotoaparátu), protože běh kódu v onPause() přímo blokuje spuštění následující aktivity. - onStop(): Hlavní místo pro uvolnění náročnějších prostředků vázaných na viditelnost UI (např. posluchače polohy/GPS, data ze senzorů, přehrávání médií nebo pravidelné dotazování sítě). Protože aktivita není vůbec viditelná, jejich ponechání v aktivním stavu plýtvá baterií a systém může proces na pozadí kdykoli bez varování ukončit. - onDestroy(): Slouží k finálnímu úklidu instance aktivity (např. ukončení lokálních vláken nebo zrušení referencí). Uvolnění kritických prostředků by se však nemělo odkládat až do onDestroy(), protože systém Android může zastavený proces aplikace ukončit za účelem uvolnění paměti přímo, aniž by metodu onDestroy() vůbec zavolal.

class LocationActivity : AppCompatActivity() {
    private var locationClient: LocationClient? = null

    override fun onStart() {
        super.onStart()
        // Start listening when visible to the user
        locationClient?.startLocationUpdates()
    }

    override fun onStop() {
        super.onStop()
        // Release when invisible to save battery and prevent leaks on process kill
        locationClient?.stopLocationUpdates()
    }
}
Zkusit odpovědět na tuto otázku s trenérem AI

2Jaké jsou čtyři primární komponenty aplikace v systému Android a jaký je životní cyklus a model vláken jejich výchozích zpětných volání (callbacks)?

Čtyři primární komponenty aplikace v systému Android jsou Activity, Service, BroadcastReceiver a ContentProvider. Activity poskytuje uživatelské rozhraní pro interakci na obrazovce. Service provádí zpracování na pozadí bez vyhrazeného UI. BroadcastReceiver naslouchá celosystémovým nebo aplikačním všesměrovým oznámením a reaguje na ně. ContentProvider spravuje a zpřístupňuje strukturovaná data ostatním aplikacím nebo interním modulům. Ve výchozím nastavení se primární metody životního cyklu pro Activity (např. onCreate, onStart, onResume), Service (např. onCreate, onStartCommand, onBind) a BroadcastReceiver (onReceive) vykonávají synchronně v hlavním vlákně aplikace (UI thread). Metody komponenty ContentProvider (např. onCreate) se také inicializují na hlavním vlákně, ačkoli operace query/insert vyvolané napříč procesy běží ve vláknech z fondu vláken Binderu. Vzhledem k tomu, že výchozí zpětná volání běží na hlavním vlákně, přímé provádění náročných blokujících operací, jako jsou síťové požadavky nebo rozsáhlé diskové I/O operace databáze, blokuje smyčku uživatelského rozhraní a vyvolá chybu ANR (Application Not Responding).

class DataSyncService : Service() {
    private val serviceScope = CoroutineScope(Dispatchers.IO + SupervisorJob())

    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
        // onStartCommand runs on the Main Thread; heavy work must be offloaded
        serviceScope.launch {
            performDiskAndNetworkSync()
            stopSelf(startId)
        }
        return START_NOT_STICKY
    }

    override fun onBind(intent: Intent?): IBinder? = null

    override fun onDestroy() {
        super.onDestroy()
        serviceScope.cancel()
    }

    private fun performDiskAndNetworkSync() {
        // Blocking I/O safely executed on Dispatchers.IO
    }
}
Zkusit odpovědět na tuto otázku s trenérem AI

3Jaké jsou v architektuře MVVM (Model-View-ViewModel) v Androidu hlavní odpovědnosti komponenty ViewModel ve srovnání s UI (User Interface) kontrolery, jako jsou Activity a Fragment, a proč se nedoporučuje předávat do ViewModelu Context?

V architektuře MVVM pro Android zodpovídá ViewModel za uchovávání a správu stavu uživatelského rozhraní, řízení prezentační logiky a zachování stavu při změnách konfigurace (jako je otočení obrazovky). Naproti tomu UI kontrolery (Activity a Fragment) se starají o vykreslování prvků rozhraní na obrazovce, sledování změn stavu, obsluhu přímých uživatelských interakcí (kliknutí, gesta) a řízení událostí životního cyklu Androidu. Předávání reference na `Context` z Activity nebo reference na View do ViewModelu se důrazně nedoporučuje, protože ViewModel obvykle přežívá životní cyklus UI kontrolerů při změnách konfigurace. Když je Activity při otočení zničena a znovu vytvořena, ViewModel držící její `Context` zabrání jejímu uvolnění správcem paměti (Garbage Collector), což vede k úniku paměti (memory leak). Pokud je Android `Context` nezbytný pro systémové operace, měl by se místo toho použít kontext na úrovni celé aplikace (například prostřednictvím `AndroidViewModel` nebo injektováním ApplicationContext).

// Bad: Storing Activity context or View reference causes leaks
class LeakyViewModel(private val activityContext: Context) : ViewModel() {
    fun showToast() { Toast.makeText(activityContext, "Hello", Toast.LENGTH_SHORT).show() }
}

// Good: ViewModel manages state; Activity observes and handles context-specific tasks
class SafeViewModel(private val repository: UserRepository) : ViewModel() {
    private val _uiState = MutableStateFlow<UserUiState>(UserUiState.Loading)
    val uiState: StateFlow<UserUiState> = _uiState.asStateFlow()
    
    fun loadUser(id: String) {
        viewModelScope.launch {
            _uiState.value = UserUiState.Success(repository.getUser(id))
        }
    }
}
Zkusit odpovědět na tuto otázku s trenérem AI

4Jaký je rozdíl mezi běžnými (normal) a nebezpečnými (dangerous) oprávněními v systému Android a co se stane, když aplikace požádá o nebezpečné oprávnění za běhu (runtime)?

Běžná oprávnění (normal permissions) představují minimální riziko pro soukromí uživatele nebo provoz zařízení a systém je automaticky uděluje při instalaci aplikace, pokud jsou deklarována v souboru `AndroidManifest.xml` (například `ACCESS_NETWORK_STATE`). Nebezpečná oprávnění (dangerous permissions, označovaná také jako runtime permissions) řídí přístup k citlivým datům uživatele nebo k omezeným funkcím zařízení, jako jsou fotoaparát, mikrofon, poloha a kontakty. Když aplikace požádá o nebezpečné oprávnění za běhu, systém Android zobrazí systémové dialogové okno vyzývající uživatele k povolení nebo zamítnutí přístupu. Pokud uživatel oprávnění udělí, aplikace provede požadovanou funkci. Pokud uživatel oprávnění zamítne, aplikace obdrží zpětné volání (callback) s informací o zamítnutí a musí tuto situaci korektně ošetřit – zakázat nebo omezit závislou funkci bez pádu aplikace a volitelně zobrazit vysvětlující uživatelské rozhraní (UI rationale).

val requestPermissionLauncher = registerForActivityResult(
    ActivityResultContracts.RequestPermission()
) { isGranted: Boolean ->
    if (isGranted) {
        accessCameraFeature()
    } else {
        showPermissionDeniedExplanation()
    }
}

fun checkAndRequestPermission() {
    when {
        ContextCompat.checkSelfPermission(context, Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED -> {
            accessCameraFeature()
        }
        shouldShowRequestPermissionRationale(Manifest.permission.CAMERA) -> {
            showRationaleDialog { requestPermissionLauncher.launch(Manifest.permission.CAMERA) }
        }
        else -> {
            requestPermissionLauncher.launch(Manifest.permission.CAMERA)
        }
    }
}
Zkusit odpovědět na tuto otázku s trenérem AI

5Jaký je rozdíl mezi sestaveními typu debug a release v systému Android a jak se typy sestavení (build types) a produktové varianty (product flavors) kombinují do variant sestavení (build variants)?

Při vývoji pro Android konfigurují typy sestavení (build types) vlastnosti balíčkování a běhu pro různé fáze vývoje. Sestavení typu debug povolují ladicí příznaky (`isDebuggable = true`), používají výchozí ladicí úložiště klíčů (keystore) a obvykle vypínají minifikaci a zmenšování kódu (R8/ProGuard) kvůli rychlým iteracím sestavení. Sestavení typu release naopak ladicí příznaky vypínají, zapínají optimalizace a obfuskaci kódu a pro distribuci vyžadují zabezpečený podpisový klíč. Produktové varianty (product flavors) představují různé verze nebo cíle aplikace sdílející stejný základ kódu, například různá prostředí (staging vs. production) nebo produktové edice (zdarma vs. placená). V nástroji Gradle vznikají varianty sestavení (build variants) kartézským součinem produktových variant a typů sestavení (Build Variant = Product Flavor × Build Type). Například kombinací variant `staging` a `production` s typy sestavení `debug` a `release` vzniknou čtyři varianty sestavení: `stagingDebug`, `stagingRelease`, `productionDebug` a `productionRelease`.

android {
    flavorDimensions += "environment"
    productFlavors {
        create("staging") {
            dimension = "environment"
            applicationIdSuffix = ".staging"
        }
        create("production") {
            dimension = "environment"
        }
    }
    buildTypes {
        getByName("debug") {
            isDebuggable = true
        }
        getByName("release") {
            isMinifyEnabled = true
            isShrinkResources = true
            proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
        }
    }
}
Zkusit odpovědět na tuto otázku s trenérem AI

6Co je to korutina v Kotlinu a jak se pozastavení za běhu v Androidu liší od blokování vlákna?

Korutina v Kotlinu je odlehčená abstrakce pro souběžnost, která umožňuje psát asynchronní, neblokující kód sekvenčním způsobem. Více korutin může běžet souběžně na jednom vlákně nebo napříč fondy vláken (thread pools) bez vysoké režie na paměť a přepínání kontextu, kterou mají vlákna na úrovni operačního systému. Zásadní rozdíl mezi pozastavením a blokováním spočívá ve využití vlákna: - Blokování (např. Thread.sleep() nebo synchronní diskové/síťové I/O) zastaví příslušné podkladové vlákno a znemožní jeho využití pro jakoukoli jinou práci. Na hlavním vlákně Androidu blokování způsobí zamrznutí UI (User Interface) a vyvolá chyby typu ANR (Application Not Responding). - Pozastavení (pomocí suspend funkcí jako delay()) pozastaví korutinu bez zastavení podkladového vlákna. Korutina zachytí svůj stav vykonávání v objektu Continuation a přenechá vlákno běhovému prostředí, aby mohlo vykonávat jinou práci (například obsluhovat interakce v UI). Jakmile asynchronní operace skončí, korutina se obnoví.

suspend fun fetchUserData(): String {
    delay(1000) // Suspends execution and yields the thread
    return "User Data"
}

fun fetchUserDataBlocking(): String {
    Thread.sleep(1000) // Blocks and locks the underlying thread
    return "User Data"
}
Zkusit odpovědět na tuto otázku s trenérem AI

7Jaký je v Jetpack Compose rozdíl mezi stavem uchovaným pomocí remember oproti rememberSaveable a jak mutableStateOf řídí aktualizace UI (User Interface) napříč rekompozicemi?

V Jetpack Compose `remember` ukládá objekt do paměti napříč rekompozicemi po dobu, kdy composable zůstává ve stromu kompozice. Stav držený pouze pomocí `remember` se však ztrácí během změn konfigurace (jako je otočení obrazovky) a při ukončení procesu systémem. Oproti tomu `rememberSaveable` zachovává stav i napříč změnami konfigurace a ukončením procesu tím, že hodnoty automaticky ukládá a obnovuje prostřednictvím Android Saved Instance State bundle (nebo pomocí vlastních implementací `Saver` pro nepodporované datové typy). `mutableStateOf` obaluje hodnotu do pozorovatelného objektu `MutableState`, který je postaven na stavovém systému Snapshot v Compose. Když composable během kompozice čte hodnotu `MutableState`, Compose tuto operaci čtení zaznamená. Jakmile se hodnota později změní, Compose automaticky označí všechny composables, které tuto hodnotu četly, za neplatné a naplánuje jejich rekompozici, což zajišťuje, že UI zůstává synchronizované s aktualizovaným stavem.

@Composable
fun CounterExample() {
    // Resets to 0 on screen rotation
    var localCount by remember { mutableStateOf(0) }

    // Persists across screen rotation and process death
    var persistentCount by rememberSaveable { mutableStateOf(0) }

    Column {
        Button(onClick = { localCount++; persistentCount++ }) {
            Text("Local: $localCount | Saved: $persistentCount")
        }
    }
}
Zkusit odpovědět na tuto otázku s trenérem AI

8Jak typová bezpečnost vůči hodnotám null (null safety) v Kotlinu pomáhá předcházet pádům v aplikacích pro Android a kdy byste měli použít typ s povolenou hodnotou null oproti typu bez povolené hodnoty null?

Typová bezpečnost vůči hodnotám null (null safety) v Kotlinu pomáhá předcházet mnoha pádům v Androidu tím, že činí možnost hodnoty null součástí typového systému. Hodnota deklarovaná jako `String` je považována za non-null (nesmí být null), takže překladač umožňuje přímý přístup jako `name.length`. Hodnota deklarovaná jako `String?` může mít hodnotu null, takže vás Kotlin nutí tuto situaci před použitím ošetřit, což omezuje neúmyslné výjimky `NullPointerException` vznikající z volitelných doplňkových dat Intentu (extras), odpovědí API, argumentů Bundle nebo rozhraní z Javy a platformy. Typ bez možnosti null použijte tehdy, když je hodnota nezbytná pro platnost objektu nebo funkce. Typ s povolenou hodnotou null použijte v případě, kdy je nepřítomnost hodnoty reálně očekávaným stavem – například volitelná URL adresa profilového obrázku nebo chybějící pole ze serveru. Hodnoty typu nullable se obvykle ošetřují bezpečným voláním jako `user?.name`, Elvis operátorem jako `user?.name ?: "Guest"` nebo explicitními kontrolami na null. Operátor `!!` vyřazuje bezpečnostní kontroly a v případě hodnoty null vyvolá výjimku, proto by se měl používat jen zřídka a pouze tehdy, když vývojář může skutečně prokázat, že hodnota nemůže být null.

val displayName: String? = intent.getStringExtra("display_name")

val greeting = "Hello, ${displayName ?: "Guest"}"
val lengthText = displayName?.length?.toString() ?: "No name provided"

// Risky: crashes if displayName is null
val riskyLength = displayName!!.length
Zkusit odpovědět na tuto otázku s trenérem AI

9Jaký je technický rozdíl mezi předáním kontextu aktivity (Activity Context) a kontextu aplikace (Application Context) dlouho žijícím objektům a jak volba nesprávné varianty vede k úniku paměti (memory leak)?

`Activity` `Context` je vázán přímo na životní cyklus konkrétní obrazovky a hierarchie uživatelského rozhraní, zatímco `Application` `Context` je vázán na životní cyklus celého procesu aplikace. Když je aktivita dokončena nebo znovu vytvořena (např. při změně konfigurace, jako je otočení obrazovky), její životní cyklus skončí a její paměť má být uvolněna správcem paměti (Garbage Collector – GC). Pokud předáte `Activity` `Context` dlouho žijícímu objektu – jako je singleton, statické pole nebo dlouho běžící vlákno na pozadí –, tento objekt si udrží silnou referenci (strong reference) na danou aktivitu. Protože dlouho žijící objekty fungují jako kořeny GC (GC roots) nebo jsou z nich dosažitelné, správce paměti nemůže zničenou aktivitu odstranit. Tím se v paměti zachová nejen samotná instance aktivity, ale také celá její připojená hierarchie `View`, grafické prostředky (drawables) a další zdroje, což způsobí značný únik paměti. Naproti tomu předání `applicationContext` je pro dlouho žijící komponenty bezpečné, protože se očekává, že jeho životnost odpovídá životnosti celého procesu.

class LocationRepository private constructor(private val context: Context) {
    companion object {
        @Volatile
        private var INSTANCE: LocationRepository? = null

        fun getInstance(context: Context): LocationRepository {
            return INSTANCE ?: synchronized(this) {
                // SAFE: Using context.applicationContext prevents retaining an Activity reference
                INSTANCE ?: LocationRepository(context.applicationContext).also { INSTANCE = it }
            }
        }
    }
}
Zkusit odpovědět na tuto otázku s trenérem AI

10Jaký je rozdíl mezi klíčem pro nahrávání (upload key) a klíčem pro podepisování aplikace (app signing key) při použití služby Google Play App Signing?

V rámci služby Google Play App Signing plní klíč pro nahrávání (upload key) a klíč pro podepisování aplikace (app signing key) dvě odlišné bezpečnostní role. Klíč pro nahrávání si ponechává vývojář a podepisuje jím artefakt sestavení (AAB) před nahráním do konzole Google Play Console, čímž Googlu ověřuje svou identitu. Klíč pro podepisování aplikace je bezpečně uložen v cloudové infrastruktuře Google a služba Google Play jej používá k podepisování vygenerovaných balíčků APK, které jsou skutečně doručovány a instalovány na koncová zařízení uživatelů, čímž vytváří trvalou kryptografickou identitu aplikace v systému Android. Hlavním provozním přínosem je možnost obnovy klíče: pokud vývojář svůj nahrávací klíč ztratí nebo dojde k jeho kompromitaci, podpora Google Play může po ověření identity vývojáře nahrávací klíč resetovat, aniž by byla narušena možnost aktualizace aplikace. V původním modelu, kdy vývojáři spravovali přímo klíč pro podepisování aplikace, znamenala ztráta tohoto klíče nemožnost aplikaci kdykoliv v budoucnu aktualizovat.

android {
    signingConfigs {
        create("release") {
            storeFile file("my-upload-key.jks")
            storePassword System.getenv("KEYSTORE_PASSWORD")
            keyAlias "upload-alias"
            keyPassword System.getenv("KEY_PASSWORD")
        }
    }
    buildTypes {
        release {
            signingConfig signingConfigs.getByName("release")
            minifyEnabled true
        }
    }
}
Zkusit odpovědět na tuto otázku s trenérem AI

Otázky pro mírně pokročilé

11Po otočení zařízení dojde na obrazovce ke ztrátě dat zadaných do formuláře. Jak byste systematicky diagnostikovali, zda je problém způsoben chybějícím uloženým stavem (saved state), nesprávným rozsahem platnosti (scoping) ViewModelu nebo opětovným navázáním zobrazení (view rebinding)?

Při systematické diagnostice ztráty dat z formuláře po otočení zařízení je třeba izolovat tři klíčové oblasti: 1. **Rozsah platnosti (scoping) a zachování ViewModelu:** Ověřte, že instance ViewModelu je při změně konfigurace skutečně zachována a nedochází k jejímu opětovnému vytvoření. Zaznamenávejte hash kód ViewModelu (`viewModel.hashCode()`) napříč otočeními. Ujistěte se, že je získáván pomocí delegovaných vlastností jako `by viewModels()` / `by activityViewModels()` nebo `ViewModelProvider(this)`, nikoli přímou instanciací přes konstruktor (např. `MyViewModel()`). 2. **Uložený stav a obnova pomocí ID komponent:** Zkontrolujte, zda formulář nespoléhal výhradně na výchozí obnovu hierarchie zobrazení v Androidu. Komponenty (views) musí mít v layoutu definovaný atribut `android:id`, aby se zapojily do automatického ukládání a obnovy stavu. Pokud používáte vlastní komponenty (custom views) nebo `SavedStateHandle`, ověřte, že `onSaveInstanceState` / `SavedStateHandle` příslušná pole skutečně ukládá a obnovuje. 3. **Opětovné navázání komponent (view rebinding) a logika observerů:** Zkontrolujte `onViewCreated` nebo odběry observerů (`StateFlow`, `LiveData`). Ověřte, zda se nově vytvořené komponenty správně znovu přihlásí k odběru, nebo zda inicializační kód zobrazení omylem nepřepisuje obnovený text výchozími prázdnými hodnotami. Dále zkontrolujte obousměrné vázání (two-way binding) nebo smyčky v `TextWatcher`, kde prázdná obnovená komponenta okamžitě odešle prázdnou aktualizaci zpět do ViewModelu.

class FormFragment : Fragment(R.layout.fragment_form) {
    // 1. Ensure correct lifecycle scoping
    private val viewModel: FormViewModel by viewModels()

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        Log.d("FormDiag", "VM instance hash: ${viewModel.hashCode()}")

        // 2. Observe state without clobbering input during view recreation
        viewLifecycleOwner.lifecycleScope.launch {
            viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
                viewModel.formText.collect { text ->
                    if (binding.inputField.text.toString() != text) {
                        binding.inputField.setText(text)
                    }
                }
            }
        }
    }
}
Zkusit odpovědět na tuto otázku s trenérem AI

12Jak moderní Jetpack Activity Result API (Application Programming Interface) nahrazuje zastaralé startActivityForResult a jak chrání před ztrátou zpětných volání (callbacks) během znovuvytvoření Activity?

Moderní Jetpack Activity Result API nahrazuje zastaralé `startActivityForResult()` a `onActivityResult()` typově bezpečným, odděleným mechanismem založeným na kontraktech. Namísto správy libovolných celočíselných kódů požadavků a rozsáhlých příkazů `switch` v `onActivityResult` volající definují `ActivityResultContract<I, O>` (který specifikuje vstupní parametry intentu a očekávaný naparsovaný výstup) a zaregistrují typované zpětné volání pomocí `registerForActivityResult()`, čímž získají `ActivityResultLauncher`. API chrání před ztrátou zpětného volání během znovuvytvoření Activity nebo ukončení procesu prostřednictvím striktní registrace vázané na životní cyklus: 1. Při spuštění externí Activity (jako je fotoaparát nebo výběr dokumentů) může být volající Activity zničena operačním systémem kvůli nedostatku paměti nebo změnám konfigurace. 2. Vyžadováním bezpodmínečného volání `registerForActivityResult()` předtím, než Activity nebo Fragment dosáhne stavu `STARTED` (obvykle jako inicializátor vlastnosti nebo uvnitř `onCreate`), je zpětné volání pro výsledek zaregistrováno do `ActivityResultRegistry` ještě před obnovením stavu. 3. Když se spuštěná aktivita vrátí a hostitelská aktivita je znovu vytvořena, `ActivityResultRegistry` spáruje čekající výsledek ze systému s nově zaregistrovaným zpětným voláním a výsledek bezpečně doručí.

class ProfileActivity : AppCompatActivity() {
    // Registered unconditionally during initialization / before onStart()
    private val takePictureLauncher = registerForActivityResult(
        ActivityResultContracts.TakePicturePreview()
    ) { bitmap: Bitmap? ->
        bitmap?.let {
            findViewById<ImageView>(R.id.avatarImage).setImageBitmap(it)
        }
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_profile)

        findViewById<Button>(R.id.btnCapture).setOnClickListener {
            takePictureLauncher.launch(null)
        }
    }
}
Zkusit odpovědět na tuto otázku s trenérem AI

13Porovnejte architektury MVVM (Model-View-ViewModel) a MVI (Model-View-Intent) pro komplexní obrazovku: jak reduktory stavu (state reducers) a odesílání záměrů (intent dispatching) v MVI zaručují atomické aktualizace stavu ve srovnání s MVVM?

V tradiční architektuře MVVM ViewModel často vystavuje několik nezávislých reaktivních proudů (například `isLoading`, `userData`, `errorMessage` jako samostatné vlastnosti `StateFlow` nebo `LiveData`) nebo několik veřejných aktualizačních metod. Když se současně dokončí více asynchronních úloh, mohou se tyto proudy aktualizovat nezávisle na sobě, což vede k souběhům (race conditions), neúplným mezistavům nebo vizuálnímu problikávání. V MVI (Model-View-Intent) se správa stavu řídí striktním jednosměrným tokem dat (Unidirectional Data Flow – UDF), který je postaven na třech prvcích: 1. Jediný neměnný (immutable) `UiState` reprezentující kompletní stav obrazovky. 2. Diskrétní typy `UiIntent` (nebo akce) reprezentující veškeré uživatelské interakce a systémové události. 3. Čistá funkce reduktoru stavu (state reducer): `(PreviousState, UiIntent) -> NewState`. MVI zaručuje atomické aktualizace stavu tím, že všechny události jsou odesílány jako samostatné záměry a sekvenčně směrovány přes reduktor stavu. Reduktor vezme neměnný snímek stávajícího stavu a vytvoří zcela nový snímek stavu, ve kterém jsou všechny související vlastnosti aktualizovány současně. Protože jsou aktualizace stavu centralizované a přechody sekvenční, uživatelské rozhraní nikdy nezachytí částečný, nekonzistentní nebo protichůdný snímek stavu.

data class ScreenState(
    val isLoading: Boolean = false,
    val items: List<Item> = emptyList(),
    val error: String? = null
)

sealed interface ScreenIntent {
    data object Refresh : ScreenIntent
    data class DataLoaded(val items: List<Item>) : ScreenIntent
    data class LoadFailed(val message: String) : ScreenIntent
}

fun reduce(state: ScreenState, intent: ScreenIntent): ScreenState = when (intent) {
    is ScreenIntent.Refresh -> state.copy(isLoading = true, error = null)
    is ScreenIntent.DataLoaded -> state.copy(isLoading = false, items = intent.items, error = null)
    is ScreenIntent.LoadFailed -> state.copy(isLoading = false, error = intent.message)
}
Zkusit odpovědět na tuto otázku s trenérem AI

14Jak byste navrhli proces udělování runtime oprávnění, který obsluhuje první žádost, UI (User Interface) s vysvětlením důvodu, odmítnutí, volbu „znovu se neptat“ a záložní přesměrování do nastavení, aniž by na uživatele vyvíjel nátlak?

Uživatelsky přívětivý proces udělování oprávnění za běhu aplikace, který na uživatele nevyvíjí nátlak, využívá postupné odkrývání (progressive disclosure), srozumitelné odůvodnění a elegantní degradaci funkčnosti (graceful degradation) napříč všemi stavy odmítnutí: 1. **Kontextová žádost (poprvé)**: Oprávnění žádejte až ve chvíli skutečné potřeby, kdy uživatel interaguje s danou funkcí (např. klepnutí na „Skenovat kód“ pro oprávnění k fotoaparátu), nikoli hned při spuštění aplikace. 2. **Uživatelské rozhraní s odůvodněním (Rationale UI)**: Pokud `shouldShowRequestPermissionRationale()` vrátí hodnotu `true`, zobrazte před vyvoláním systémového dialogu srozumitelné vysvětlení přímo v aplikaci (např. spodní list / bottom sheet nebo dialogové okno), které objasní, proč je oprávnění potřeba a jaký přínos přináší. 3. **Elegantní degradace při odmítnutí**: Pokud uživatel žádost odmítne, respektujte jeho volbu a neblokujte nesouvisející funkce. Kde je to možné, nabídněte alternativní postup (např. ruční zadání textu, pokud byl zamítnut přístup k fotoaparátu). 4. **Trvalé odmítnutí („Znovu se neptat“) a přesměrování do nastavení**: Pokud je oprávnění zamítnuto a `shouldShowRequestPermissionRationale()` vrátí `false` (uživatel zvolil možnost „Znovu se neptat“ nebo oprávnění opakovaně odmítl), informujte uživatele, proč je funkce nedostupná, a nabídněte volitelné tlačítko přesměrovávající do systémového nastavení aplikace pomocí `Settings.ACTION_APPLICATION_DETAILS_SETTINGS`. Zajistěte, aby uživatel mohl obrazovku snadno opustit nebo přejít zpět, aniž by v aplikaci uvízl.

fun onScanButtonClicked() {
    when {
        ContextCompat.checkSelfPermission(context, Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED -> {
            openScanner()
        }
        activity.shouldShowRequestPermissionRationale(Manifest.permission.CAMERA) -> {
            showRationaleDialog(onConfirm = { cameraPermissionLauncher.launch(Manifest.permission.CAMERA) })
        }
        else -> {
            cameraPermissionLauncher.launch(Manifest.permission.CAMERA)
        }
    }
}

fun handlePermissionResult(isGranted: Boolean) {
    if (isGranted) {
        openScanner()
    } else if (!activity.shouldShowRequestPermissionRationale(Manifest.permission.CAMERA)) {
        showSettingsRedirectDialog()
    } else {
        showManualEntryFallback()
    }
}
Zkusit odpovědět na tuto otázku s trenérem AI

15Jak byste navrhli Android CI/CD (Continuous Integration/Continuous Delivery) pipeline od validace pull requestů přes vytvoření podepsaného kandidáta na vydání (release candidate), interní testování, nahrání do testovací větve na Google Play až po postupné nasazení do produkce?

Produkční Android CI/CD pipeline začíná ve fázi validace pull requestů rychlými automatizovanými kontrolami: statickou analýzou (ktlint, detekt, Android Lint), jednotkovými testy (unit tests) a kompilací debug sestavení. Po sloučení do hlavní (`main`) nebo vydávací (`release`) větve se spustí pipeline pro sestavení vydání, která vygeneruje podepsaný Android App Bundle (`.aab`) ve formě Release Candidate (RC). K tomu využívá přihlašovací údaje k produkčnímu keystore spravované v CI, zmenšování kódu pomocí ProGuard/R8 a automatické navýšení čísla verze. Podepsaný soubor AAB a příslušné mapovací soubory pro deobfuskaci se nahrají do interní testovací větve (Internal Testing track) v Google Play (nebo do Firebase App Distribution) pomocí nástrojů jako Gradle Play Publisher (GPP) nebo Fastlane, kde proběhnou automatizované kouřové (smoke) či instrumentační testy. Jakmile interní tým QA a zúčastněné strany sestavení schválí, tentýž binární artefakt je bez rekompilace ze zdrojových kódů postupně povýšen do dalších fází (Internal -> Closed Alpha/Beta -> Production Rollout). Bezpečné nasazení nakonec zajistí postupné uvolňování (staged rollout, např. 5 % -> 20 % -> 100 %) spojené s automatickým sledováním chybovosti (crash rate) a metrik Android Vitals.

lane :promote_internal_to_beta do
  upload_to_play_store(
    track: 'internal',
    track_promote_to: 'beta',
    skip_upload_aab: true, # Promotes existing binary without rebuilding
    skip_upload_metadata: false,
    skip_upload_changelogs: false
  )
end
Zkusit odpovědět na tuto otázku s trenérem AI

16Proč zachycení obecné výjimky Exception nebo Throwable bez opětovného vyhození CancellationException naruší rušení korutin a jak se problémy s rušením diagnostikují?

Kotlin Coroutines spoléhají na kooperativní rušení (cooperative cancellation) implementované pomocí výjimky CancellationException. Když je `Job` korutiny zrušen, pozastavující funkce (jako `delay` nebo `yield`) vyhodí CancellationException, aby došlo k odvinutí zásobníku volání a ukončení korutiny. Pokud kód zachytí obecnou Exception nebo Throwable bez opětovného vyhození (rethrow) CancellationException, signál k ukončení se potlačí. Korutina se nepřeruší a pokračuje v běhu, čímž vzniká „zombie korutina“, která plýtvá procesorovým časem a pamětí, způsobuje úniky prostředků a může vyvolat neplatné stavové přechody nebo pády při interakci s již zaniklými komponentami uživatelského rozhraní. Diagnostika problémů s rušením: 1. Ověřte správnost ošetření výjimek tím, že zajistíte explicitní opětovné vyhazování CancellationException nebo zachytávání pouze specifických doménových výjimek namísto obecných Throwable/Exception. 2. Zkontrolujte stavy korutin v ladicích sestaveních pomocí Kotlin Coroutines Debuggeru (`-Dkotlinx.coroutines.debug`) nebo nástroje Coroutines Inspector v Android Studiu a identifikujte běžící korutiny, které již měly být ukončeny. 3. Zaznamenávejte stavy životního cyklu korutiny do logu (např. `job.isActive`, `job.isCancelled`) nebo použijte strukturované logování v handlerech dokončení rušení (`job.invokeOnCompletion`).

import kotlinx.coroutines.CancellationException

// ANTI-PATTERN: Swallows cancellation, creating a zombie coroutine
try {
    doSuspendingWork()
} catch (e: Exception) {
    logError(e)
}

// CORRECT: Preserves cancellation propagation
try {
    doSuspendingWork()
} catch (e: Exception) {
    if (e is CancellationException) throw e
    logError(e)
}
Zkusit odpovědět na tuto otázku s trenérem AI

17Jak funguje vynášení stavu (state hoisting) a jednosměrný tok dat (UDF – Unidirectional Data Flow) v Jetpack Compose a podle čeho se rozhodujete, zda stav patří do listové komponenty (leaf composable), rodičovské komponenty nebo do ViewModelu?

Vynášení stavu (state hoisting) je návrhový vzor v Jetpack Compose, při kterém se stav přesouvá nahoru v hierarchii kompozice, aby byla komponenta bezstavová (stateless) a stala se čistou komponentou uživatelského rozhraní. Tento přístup umožňuje jednosměrný tok dat (UDF – Unidirectional Data Flow), kdy stav putuje dolů (z rodiče/ViewModelu do potomků jako argumenty) a události putují nahoru (z potomků do rodiče/ViewModelu jako zpětná volání (callbacks)). Rozhodování o umístění stavu se řídí těmito pravidly: 1. **Listová komponenta (lokální stav UI):** Pokud je stav čistě přechodný, vizuální a nepotřebuje ho žádný rodič ani sourozenecká komponenta (např. zda právě běží interní animace rozbalení/sbalení nebo lokální efekt zvlnění – ripple), ponechte jej lokálně v listové komponentě pomocí `remember`. 2. **Rodičovská komponenta (vynesený stav UI):** Pokud sourozenecké komponenty potřebují stav sdílet nebo na něj reagovat, případně pokud rodič řídí viditelnost či validaci komponenty, vyneste stav do bezprostředně nadřazeného společného rodiče. Listová komponenta se tím stane bezstavovou (přijímá `value` a `onValueChange`). 3. **ViewModel (stav obrazovky / byznys stav):** Pokud stav reprezentuje byznysová data, musí přežít změny konfigurace, řídí navigaci nebo vyžaduje interakci s doménovou/repositářovou vrstvou, patří do ViewModelu a je vystaven jako pozorovatelný stav (např. `StateFlow`). ViewModel zpracovává byznys logiku a podle toho aktualizuje stav UI.

// Stateless Leaf Composable
@Composable
fun SearchBar(query: String, onQueryChange: (String) -> Unit) {
    TextField(value = query, onValueChange = onQueryChange)
}

// Stateful Screen / ViewModel Owner
@Composable
fun SearchScreen(viewModel: SearchViewModel = viewModel()) {
    val uiState by viewModel.uiState.collectAsStateWithLifecycle()
    SearchBar(
        query = uiState.query,
        onQueryChange = { newQuery -> viewModel.onQueryChanged(newQuery) }
    )
}
Zkusit odpovědět na tuto otázku s trenérem AI

Otázky pro zkušené

18Jak byste navrhli telemetrii, záznamy stop před pádem (crash breadcrumbs) a architektonické mantinely pro detekci, diagnostiku a prevenci ztráty stavu FragmentManageru a souběhů při navigaci v produkčním prostředí?

Ztráta stavu FragmentManageru (`IllegalStateException: Can not perform this action after onSaveInstanceState`) a asynchronní souběhy při navigaci nastávají, když se asynchronní operace – například síťová zpětná volání nebo reaktivní streamy – pokusí provést transakce UI poté, co životní cyklus hostitele přešel za `onSaveInstanceState()` nebo `onStop()`. Pro jejich detekci a diagnostiku v produkci implementujeme telemetrii životního cyklu a stopy (breadcrumbs) prostřednictvím `Application.ActivityLifecycleCallbacks` a `FragmentManager.FragmentLifecycleCallbacks`, které zaznamenávají přechody s časovými razítky, počet čekajících položek v backstacku a kontext spuštění před pádem. Pro architektonickou prevenci ztráty stavu musí být navigace a transakce UI řízeny výhradně pozorovateli stavu, kteří berou v úvahu životní cyklus (např. `repeatOnLifecycle(Lifecycle.State.RESUMED)` nebo `StateFlow` sbíraný s vazbou na životní cyklus), nikoli surovými asynchronními zpětnými voláními. Navigační události by měly být modelovány jako diskrétní stavové přechody jednosměrného toku dat (UDF – Unidirectional Data Flow) nebo jednorázové události konzumované pouze tehdy, když je stav životního cyklu alespoň `STARTED` nebo `RESUMED`. Architektonické mantinely by dále měly vynucovat bezpečnost povolením `commitStateLoss()` pouze v explicitních neobnovitelných přechodných kontextech, nebo ideálně migrací na komponentu Jetpack Navigation se striktními vazbami na životní cyklus. Statická analýza pomocí vlastních pravidel Android Lint a kontrola za běhu v debug sestaveních (např. Fragment StrictMode) pomáhají zachytit porušení pravidel ještě před nasazením do produkce.

class NavigationDispatcher @Inject constructor() {
    private val _events = Channel<NavigationCommand>(Channel.BUFFERED)
    val events: Flow<NavigationCommand> = _events.receiveAsFlow()

    fun navigate(command: NavigationCommand) {
        _events.trySend(command)
    }
}

// In Fragment / Activity
viewLifecycleOwner.lifecycleScope.launch {
    viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.RESUMED) {
        navigationDispatcher.events.collect { command ->
            CrashReporting.leaveBreadcrumb("Navigating to ${command.destination} at state ${lifecycle.currentState}")
            command.execute(parentFragmentManager)
        }
    }
}
Zkusit odpovědět na tuto otázku s trenérem AI

19Jak funkce správy napájení na úrovni systému, jako jsou Doze Mode, App Standby Buckets a nástroje OEM (Original Equipment Manufacturer) pro ukončování úloh, ovlivňují plánování procesů na pozadí a jak byste navrhli odolný synchronizační mechanismus pomocí knihovny WorkManager?

Optimalizace baterie na úrovni systému výrazně omezují běh procesů na pozadí. Režim Doze Mode omezuje využití procesoru, přístup k síti a úlohy na pozadí v obdobích nečinnosti a povoluje jejich spuštění pouze během periodických oken údržby. App Standby Buckets dynamicky omezují frekvenci úloh na pozadí a přístup k síti na základě toho, jak nedávno byla aplikace použita (od stavu Active až po Restricted nebo Never). Agresivní vlastní správci napájení od výrobců OEM (například v MIUI nebo OneUI) navíc často ukončují procesy na pozadí, ignorují standardní budíky a odebírají oprávnění k automatickému spuštění bez ohledu na standardní chování AOSP. Jako standardní základ pro vytvoření odolného synchronizačního jádra slouží WorkManager, protože abstrahuje JobScheduler, AlarmManager i BroadcastReceivers a zároveň se integruje přímo s omezeními operačního systému. WorkManager umožňuje deklarovat striktní podmínky pro spuštění (např. NetworkType.CONNECTED, requiresBatteryNotLow(true)) a automaticky odkládá spuštění na okna údržby režimu Doze nebo do obnovení konektivity. Aby synchronizační mechanismus odolal neočekávanému ukončení procesu, výpadkům sítě a zásahům ze strany OEM, musí dodržovat dva klíčové principy: exponenciální odklad (exponential backoff) a end-to-end idempotenci. WorkManager by měl být konfigurován s BackoffPolicy.EXPONENTIAL, aby se po probuzení z režimu Doze předešlo přetížení backendových serverů (problém thundering herd). Pracovní vlákna (workers) navíc musí se synchronizačními operacemi nakládat jako s atomickými a idempotentními — pomocí deterministických ID transakcí, příznaků lokálního stavu a deduplikačních klíčů na straně serveru —, takže pokud je worker uprostřed práce náhle ukončen a znovu zařazen do fronty, opakovaný pokus nezpůsobí duplikaci dat ani nepoškodí lokální databázi.

val syncConstraints = Constraints.Builder()
    .setRequiredNetworkType(NetworkType.CONNECTED)
    .setRequiresBatteryNotLow(true)
    .build()

val syncWorkRequest = OneTimeWorkRequestBuilder<SyncWorker>()
    .setConstraints(syncConstraints)
    .setBackoffCriteria(
        BackoffPolicy.EXPONENTIAL,
        WorkRequest.MIN_BACKOFF_MILLIS,
        TimeUnit.MILLISECONDS
    )
    .addTag("data_sync_tag")
    .build()

WorkManager.getInstance(context).enqueueUniqueWork(
    "unique_data_sync",
    ExistingWorkPolicy.KEEP,
    syncWorkRequest
)
Zkusit odpovědět na tuto otázku s trenérem AI

20Jak navrhnete škálovatelnou multi-týmovou architekturu pro Android s využitím vzoru oddělení modulů „API (Application Programming Interface) – implementace“, abyste optimalizovali dobu sestavení v Gradle a vynutili striktní hranice kontraktů?

V rozsáhlém multi-týmovém Android projektu rozděluje vzor API-Implementation (API-Impl) každý modul funkcionality nebo domény na dva samostatné Gradle subprojekty: odlehčený modul `:feature:api` obsahující veřejná rozhraní, modely a navigační kontrakty, a modul `:feature:impl` obsahující interní byznys logiku, UI a implementace repozitářů. Konzumentské moduly závisí výhradně na `:feature:api` pomocí `implementation project(':feature:api')`, zatímco kořenový modul `:app` nebo vyhrazené kořeny kompozice propojují konkrétní implementace dohromady pomocí Dependency Injection (např. Dagger/Hilt). Tento vzor dramaticky optimalizuje výkon sestavení v Gradle díky stabilitě aplikačního binárního rozhraní (ABI – Application Binary Interface) a izolaci kompilační classpath. Když vývojáři upraví detaily implementace v `:feature:impl`, veřejné ABI modulu `:feature:api` zůstává nezměněno. V důsledku toho Gradle přeskočí rekompilaci všech navazujících modulů, které závisejí pouze na `:feature:api`, což maximalizuje efektivitu Gradle Remote Build Cache a konfigurační mezipaměti (configuration cache). Z organizačního hlediska a správy kódu tento vzor vytváří jasné hranice vlastnictví napříč týmy pomocí nástrojů jako CODEOWNERS. Týmy mohou bezpečně vyvíjet své interní implementační detaily, aniž by vystavovaly privátní třídy, což zabraňuje nežádoucímu těsnému provázání a cyklickým závislostem ve velkých týmech.

// :feature:checkout:api/src/main/java/com/example/checkout/api/CheckoutApi.kt
package com.example.checkout.api

interface CheckoutLauncher {
    fun launchCheckoutFlow(cartId: String)
}

// :feature:checkout:impl/src/main/java/com/example/checkout/impl/CheckoutLauncherImpl.kt
package com.example.checkout.impl

import com.example.checkout.api.CheckoutLauncher
import javax.inject.Inject

internal class CheckoutLauncherImpl @Inject constructor(
    private val paymentGateway: PaymentGateway
) : CheckoutLauncher {
    override fun launchCheckoutFlow(cartId: String) {
        // Concrete checkout implementation
    }
}

// :feature:checkout:impl/src/main/java/com/example/checkout/impl/di/CheckoutModule.kt
package com.example.checkout.impl.di

import com.example.checkout.api.CheckoutLauncher
import com.example.checkout.impl.CheckoutLauncherImpl
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent

@Module
@InstallIn(SingletonComponent::class)
internal abstract class CheckoutBindingModule {
    @Binds
    abstract fun bindCheckoutLauncher(impl: CheckoutLauncherImpl): CheckoutLauncher
}
Zkusit odpovědět na tuto otázku s trenérem AI