Beginner Fundamentals
What is the difference between val and var in Kotlin?
val declares a read-only reference — it can be assigned once, at declaration or in an init block. var declares a mutable reference that can be reassigned later. Note that val only locks the reference, not the object it points to — a val list = mutableListOf(1) can still have items added to it.
What is the Activity lifecycle?
The set of callbacks Android calls as an Activity moves through states: onCreate (first creation) → onStart (becoming visible) → onResume (in the foreground) → onPause (partially obscured) → onStop (no longer visible) → onDestroy. onRestart fires when a stopped Activity becomes visible again, right before onStart.
What is the difference between an Activity and a Fragment?
An Activity is a single screen with its own window and entry in the system's task stack. A Fragment is a reusable portion of UI with its own lifecycle, hosted inside an Activity — used historically for multi-pane layouts. In modern Compose-first apps, fragments are largely replaced by composables and Navigation Compose.
What does @Composable mean in Jetpack Compose?
It marks a function as one that describes UI and can participate in Compose's recomposition system — the function may be re-invoked automatically whenever the state it reads changes, so Compose knows what to redraw.
What is the difference between LinearLayout and ConstraintLayout in the XML View system?
LinearLayout arranges children in a single row or column, nesting required for complex UIs. ConstraintLayout positions each child relative to other views or the parent using constraints, letting you build flat, complex layouts without deep nesting — better for performance and maintainability.
What is a null-safety operator like ?. or ?: used for?
?. (safe call) evaluates to null instead of throwing if the receiver is null, e.g. name?.length. ?: (Elvis operator) supplies a default when the left side is null, e.g. name?.length ?: 0. Together they let you handle nullable values without explicit if-null checks everywhere.
Intermediate Architecture & State
Why use a ViewModel instead of storing state directly in an Activity?
A ViewModel is scoped to its owner (Activity, Fragment, or nav graph) and survives configuration changes like screen rotation, so UI state isn't lost when the Activity is recreated. It also keeps UI logic separate from lifecycle-bound UI code, making it independently testable.
What is state hoisting in Jetpack Compose?
Moving state out of a composable and up to its caller, so the composable receives the current value and an event callback rather than owning the state itself. This makes composables stateless, reusable, and easy to preview or test with different data sources.
What is the difference between LiveData and StateFlow?
Both are observable holders of a current value. LiveData is lifecycle-aware out of the box and designed for the View system. StateFlow is part of Kotlin coroutines, works anywhere coroutines do (not just Android), always has an initial value, and integrates naturally with Compose via collectAsState(). Most new Compose code prefers StateFlow.
What does remember do, and how is it different from rememberSaveable?
remember caches a value across recompositions but loses it if the Activity is recreated (e.g. rotation) or the process dies. rememberSaveable additionally saves the value into the instance state bundle, so it survives configuration changes and process death, as long as the type is savable (primitives, Parcelable, etc.).
What is the purpose of a key parameter in LazyColumn's items()?
It gives Compose a stable identity for each item so it can correctly track insertions, removals, and reorders across recompositions — without it, Compose falls back to position-based identity, which can cause incorrect animations or lost item state when the list changes.
What is the difference between launch and async in coroutines?
launch starts a coroutine that returns a Job and doesn't produce a result — used for fire-and-forget work. async starts a coroutine that returns a Deferred<T>, whose result you retrieve with await() — used when you need a computed value, often to run multiple operations concurrently.
val a = async { fetchA() }
val b = async { fetchB() }
val combined = a.await() + b.await() // both run concurrently
What does Room do at compile time that raw SQLite doesn't give you?
Room validates your @Query SQL against your entity schema at compile time, so a typo'd column name or mismatched return type fails the build instead of crashing at runtime. It also generates the boilerplate for cursor mapping and integrates with Flow/LiveData for reactive queries.
Advanced Deep Dives
How does Compose decide what to recompose, and how do you avoid unnecessary recompositions?
Compose tracks which composables read which State objects during composition and only re-invokes the composables that actually read a value that changed ("smart recomposition"), skipping the rest of the tree. To avoid unnecessary work: keep state as close as possible to where it's used (avoid hoisting further than needed), use stable/immutable data classes so Compose can skip recomposition when nothing meaningfully changed, and use derivedStateOf for values computed from other state so they only trigger recomposition when the derived result actually changes.
What is SharingStarted.WhileSubscribed(timeout) used for with stateIn?
It controls when a hot StateFlow created from a cold Flow (e.g. a Room query) starts and stops collecting the upstream. WhileSubscribed(5000) keeps the upstream active for 5 seconds after the last collector unsubscribes — long enough to survive a quick configuration change like rotation without restarting the query, but short enough to release resources when the screen is truly gone.
How does Hilt's dependency graph work, and what does @Singleton actually scope to?
Hilt generates a hierarchy of components mirroring Android's lifecycle owners (Application, Activity, Fragment, ViewModel, etc.). @Singleton scopes a binding to the SingletonComponent, tied to the Application's lifecycle — one instance for the entire app process, created lazily on first injection and never destroyed until the process dies.
What's the difference between Dispatchers.IO and Dispatchers.Default?
Dispatchers.IO is backed by a large, elastic thread pool optimized for blocking I/O work (network calls, disk access, database queries) where threads spend most of their time waiting. Dispatchers.Default is backed by a thread pool sized to the number of CPU cores, optimized for CPU-intensive work like sorting large lists or parsing. Using the wrong one — e.g. CPU-heavy work on IO — can starve other I/O operations or waste threads.
How would you handle process death with unsaved user input in a form screen?
Use SavedStateHandle inside the ViewModel to persist critical form fields — it survives process death (unlike plain ViewModel state, which only survives configuration changes) by writing to the same Bundle the system already saves for instance state. Read/write through savedStateHandle.get/set or expose it as a StateFlow so the UI updates automatically when state is restored after the process is recreated.