Core Android Fundamentals
1. Activity Lifecycle
Q: Explain the Activity lifecycle and its callback methods.
The Activity lifecycle represents the states an Activity transitions through from creation to destruction. Proper lifecycle management is essential for resource handling and user experience.
| Callback | State | What to Do |
|---|---|---|
| onCreate() | Created | Initialize UI (setContentView), restore saved state, setup ViewModel |
| onStart() | Visible | Start animations, refresh data that may have changed |
| onResume() | Foreground | Register sensors/camera, resume video playback |
| onPause() | Partially visible | Pause video, save critical data quickly (keep this fast) |
| onStop() | Not visible | Release heavy resources, unregister receivers, save to database |
| onDestroy() | Destroyed | Final cleanup, cancel non-ViewModel coroutines |
Q: What triggers each callback?
- onStart/onStop: Visibility changes (another Activity covers or uncovers)
- onResume/onPause: Focus changes (dialog appears, split-screen)
- onDestroy: User presses back, calls
finish(), or system kills for memory
Q: What happens during configuration changes (rotation)?
The Activity is destroyed and recreated: onPause() → onStop() → onDestroy() → onCreate() → onStart() → onResume(). ViewModel survives this process. Use onSaveInstanceState() to preserve UI state (scroll position, form input).
2. Fragment Lifecycle
Q: How does Fragment lifecycle differ from Activity lifecycle?
Fragments have additional callbacks because they’re tied to a host Activity and have a separate view lifecycle:
| Callback | Purpose |
|---|---|
| onAttach() | Fragment attached to Activity—get context reference |
| onCreate() | Initialize non-UI components, parse arguments |
| onCreateView() | Inflate and return the fragment’s layout |
| onViewCreated() | Best place to setup UI, observers, click listeners |
| onDestroyView() | View destroyed—must null out binding to prevent leaks |
| onDetach() | Fragment detached from Activity |
Q: Why is viewLifecycleOwner important?
The Fragment instance can outlive its view (e.g., on back stack). Using viewLifecycleOwner for LiveData/Flow observation ensures observers are removed when the view is destroyed, preventing memory leaks and crashes from updating dead views.
Q: What’s the critical cleanup in onDestroyView()?
Always set _binding = null. The Fragment survives on the back stack, but holding view references prevents garbage collection of the entire view hierarchy.
3. Configuration Changes
Q: How do you handle configuration changes properly?
Three strategies:
| Strategy | Use Case | Survives Process Death? |
|---|---|---|
| ViewModel | Business data, network results | No (use SavedStateHandle) |
| onSaveInstanceState | UI state (scroll position, text input) | Yes |
| configChanges in manifest | Video players, games, camera apps | N/A |
Q: Why is configChanges in manifest discouraged?
It requires manual handling of all layout adjustments, breaks automatic resource selection (different layouts for orientations), and makes code harder to maintain. Use only for special cases like video playback.
Q: What’s the size limit for onSaveInstanceState?
About 500KB. Use it only for small, essential UI state. For large data, use ViewModel with SavedStateHandle.
4. Android Components
Q: What are the four main Android components?
| Component | Purpose | Has UI | Entry Point |
|---|---|---|---|
| Activity | Single screen with UI | Yes | Intent |
| Service | Background operations | No | Intent/Binding |
| BroadcastReceiver | Respond to system/app events | No | Intent broadcast |
| ContentProvider | Share data between apps | No | Content URI |
Q: Explain the three types of Services.
-
Foreground Service: Shows persistent notification, continues when app is backgrounded (music player, fitness tracking). Required for long-running user-visible tasks.
-
Background Service: No notification, heavily restricted since Android 8.0. Use WorkManager instead for most background work.
-
Bound Service: Provides client-server interface via
onBind(). Lives only while clients are bound to it.
Q: What are Service return values?
- START_STICKY: Restart with null intent if killed—for ongoing tasks
- START_NOT_STICKY: Don’t restart—for tasks that can wait
- START_REDELIVER_INTENT: Restart with last intent—for tasks that must complete
Q: When would you use a ContentProvider?
When sharing structured data with other apps (like Contacts or Calendar). Also useful for accessing data via URIs with CursorLoaders or for integrating with system features like search suggestions.
5. Context
Q: What’s the difference between Application Context and Activity Context?
| Aspect | Application Context | Activity Context |
|---|---|---|
| Lifecycle | Lives with app | Dies with Activity |
| UI Operations | Cannot show dialogs, inflate themed views | Full UI support |
| Memory Safety | Safe in singletons | Can cause leaks if held long-term |
Q: When to use each?
Application Context:
- Singletons (database, analytics, image loader)
- Starting services
- Registering long-lived receivers
- Anything that outlives an Activity
Activity Context:
- Showing dialogs (requires Activity window)
- Inflating views with correct theme
- Starting Activities with proper task stack
- Creating themed UI components
Q: How do memory leaks happen with Context?
Storing Activity Context in a static/singleton object prevents the Activity from being garbage collected. The entire Activity hierarchy stays in memory. Always convert to applicationContext when storing in long-lived objects.
6. Intents
Q: What’s the difference between explicit and implicit Intents?
Explicit Intent: Specifies the exact component to start by class name. Used for internal app navigation.
Implicit Intent: Declares an action and lets the system find matching components. Used for cross-app functionality (share, view URL, send email).
Q: What are Intent flags and when do you use them?
| Flag | Effect |
|---|---|
| FLAG_ACTIVITY_NEW_TASK | Start in new task (required from non-Activity context) |
| FLAG_ACTIVITY_CLEAR_TOP | Pop activities above target, bring target to front |
| FLAG_ACTIVITY_SINGLE_TOP | Don’t create new instance if already at top |
| FLAG_ACTIVITY_CLEAR_TASK | Clear entire task before starting |
Q: What’s a PendingIntent?
A token that grants another app permission to execute an Intent on your behalf—used for notifications, widgets, and alarms. Specify mutability (MUTABLE/IMMUTABLE) for security on Android 12+.
7. Launch Modes
Q: Explain Activity launch modes.
| Mode | Behavior | Use Case |
|---|---|---|
| standard | New instance every time | Most activities |
| singleTop | Reuse if already at top (onNewIntent) | Search results, notifications |
| singleTask | One instance per task, clears above | Main/Home screen |
| singleInstance | Alone in its own task | Launcher, call screen |
Q: What’s onNewIntent() and when is it called?
Called instead of creating a new instance when using singleTop (and Activity is at top) or singleTask (and Activity exists in task). Extract new extras here instead of onCreate().
8. Process and App Lifecycle
Q: How does Android prioritize processes for killing?
From least to most likely to be killed:
- Foreground (visible Activity, foreground Service)
- Visible (partially obscured Activity)
- Service (background Service running)
- Cached (background Activities, stopped)
- Empty (no active components)
Q: How do you handle process death?
- ViewModel data is lost—use
SavedStateHandlefor critical state onSaveInstanceState()is called before process death- Test with “Don’t keep activities” developer option
- Activities are recreated with saved Bundle when user returns
Quick Reference
| Topic | Key Points |
|---|---|
| Activity Lifecycle | onCreate (init) → onStart (visible) → onResume (interactive) → reverse for shutdown |
| Fragment Lifecycle | Additional onCreateView/onDestroyView; null binding in onDestroyView |
| Config Changes | ViewModel survives; onSaveInstanceState for UI state; avoid configChanges |
| Components | Activity (UI), Service (background), BroadcastReceiver (events), ContentProvider (data sharing) |
| Context | Application for singletons; Activity for UI; never store Activity in long-lived objects |
| Launch Modes | standard (new), singleTop (reuse at top), singleTask (one per task), singleInstance (own task) |
Modern Architecture Patterns
1. MVVM Architecture
Q: Explain MVVM architecture and why we use it in Android.
MVVM (Model-View-ViewModel) separates an application into three layers:
| Layer | Responsibility | Android Components |
|---|---|---|
| Model | Data and business logic | Repository, Data Sources, Room, Retrofit |
| View | Display UI, capture user input | Activity, Fragment, Composable |
| ViewModel | Hold UI state, expose data to View | Jetpack ViewModel |
Q: Why is MVVM the recommended pattern?
- Separation of Concerns: Each layer has a single responsibility
- Testability: ViewModel can be unit tested without Android framework
- Lifecycle Awareness: ViewModel survives configuration changes
- Maintainability: Changes in one layer don’t cascade to others
Q: What are common MVVM mistakes?
- Passing Activity/Fragment to ViewModel: Causes memory leaks
- Holding View references in ViewModel: Same leak issue
- Business logic in View: Should be in ViewModel or Use Cases
- ViewModel importing android. packages*: Breaks testability (lifecycle components excepted)
Q: What’s the Single UI State pattern?
Instead of multiple LiveData/StateFlow for different UI elements, use one sealed class representing all possible screen states. This prevents impossible state combinations and makes reasoning about UI easier.
2. MVI Architecture
Q: Explain MVI and when to use it over MVVM.
MVI (Model-View-Intent) enforces unidirectional data flow:
- Intent: User actions or events (ButtonClicked, TextChanged)
- Model: Complete, immutable UI state at any moment
- View: Renders state, emits intents
The flow is circular: User Action → Intent → Reducer → New State → View Update
Q: MVVM vs MVI comparison?
| Aspect | MVVM | MVI |
|---|---|---|
| State | Multiple StateFlows | Single ViewState |
| Complexity | Lower | Higher boilerplate |
| Predictability | Good | Excellent (single source of truth) |
| Debugging | Moderate | Easy (log all state changes) |
| Best For | Simple-medium apps | Complex interactions, forms |
Q: When to use MVI?
- Complex multi-step workflows
- Forms with interdependent validation
- Need to track/debug all state changes
- Time-travel debugging requirements
- State consistency is critical (financial, healthcare apps)
3. Google’s Recommended Architecture
Q: Describe the layered architecture Google recommends.
Three layers with clear dependencies (outer layers depend on inner):
UI Layer (Presentation)
- UI elements: Activities, Fragments, Composables
- State holders: ViewModels
- Observes state, forwards events
Domain Layer (Optional)
- Use Cases / Interactors
- Contains reusable business logic
- Combines data from multiple repositories
Data Layer
- Repositories: Single source of truth for each data type
- Data Sources: Remote (API), Local (Room), Cache
Q: What are the key principles?
- Separation of Concerns: UI shouldn’t know about data sources
- Drive UI from Data Models: UI reflects state, doesn’t own it
- Single Source of Truth (SSOT): One owner per data type
- Unidirectional Data Flow: State flows down, events flow up
Q: When is the Domain layer needed?
- Business logic reused across multiple ViewModels
- Complex data transformations or combinations
- Need to enforce business rules consistently
- Large teams where clear boundaries help
4. Repository Pattern
Q: What is the Repository pattern and why use it?
Repository is an abstraction over data sources. It:
- Decides whether to fetch from network or cache
- Handles caching strategies
- Transforms DTOs to domain models
- Provides a clean API to the rest of the app
Q: What caching strategies are common?
| Strategy | Behavior | Use Case |
|---|---|---|
| Cache First | Return cache, then refresh | News feeds, social content |
| Network First | Try network, fallback to cache | Critical data, payments |
| Cache Only | Never hit network | Offline-first apps |
| Network Only | Never cache | Sensitive data |
Q: How do you expose data from Repository?
Use Flow for observable data streams. The Room DAO returns Flow, Repository exposes it, ViewModel collects it. Changes propagate automatically from database to UI.
5. Clean Architecture
Q: How does Clean Architecture relate to Android?
Clean Architecture organizes code in concentric circles:
- Entities (innermost): Business objects, pure Kotlin
- Use Cases: Application-specific business rules
- Interface Adapters: ViewModels, Repositories, Presenters
- Frameworks (outermost): Android, Room, Retrofit
The Dependency Rule: Dependencies point inward. Inner layers know nothing about outer layers.
Q: What are the benefits?
- Business logic is completely isolated from frameworks
- Easy to test core logic without Android dependencies
- Can swap frameworks (e.g., Retrofit → Ktor) without touching business logic
- Forces clear separation and single responsibility
Q: What are the drawbacks?
- More boilerplate (mappers, interfaces, multiple models)
- Can be overkill for simple apps
- Learning curve for the team
- Need discipline to maintain boundaries
6. State Management
Q: LiveData vs StateFlow vs SharedFlow?
| Type | Hot/Cold | Lifecycle Aware | Initial Value | Replay |
|---|---|---|---|---|
| LiveData | Hot | Yes (automatic) | Optional | Latest |
| StateFlow | Hot | No (need repeatOnLifecycle) | Required | Latest |
| SharedFlow | Hot | No | Not required | Configurable |
Q: When to use each?
- LiveData: Simple cases, team familiar with it, automatic lifecycle handling
- StateFlow: UI state, need
.valueaccess, Compose integration - SharedFlow: One-time events (navigation, toasts), multiple subscribers
Q: How do you handle one-time events in MVVM?
Options:
- Channel + receiveAsFlow(): Consumed exactly once
- SharedFlow with replay=0: Events don’t replay to new subscribers
- Event wrapper class: Wrap value, mark as consumed
Avoid using StateFlow for events—new subscribers receive the last event again.
7. Dependency Injection in Architecture
Q: How does DI fit into the architecture?
DI provides dependencies to each layer:
- ViewModel receives Repository and Use Cases
- Repository receives DataSources
- DataSources receive Retrofit, Room, etc.
This enables:
- Swapping implementations (real vs fake for testing)
- Scoping (singleton, per-screen, per-ViewModel)
- Lazy initialization
Q: What scopes are common in Android DI?
| Scope | Lifecycle | Example |
|---|---|---|
| Singleton | App lifetime | Retrofit, OkHttp, Room Database |
| Activity/Fragment | Screen lifetime | Screen-specific analytics |
| ViewModel | ViewModel lifetime | Use Cases with state |
Quick Reference
| Pattern | Key Insight |
|---|---|
| MVVM | ViewModel holds UI state, survives config changes, never references View |
| MVI | Single immutable state, unidirectional flow, great for complex UIs |
| Layered Architecture | UI → Domain (optional) → Data; dependencies point inward |
| Repository | Abstracts data sources, handles caching, single source of truth |
| Clean Architecture | Business logic independent of frameworks; more structure, more boilerplate |
| State Management | StateFlow for state, Channel/SharedFlow for events, avoid LiveData for new code |
Jetpack Compose
1. Recomposition
Q: What is recomposition in Jetpack Compose?
Recomposition is when Compose re-executes composable functions to update the UI after state changes. Compose intelligently recomposes only the composables that read the changed state, not the entire tree.
Q: What triggers recomposition?
Any state read by a composable changing its value. This includes mutableStateOf, StateFlow.collectAsState(), or any State<T> object that the composable reads during execution.
Q: What are the key rules composables must follow?
- Idempotent: Same inputs produce same UI
- Side-effect free: No external mutations in the function body
- Independent of order: Don’t assume execution order
- Can be skipped: Compose may skip recomposition if inputs haven’t changed
Q: How does Compose decide what to skip?
Compose compares parameters using equals(). If all parameters are equal to the previous composition, it skips recomposition. For this to work, parameters must be stable—either primitives, immutable data classes, or annotated with @Stable/@Immutable.
2. Stability and Performance
Q: What makes a type “stable” in Compose?
A stable type guarantees that:
equals()always returns the same result for the same instances- If a public property changes, Compose is notified
- All public properties are also stable
Primitives, String, and immutable data classes with stable properties are stable by default.
Q: What makes a type “unstable”?
- Mutable collections (
MutableList,ArrayList) - Data classes with mutable properties
- Classes from external libraries Compose can’t verify
- Lambdas captured in composition
Q: How do you fix stability issues?
| Problem | Solution |
|---|---|
| Mutable collection | Use immutable List, Set, Map |
| External library class | Wrap in stable wrapper or use @Immutable |
| Lambda recreation | Hoist lambda or use remember |
| Unknown class | Annotate with @Stable if you guarantee stability |
Q: How do you debug recomposition issues?
- Use Layout Inspector’s “Show Recomposition Counts”
- Add
SideEffect { log("Recomposed") }temporarily - Check stability with Compose Compiler reports
- Profile with Android Studio Profiler
3. State Management
Q: Explain the different ways to hold state in Compose.
| API | Survives Recomposition | Survives Config Change | Survives Process Death |
|---|---|---|---|
remember | ✅ | ❌ | ❌ |
rememberSaveable | ✅ | ✅ | ✅ |
ViewModel | ✅ | ✅ | ❌ (use SavedStateHandle) |
Q: What is state hoisting?
Moving state up from a composable to its caller, making the composable stateless. The composable receives the state value and a callback to request changes. Benefits:
- Reusable composables
- Easier testing
- Single source of truth
- Parent controls the state
Q: When should state live in ViewModel vs Composable?
| ViewModel | Composable (remember) |
|---|---|
| Business logic state | Pure UI state |
| Data from repository | Animation state |
| Survives config change | Scroll position, expansion state |
| Shared across composables | Local to one composable |
4. Side Effects
Q: What are side effects and why do they need special handling?
Side effects are operations that escape the composable’s scope—network calls, database writes, navigation, analytics. Composables can recompose at any time, in any order, so side effects in the body would execute unpredictably.
Q: Explain the side effect APIs.
| API | Purpose | Lifecycle |
|---|---|---|
LaunchedEffect(key) | Launch coroutine | Cancelled when key changes or leaves composition |
DisposableEffect(key) | Setup/cleanup resources | Cleanup called when key changes or leaves composition |
SideEffect | Non-suspending effect on every successful recomposition | Runs after every recomposition |
rememberCoroutineScope() | Get scope for event handlers | Tied to composition lifecycle |
derivedStateOf | Compute value from other states, avoiding recomposition | Updates only when result changes |
produceState | Convert non-Compose state to Compose state | Runs coroutine to produce values |
Q: LaunchedEffect vs rememberCoroutineScope?
- LaunchedEffect: For effects that should run when entering composition or when a key changes. Automatic lifecycle management.
- rememberCoroutineScope: For effects triggered by user events (button clicks). You control when to launch.
Q: When do you use DisposableEffect?
For resources that need explicit cleanup:
- Registering/unregistering listeners
- Binding to lifecycle
- Managing third-party SDK lifecycles
- Callback registrations
5. Composition Local
Q: What is CompositionLocal?
A way to pass data implicitly through the composition tree without explicit parameters. Useful for cross-cutting concerns like theme, navigation, or locale.
Q: compositionLocalOf vs staticCompositionLocalOf?
| Type | Recomposition | Use Case |
|---|---|---|
compositionLocalOf | Only readers recompose when value changes | Frequently changing values |
staticCompositionLocalOf | Entire subtree recomposes when value changes | Rarely changing values (theme, locale) |
Q: When should you use CompositionLocal?
Sparingly. Good uses:
- Theme/colors (already provided by MaterialTheme)
- Navigation controller
- Dependency injection in Compose
Avoid for business data—use parameters for explicit, traceable data flow.
6. Lists and Performance
Q: How do you build efficient lists in Compose?
Use LazyColumn/LazyRow instead of Column/Row with forEach. Lazy composables only compose visible items.
Q: What is the key parameter and why is it important?
Keys help Compose identify which items changed, moved, or stayed the same. Without keys, Compose uses position—moving an item causes unnecessary recomposition of all items between old and new positions. With stable keys (like IDs), Compose preserves state and animates correctly.
Q: How do you optimize LazyColumn performance?
- Always provide keys: Use unique, stable identifiers
- Avoid heavy computation in items: Use
rememberor move to ViewModel - Use
contentType: Helps Compose reuse compositions - Avoid nesting scrollable containers: Causes measurement issues
- Use
derivedStateOffor scroll-dependent state: Reduces recomposition
7. Navigation
Q: How does Navigation work in Compose?
Navigation Compose uses a NavHost composable with a NavController. Routes are strings (optionally with arguments). Each route maps to a composable destination.
Q: How do you pass arguments between screens?
- Define route with placeholders:
"user/{userId}" - Navigate with values:
navController.navigate("user/123") - Receive in destination via
NavBackStackEntry.arguments
For complex objects, pass IDs and fetch data in the destination’s ViewModel.
Q: How do you handle navigation events from ViewModel?
Expose a one-time event (Channel or SharedFlow) that the UI collects and acts upon. Don’t hold NavController in ViewModel—it’s a UI concern.
8. Interop with Views
Q: How do you use Views inside Compose?
Use AndroidView composable. Provide a factory to create the View and an update lambda for when state changes. The View is created once and updated on recomposition.
Q: How do you use Compose inside Views?
Use ComposeView in XML or create it programmatically. Call setContent {} to provide composables. In Fragments, set viewCompositionStrategy to match the Fragment’s lifecycle.
Q: What’s important when mixing Compose and Views?
- Theme bridging: Use
MdcThemeor custom bridging - Lifecycle: Ensure Compose respects the View’s lifecycle owner
- State: Be careful about state ownership—one source of truth
- Performance: Minimize interop boundaries for best performance
Quick Reference
| Topic | Key Points |
|---|---|
| Recomposition | Triggers on state change; skips stable unchanged inputs; keep composables pure |
| Stability | Immutable = stable; mutable collections = unstable; use @Stable/@Immutable when needed |
| State | remember (composition), rememberSaveable (config change), ViewModel (business logic) |
| Side Effects | LaunchedEffect for coroutines, DisposableEffect for cleanup, SideEffect for every recomposition |
| Lists | Use LazyColumn; always provide keys; use contentType for mixed lists |
| Navigation | NavHost + NavController; string routes; pass IDs not objects |
Kotlin Advanced Features
1. Coroutines & Structured Concurrency
Q: What are Kotlin Coroutines and why are they important for Android?
Coroutines are lightweight, suspendable computations that allow you to write asynchronous code in a sequential, readable style. Unlike threads, coroutines are extremely cheap to create—you can launch thousands without significant overhead.
The key insight is that coroutines suspend rather than block. When a coroutine hits a suspension point (like a network call), it releases the thread to do other work, then resumes when the result is ready.
Q: Explain structured concurrency.
Structured concurrency means coroutines follow a parent-child hierarchy. When you launch a coroutine within a scope:
- The parent waits for all children to complete
- If the parent is cancelled, all children are cancelled
- If a child fails, the parent (and siblings) are cancelled by default
In Android, viewModelScope and lifecycleScope provide built-in structured concurrency—coroutines are automatically cancelled when the ViewModel clears or the lifecycle ends, preventing memory leaks and wasted work.
Q: What are the different Dispatchers and when do you use each?
| Dispatcher | Thread Pool | Use Case |
|---|---|---|
| Main | Android main thread | UI updates, light work |
| IO | Shared pool (64+ threads) | Network, database, file I/O |
| Default | CPU cores | Heavy computation, sorting, parsing |
| Unconfined | Inherits caller’s thread | Testing only, avoid in production |
Q: How do Dispatchers work under the hood?
Dispatchers are CoroutineContext elements that determine which thread(s) execute the coroutine. When you call withContext(Dispatchers.IO), the coroutine suspends, moves to an IO thread, executes the block, then resumes on the original dispatcher.
- Main uses Android’s main Looper—there’s only one main thread
- IO and Default share threads but have different policies. IO allows many concurrent operations (blocking calls), while Default is sized to CPU cores (compute-bound work)
- IO can grow beyond 64 threads if needed, but reuses threads from Default when possible
Q: What is withContext vs switching dispatchers with launch?
withContext suspends the current coroutine and runs the block on another dispatcher, then returns the result. It’s for sequential switching within a coroutine.
launch(Dispatchers.IO) creates a new child coroutine on that dispatcher. Use this for parallel/concurrent work.
Q: What’s the difference between launch and async?
- launch: Fire-and-forget. Returns a
Job. Use when you don’t need a result. - async: Returns a
Deferred<T>. Use when you need to compute a value, especially for parallel operations where youawait()multiple results.
Q: How does exception handling work in coroutines?
Regular launch propagates exceptions to the parent, which cancels siblings. Use SupervisorJob when you want child failures to be independent—one child’s exception won’t cancel others.
For async, exceptions are thrown when you call await(), so wrap that in try-catch.
2. Flow & Reactive Streams
Q: What is Kotlin Flow?
Flow is a cold asynchronous stream that emits multiple values over time. “Cold” means it doesn’t produce values until someone collects it, and each collector gets its own independent stream.
Q: Compare Flow, StateFlow, and SharedFlow.
| Type | Hot/Cold | Initial Value | Replay | Use Case |
|---|---|---|---|---|
| Flow | Cold | No | No | One-shot data, transformations |
| StateFlow | Hot | Required | Latest value | UI state, always has current value |
| SharedFlow | Hot | Optional | Configurable | Events, can have multiple subscribers |
Q: What are the key Flow operators?
- map/filter: Transform or filter emissions
- flatMapLatest: Cancel previous work when new value arrives (great for search)
- combine: Merge multiple flows, emit when any changes
- debounce: Wait for pause in emissions (search input)
- catch: Handle upstream errors
- flowOn: Change dispatcher for upstream operations
- stateIn/shareIn: Convert cold Flow to hot StateFlow/SharedFlow
Q: How does backpressure work in Flow?
Backpressure occurs when the producer emits faster than the consumer can process. Flow handles this naturally because it’s sequential by default—the producer suspends until the collector processes each emission.
For cases where you need different behavior:
- buffer(): Runs collector in separate coroutine, emissions don’t wait for processing
- conflate(): Skip intermediate values, only process the latest when collector is ready
- collectLatest(): Cancel previous collection when new value arrives (similar to flatMapLatest)
Use buffer() when you want to decouple producer/consumer speeds. Use conflate() or collectLatest() when only the latest value matters (UI updates, search queries).
Q: How do you safely collect Flow in Android?
Use repeatOnLifecycle or flowWithLifecycle to collect only when the UI is visible. This prevents wasted work when the app is in the background and avoids crashes from updating views after the Activity is destroyed.
3. Generics & Variance
Q: Explain variance in Kotlin generics.
Variance defines how generic types with inheritance relate to each other.
Invariant (default): Box<Dog> is NOT a subtype of Box<Animal>, even though Dog extends Animal. This is safe because the box could accept writes.
Covariant (out): The type can only be produced/returned, never consumed. List<out T> means you can read items but not add them. A List<Dog> IS a subtype of List<Animal>.
Contravariant (in): The type can only be consumed, never produced. Comparable<in T> means you can pass items in but not get them out. A Comparator<Animal> IS a subtype of Comparator<Dog>.
Q: How do you remember in vs out?
- out = output position only (return types) → Producer
- in = input position only (parameter types) → Consumer
Think “PECS” from Java: Producer Extends, Consumer Super.
4. Sealed Classes vs Enums
Q: When should you use sealed classes instead of enums?
Enums are perfect for fixed sets of constants with no varying data—like days of the week or simple status codes.
Sealed classes are better when:
- Different states carry different data (Success has data, Error has message)
- You need inheritance hierarchies
- Subtypes are data classes or objects with properties
The compiler enforces exhaustive when expressions for both, but sealed classes let each subtype have its own structure.
Q: What about sealed interfaces?
Sealed interfaces (Kotlin 1.5+) allow a class to implement multiple sealed hierarchies, which sealed classes can’t do. Use them when you need more flexible type modeling.
5. Inline Functions & Reified Types
Q: What does inline do?
The compiler copies the function body directly to every call site, eliminating the function call overhead and lambda object allocation. This matters for higher-order functions called frequently.
Q: What is reified and why is it useful?
Normally, generic type parameters are erased at runtime—you can’t access T::class. The reified keyword (only works with inline functions) preserves the type information, letting you:
- Get the class:
T::class.java - Check types:
value is T - Create instances or pass to reflection APIs
Common uses: JSON parsing (gson.fromJson<User>(json)), ViewModel creation, intent extras.
Q: What are the downsides of inline?
- Increases bytecode size if overused
- Can’t be used with recursive functions
- Public inline functions can’t access private members
6. Extension Functions
Q: How do extension functions work internally?
They’re compiled as static methods where the receiver becomes the first parameter. fun String.isEmail() becomes static boolean isEmail(String $this). They’re resolved statically at compile time, not dynamically.
Q: What can’t extension functions do?
- Access private/protected members of the class
- Override existing member functions (members always win)
- Be truly polymorphic (static dispatch, not virtual)
Q: Explain the scope functions (let, apply, run, also, with).
| Function | Returns | Receiver Access | Use Case |
|---|---|---|---|
| let | Lambda result | it | Null checks, transformations |
| apply | Receiver object | this | Object configuration |
| run | Lambda result | this | Execute block, compute result |
| also | Receiver object | it | Side effects, logging |
| with | Lambda result | this | Multiple calls on same object |
7. Data Classes
Q: What does declaring a data class give you automatically?
equals()andhashCode()based on constructor propertiestoString()with property names and valuescopy()for creating modified copiescomponentN()functions for destructuring
Q: What are the limitations?
- Must have at least one primary constructor parameter
- All constructor parameters must be
valorvar - Cannot be abstract, open, sealed, or inner
- Only primary constructor properties are used in generated methods
Q: Best practices for data classes?
- Prefer
valfor immutability - Avoid mutable collections as properties
- Be careful with properties declared in the body—they’re excluded from equals/hashCode
- Consider using
copy()instead of mutable properties
Quick Reference
| Topic | Key Points |
|---|---|
| Coroutines | Suspend don’t block; structured concurrency with scopes; Main/IO/Default dispatchers |
| Flow | Cold streams; StateFlow for state; SharedFlow for events; flatMapLatest for search |
| Variance | out = producer (covariant); in = consumer (contravariant) |
| Sealed Classes | States with data; exhaustive when; better than enum for complex cases |
| Inline/Reified | Inline copies body; reified preserves generic type at runtime |
| Extensions | Static dispatch; can’t access private; scope functions for fluent code |
| Data Classes | Auto equals/hashCode/copy/toString; immutable preferred |
Dependency Injection
1. DI Fundamentals
Q: What is Dependency Injection and why use it?
Dependency Injection is a design pattern where objects receive their dependencies from external sources rather than creating them internally. Benefits:
- Testability: Swap real implementations with fakes/mocks
- Decoupling: Classes don’t know how dependencies are created
- Reusability: Same class works with different implementations
- Maintainability: Change dependency configuration in one place
Q: What’s the difference between DI and Service Locator?
| Dependency Injection | Service Locator |
|---|---|
| Dependencies pushed to object | Object pulls dependencies |
| Dependencies explicit in constructor | Dependencies hidden inside class |
| Compile-time verification | Runtime failures possible |
| Easier to test | Requires global state management |
Q: Constructor injection vs field injection?
Constructor injection (preferred): Dependencies are required parameters. Object can’t exist without them. Immutable, testable, clear dependencies.
Field injection: Dependencies set after construction via annotation. Required for Android components (Activity, Fragment) where we don’t control construction. Use sparingly elsewhere.
2. Hilt
Q: What is Hilt and how does it relate to Dagger?
Hilt is Google’s DI library built on top of Dagger. It provides:
- Predefined components matching Android lifecycles
- Standard scopes (Singleton, ActivityScoped, etc.)
- Built-in support for ViewModel injection
- Less boilerplate than pure Dagger
Dagger is the underlying code generation engine; Hilt adds Android-specific conventions.
Q: What are the key Hilt annotations?
| Annotation | Purpose |
|---|---|
@HiltAndroidApp | Trigger Hilt code generation on Application class |
@AndroidEntryPoint | Enable injection in Activity, Fragment, Service, etc. |
@HiltViewModel | Enable constructor injection for ViewModel |
@Inject | Mark constructor for injection or field for field injection |
@Module | Class that provides dependencies |
@InstallIn | Specify which component the module belongs to |
@Provides | Method that creates a dependency |
@Binds | Bind interface to implementation (more efficient than @Provides) |
Q: Explain Hilt’s component hierarchy.
SingletonComponent (Application lifetime)
↓
ActivityRetainedComponent (survives config changes)
↓
ViewModelComponent (ViewModel lifetime)
↓
ActivityComponent → FragmentComponent → ViewComponent
Objects in parent components can be injected into child components, but not vice versa.
3. Scopes
Q: What are scopes and why do they matter?
Scopes control the lifetime and sharing of dependencies. Without scoping, Hilt creates a new instance every time one is requested.
| Scope | Lifetime | Use Case |
|---|---|---|
@Singleton | App lifetime | Retrofit, OkHttp, Database |
@ActivityRetainedScoped | Survives rotation | Shared state across Activity recreations |
@ViewModelScoped | ViewModel lifetime | Dependencies specific to one ViewModel |
@ActivityScoped | Activity lifetime | Activity-specific presenters |
@FragmentScoped | Fragment lifetime | Fragment-specific logic |
Q: What’s the cost of @Singleton?
Singleton objects live for the entire app lifetime, consuming memory even when not needed. They can’t be garbage collected. Use only for truly app-wide dependencies.
Q: How do scopes affect testing?
Scoped dependencies are shared within that scope. In tests, you may need to replace the entire scoped object, not just one usage. Hilt provides @TestInstallIn to swap modules for tests.
4. Providing Dependencies
Q: When do you use @Binds vs @Provides?
| @Binds | @Provides |
|---|---|
| Interface → Implementation mapping | Complex object creation |
| Abstract method | Concrete method |
| No method body | Method body with creation logic |
| More efficient (no extra method call) | Required for third-party classes |
Q: How do you provide third-party dependencies?
Use @Provides in a module since you can’t add @Inject to classes you don’t own:
@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {
@Provides
@Singleton
fun provideRetrofit(): Retrofit = Retrofit.Builder()
.baseUrl("https://api.example.com/")
.build()
}
Q: What are Qualifiers and when do you need them?
Qualifiers distinguish between multiple bindings of the same type. Example: You might have two OkHttpClient instances—one with auth interceptor, one without. Use custom qualifier annotations to differentiate.
5. ViewModel Injection
Q: How does ViewModel injection work in Hilt?
Mark ViewModel with @HiltViewModel and use @Inject constructor. Hilt creates a ViewModelProvider.Factory automatically. In UI, use by viewModels() (Fragment) or hiltViewModel() (Compose).
Q: How do you pass arguments to ViewModel via Hilt?
Use SavedStateHandle—it’s automatically populated with arguments from the Fragment’s bundle or Navigation arguments.
@HiltViewModel
class DetailViewModel @Inject constructor(
private val repository: Repository,
private val savedStateHandle: SavedStateHandle
) : ViewModel() {
private val itemId: String = savedStateHandle.get<String>("itemId")!!
}
Q: How do you inject assisted dependencies (runtime values)?
Use @AssistedInject and @AssistedFactory. The factory takes runtime parameters that can’t be provided by Hilt.
6. Testing with Hilt
Q: How do you test with Hilt?
- Use
@HiltAndroidTeston test class - Add
HiltAndroidRuleas a JUnit rule - Call
hiltRule.inject()before tests - Use
@TestInstallInto replace production modules
Q: How do you replace dependencies for testing?
Create a test module that replaces the production module:
@Module
@TestInstallIn(
components = [SingletonComponent::class],
replaces = [RepositoryModule::class]
)
abstract class FakeRepositoryModule {
@Binds
abstract fun bind(impl: FakeRepository): Repository
}
Q: What about unit testing ViewModels?
For unit tests, skip Hilt entirely. Construct ViewModel directly with fake dependencies:
@Test
fun `test viewmodel`() {
val fakeRepository = FakeRepository()
val viewModel = MyViewModel(fakeRepository)
// test...
}
7. Common Patterns
Q: How do you handle optional dependencies?
Use @Nullable or provide a default. Hilt doesn’t support optional bindings directly—every requested dependency must have a binding.
Q: How do you handle multibindings?
Hilt supports @IntoSet and @IntoMap for collecting multiple implementations:
@Provides
@IntoSet
fun provideInterceptor(): Interceptor = LoggingInterceptor()
All items annotated with @IntoSet for that type are collected into a Set<Interceptor>.
Q: How do you lazily initialize dependencies?
Inject Lazy<T> or Provider<T>:
Lazy<T>: Creates instance on first.get(), returns same instance thereafterProvider<T>: Creates new instance on every.get()call
Quick Reference
| Concept | Key Points |
|---|---|
| Hilt | Dagger + Android conventions; @HiltAndroidApp, @AndroidEntryPoint, @HiltViewModel |
| Scopes | @Singleton (app), @ViewModelScoped (VM), @ActivityScoped (activity); unscoped = new instance each time |
| @Binds | Interface → impl mapping; abstract, more efficient |
| @Provides | Complex creation, third-party classes; concrete method body |
| Qualifiers | Distinguish same-type bindings; custom annotations |
| Testing | @HiltAndroidTest, @TestInstallIn to swap modules; unit tests skip Hilt |
Networking and Data
1. Networking Fundamentals
Q: Explain the OSI model layers relevant to mobile development.
| Layer | Protocol | Mobile Relevance |
|---|---|---|
| Application (7) | HTTP, WebSocket, gRPC | API communication |
| Transport (4) | TCP, UDP | Reliability vs speed |
| Network (3) | IP | Routing, addressing |
| Link (2) | WiFi, Cellular | Connection type affects behavior |
Most Android work happens at Application layer; understanding Transport helps with optimization.
Q: TCP vs UDP—when would you use each?
| TCP | UDP |
|---|---|
| Connection-oriented | Connectionless |
| Guaranteed delivery, ordering | No guarantees |
| Slower, more overhead | Fast, lightweight |
| HTTP, HTTPS, WebSocket | Video streaming, VoIP, gaming |
Mobile apps typically use TCP (via HTTP) for reliability. UDP for real-time where dropped packets are acceptable.
Q: Explain HTTP/1.1 vs HTTP/2 vs HTTP/3.
| Version | Connection | Key Features |
|---|---|---|
| HTTP/1.1 | One request per connection (or keep-alive) | Sequential, head-of-line blocking |
| HTTP/2 | Multiplexed streams over one connection | Parallel requests, header compression, server push |
| HTTP/3 | QUIC (UDP-based) | Faster handshakes, no TCP head-of-line blocking |
OkHttp supports HTTP/2 by default. HTTP/3 adoption is growing.
Q: What is TLS/SSL and why is it important?
TLS (Transport Layer Security) encrypts communication:
- Confidentiality: Data can’t be read in transit
- Integrity: Data can’t be modified
- Authentication: Server proves identity via certificate
Android enforces HTTPS by default (cleartext blocked). TLS 1.2+ required for modern apps.
Q: What happens during a TLS handshake?
- Client sends supported cipher suites
- Server chooses cipher, sends certificate
- Client verifies certificate against trusted CAs
- Key exchange (asymmetric) establishes session key
- Symmetric encryption begins
This adds latency; HTTP/2 and HTTP/3 reduce repeat handshakes.
2. REST & API Design
Q: What is REST and its principles?
REST (Representational State Transfer) is an architectural style:
| Principle | Meaning |
|---|---|
| Stateless | Each request contains all needed info |
| Client-Server | Separation of concerns |
| Cacheable | Responses indicate cacheability |
| Uniform Interface | Standard methods (GET, POST, PUT, DELETE) |
| Layered | Client doesn’t know if talking to server or intermediary |
Q: Explain HTTP methods and when to use each.
| Method | Purpose | Idempotent | Safe |
|---|---|---|---|
| GET | Retrieve resource | Yes | Yes |
| POST | Create resource | No | No |
| PUT | Replace resource | Yes | No |
| PATCH | Partial update | No | No |
| DELETE | Remove resource | Yes | No |
Idempotent: Same request multiple times = same result Safe: Doesn’t modify server state
Q: What are common HTTP status codes?
| Code | Meaning | Action |
|---|---|---|
| 200 | OK | Success |
| 201 | Created | Resource created (POST) |
| 204 | No Content | Success, no body |
| 400 | Bad Request | Client error, fix request |
| 401 | Unauthorized | Need authentication |
| 403 | Forbidden | Authenticated but not allowed |
| 404 | Not Found | Resource doesn’t exist |
| 429 | Too Many Requests | Rate limited, backoff |
| 500 | Server Error | Server problem, maybe retry |
| 503 | Service Unavailable | Temporary, retry with backoff |
Q: REST vs GraphQL vs gRPC?
| Aspect | REST | GraphQL | gRPC |
|---|---|---|---|
| Data Format | JSON | JSON | Protobuf (binary) |
| Flexibility | Fixed endpoints | Client specifies fields | Defined services |
| Over-fetching | Common | Solved | N/A |
| Performance | Good | Good | Best (binary) |
| Mobile Use | Most common | Growing | Backend-to-backend, some mobile |
3. WebSocket & Real-Time
Q: When would you use WebSocket instead of HTTP?
| HTTP | WebSocket |
|---|---|
| Request-response | Bidirectional, persistent |
| Client initiates | Either side can send |
| Stateless | Stateful connection |
| Polling for updates | Push updates |
Use WebSocket for: chat, live updates, collaborative editing, gaming.
Q: How does WebSocket work?
- HTTP request with
Upgrade: websocketheader - Server responds 101 Switching Protocols
- Connection upgrades to WebSocket
- Full-duplex communication over same TCP connection
- Either side can close
Q: What alternatives exist for real-time updates?
| Technique | How It Works | Trade-off |
|---|---|---|
| Polling | Repeated HTTP requests | Simple but wasteful |
| Long Polling | Server holds request until data | Better but complex |
| SSE | Server-Sent Events, one-way stream | Simple, HTTP-based |
| WebSocket | Full duplex | Most capable, more complex |
| Push Notifications | FCM/APNs | Works when app closed |
4. Retrofit & OkHttp
Q: Explain the Android networking stack.
The typical stack consists of:
- Retrofit: Type-safe HTTP client, converts API endpoints to Kotlin interfaces
- OkHttp: Underlying HTTP client handling connections, caching, interceptors
- Gson/Moshi/Kotlinx.serialization: JSON parsing
Retrofit uses OkHttp internally. You configure OkHttp with interceptors and timeouts, then pass it to Retrofit.
Q: What are interceptors and what types exist?
Interceptors observe, modify, and potentially short-circuit requests/responses.
| Type | Runs When | Use Case |
|---|---|---|
| Application Interceptor | Before OkHttp core | Add headers, logging, modify requests |
| Network Interceptor | After connection established | Access network-level info, redirects |
Common interceptors:
- Authentication: Add auth headers to every request
- Logging: Log request/response for debugging
- Retry: Retry failed requests with backoff
- Caching: Custom cache control
Q: How do you handle authentication token refresh?
Use an Authenticator (not interceptor). When a 401 is received, the authenticator refreshes the token and retries the request. This handles the race condition of multiple simultaneous requests hitting 401.
Q: How do you handle errors?
Map HTTP errors to domain exceptions:
- IOException: Network failures (no connection, timeout)
- HttpException: Server returned error status (401, 404, 500)
Create sealed classes like Result<T> or NetworkResult<T> to represent Success/Error states uniformly.
5. Room Database
Q: What is Room and its components?
Room is Jetpack’s SQLite abstraction providing:
- Entity: Data class representing a table (
@Entity) - DAO: Interface with SQL queries (
@Dao,@Query,@Insert) - Database: Abstract class holding DAOs (
@Database)
Room generates implementation at compile time, catches SQL errors early.
Q: How does Room integrate with reactive streams?
DAOs can return Flow<T> or LiveData<T> for observable queries. When data changes, observers automatically receive updates.
Q: What are common Room annotations?
| Annotation | Purpose |
|---|---|
@Entity | Define table |
@PrimaryKey | Primary key column |
@ColumnInfo | Customize column name |
@Embedded | Flatten nested object into table |
@Relation | Define relationship between entities |
@TypeConverter | Convert custom types to/from SQLite types |
Q: How do you handle database migrations?
Define Migration objects specifying how to alter schema from version N to N+1. Room runs migrations sequentially. If migration is missing, Room throws an exception (fail-safe). Use fallbackToDestructiveMigration() only for development.
Q: What about testing Room?
Use in-memory database for tests:
Room.inMemoryDatabaseBuilder(context, AppDatabase::class.java)
.allowMainThreadQueries() // Only for tests!
.build()
6. DataStore
Q: What is DataStore and how does it differ from SharedPreferences?
DataStore is the modern replacement for SharedPreferences:
| SharedPreferences | DataStore |
|---|---|
| Synchronous API | Async (Flow-based) |
| Can block main thread | Never blocks |
| No type safety | Type-safe with Proto DataStore |
| Exception handling unclear | Exceptions in Flow |
| No transaction guarantees | Transactional |
Q: What are the two types of DataStore?
- Preferences DataStore: Key-value pairs, like SharedPreferences but async
- Proto DataStore: Typed objects using Protocol Buffers, schema-defined
Q: When do you use each?
- Preferences DataStore: Simple settings, feature flags, user preferences
- Proto DataStore: Complex structured data, type safety critical
- Room: Relational data, complex queries, multiple tables
7. Caching Strategies
Q: What caching strategies are common in Android?
| Strategy | Flow | Use Case |
|---|---|---|
| Cache-first | Return cache → fetch network → update cache | News feeds, social content |
| Network-first | Try network → fallback to cache | Critical data, stock prices |
| Cache-only | Only return cache | Offline-first apps |
| Network-only | Always fetch, no cache | Authentication, payments |
| Stale-while-revalidate | Return stale cache → update in background | Best UX for most apps |
Q: How do you implement cache-first with Room + Retrofit?
- Repository observes Room via Flow
- On request, emit cached data immediately
- Fetch from network in parallel
- Insert network data into Room
- Room Flow automatically emits updated data
This gives instant UI with fresh data arriving shortly after.
Q: How do you determine cache validity?
- Timestamp-based: Store fetch time, expire after duration
- ETags: Let server determine if data changed
- Server-provided expiry: Use Cache-Control headers
- Event-based: Invalidate on specific user actions
8. Pagination
Q: How does Paging 3 work?
Paging 3 loads data in chunks as the user scrolls:
| Component | Role |
|---|---|
| PagingSource | Loads pages from single source (network or database) |
| RemoteMediator | Loads network data into database for offline support |
| Pager | Produces PagingData Flow |
| PagingDataAdapter | Submits PagingData to RecyclerView/LazyColumn |
Q: PagingSource vs RemoteMediator?
- PagingSource alone: Simple pagination, network-only or database-only
- RemoteMediator + PagingSource: Offline-first. RemoteMediator fetches network data into DB, PagingSource loads from DB. Best practice for production apps.
Q: How do you handle errors and retry in Paging?
LoadState contains Loading, NotLoading, and Error states for prepend, append, and refresh. Display loading/error UI based on these states. Provide retry action that calls adapter.retry().
9. Image Loading
Q: What does an image loading library do?
- Async download with disk/memory caching
- Image transformations (resize, crop, round)
- Placeholder and error images
- Lifecycle awareness (cancel on destroy)
- Memory management (avoid OOM)
Q: Coil vs Glide?
| Coil | Glide |
|---|---|
| Kotlin-first, uses coroutines | Java-based, custom threading |
| Lighter, modern | More features, mature |
| Compose integration built-in | Compose support via extension |
| 10KB method count | Larger footprint |
Both are excellent. Coil is preferred for new Kotlin-only projects; Glide for maximum feature set.
Q: How do you optimize image loading?
- Request correct size (don’t load 4000px for 100dp view)
- Use appropriate format (WebP for compression)
- Enable hardware bitmaps where supported
- Cache appropriately (memory for frequent, disk for all)
- Preload upcoming images in lists
Quick Reference
| Topic | Key Points |
|---|---|
| HTTP Basics | TCP for reliability; HTTP/2 for multiplexing; TLS for security |
| REST | Stateless; GET/POST/PUT/DELETE; status codes communicate result |
| WebSocket | Bidirectional; persistent connection; use for chat, live updates |
| Retrofit/OkHttp | Type-safe client; interceptors for headers/logging; Authenticator for 401 |
| Room | Entity + DAO + Database; Flow for reactive queries; migrations for schema |
| DataStore | Async SharedPreferences replacement; Preferences or Proto types |
| Caching | Cache-first for UX; network-first for critical data; Room as cache |
| Paging | PagingSource for simple; RemoteMediator for offline-first; handle LoadState |
Concurrency and Threading
1. Android Threading Model
Q: What is Android’s threading rule?
Never block the main (UI) thread. The main thread:
- Handles all UI rendering and user input
- Blocks for >5 seconds → ANR (Application Not Responding)
- Must be responsive for 60fps (16ms per frame)
All long operations (network, database, heavy computation) must run on background threads.
Q: What are the options for background work?
| Tool | Use Case | Lifecycle |
|---|---|---|
| Coroutines | Most async work | Tied to scope (viewModelScope, lifecycleScope) |
| WorkManager | Guaranteed, deferrable work | Survives process death, respects constraints |
| Foreground Service | Long-running user-visible work | Lives independently of UI |
| Handler/Looper | Legacy, low-level thread communication | Manual management |
Q: Why are coroutines preferred over RxJava or raw threads?
- First-party Kotlin support
- Simpler syntax (sequential code style)
- Structured concurrency prevents leaks
- Less boilerplate than RxJava
- Built-in cancellation
- Better Compose/Flow integration
2. Coroutine Dispatchers
Q: What are the main dispatchers and their characteristics?
| Dispatcher | Thread Pool | Optimized For |
|---|---|---|
| Main | Single main thread | UI updates, light work |
| IO | 64+ threads (elastic) | Blocking I/O (network, disk) |
| Default | CPU core count | CPU-intensive computation |
| Unconfined | Inherits caller | Testing only (dangerous in production) |
Q: How do IO and Default share threads?
They share the same underlying pool but have different limits. IO can scale to many threads (blocking calls don’t hurt), while Default is limited to CPU cores (compute-bound work benefits from locality).
Q: What’s the difference between withContext and launch?
- withContext: Suspends current coroutine, runs block on specified dispatcher, returns result. For sequential dispatcher switching.
- launch(Dispatcher): Creates new child coroutine on that dispatcher. For parallel/concurrent work.
3. Structured Concurrency
Q: What is structured concurrency?
Coroutines follow a parent-child hierarchy within a scope:
- Parent waits for all children to complete
- Cancelling parent cancels all children
- Child failure propagates to parent (by default)
This prevents orphan coroutines and ensures cleanup.
Q: What scopes should you use in Android?
| Scope | Lifecycle | Use Case |
|---|---|---|
| viewModelScope | ViewModel | Most business logic |
| lifecycleScope | Activity/Fragment | UI-related work |
| rememberCoroutineScope | Composition | Compose event handlers |
| CoroutineScope | Custom | Services, application-level |
Q: Why avoid GlobalScope?
GlobalScope creates coroutines that:
- Never auto-cancel (leak potential)
- Live for app lifetime
- Can’t be tested properly
- Violate structured concurrency
Use custom scopes instead, cancelled when appropriate.
4. Exception Handling
Q: How do exceptions propagate in coroutines?
| Builder | Exception Behavior |
|---|---|
| launch | Propagates to parent immediately, cancels siblings |
| async | Stored until await() is called, then thrown |
Q: What is SupervisorJob and when do you use it?
SupervisorJob prevents child failure from canceling siblings. Use when:
- Multiple independent operations shouldn’t affect each other
- UI can show partial results even if some fail
- Retry logic for individual failures
viewModelScope uses SupervisorJob by default.
Q: How should you handle exceptions?
try/catch in coroutine → Handle expected errors (network, parsing)
CoroutineExceptionHandler → Global fallback for uncaught exceptions
SupervisorJob → Isolate failures between siblings
For async, always wrap await() in try/catch or use runCatching.
5. Cancellation
Q: How does coroutine cancellation work?
Cancellation is cooperative. Coroutines must check for cancellation:
isActiveproperty- Suspending functions (delay, yield, withContext)
ensureActive()call
If you have a tight loop without suspension, it won’t cancel until it checks.
Q: What happens on cancellation?
CancellationException is thrown. It’s special:
- Doesn’t propagate to parent as failure
- Caught silently by the coroutine machinery
finallyblocks still execute for cleanup
Q: How do you clean up on cancellation?
Use finally block:
try {
doWork()
} finally {
// Always runs, even on cancellation
cleanup()
}
For suspending cleanup, wrap in withContext(NonCancellable).
6. Thread Safety
Q: What thread safety issues occur with coroutines?
Even with single-threaded dispatcher (Main), coroutines can interleave at suspension points. With multi-threaded dispatchers (IO, Default), classic race conditions apply.
Q: How do you achieve thread safety?
| Approach | Use Case |
|---|---|
| Mutex | Protect critical sections (like synchronized) |
| Atomic types | Single variable updates (AtomicInteger, etc.) |
| Single-threaded confinement | Keep state access on one dispatcher |
| Immutable data | Prevent mutation entirely |
| Channel | Thread-safe communication between coroutines |
Q: What’s the Actor pattern?
A coroutine that processes messages sequentially from a Channel. All state is confined to the actor, eliminating race conditions. Messages are sent from any thread, processed one at a time.
7. WorkManager
Q: When do you use WorkManager?
For guaranteed background work that must complete even if:
- App is killed
- Device restarts
- Process dies
Examples: syncing data, uploading logs, periodic cleanup.
Q: What are WorkManager constraints?
| Constraint | Purpose |
|---|---|
| NetworkType | Require connected, unmetered, etc. |
| BatteryNotLow | Wait for sufficient battery |
| StorageNotLow | Wait for storage space |
| DeviceIdle | Wait for device idle |
| RequiresCharging | Wait for charging |
Q: OneTimeWorkRequest vs PeriodicWorkRequest?
- OneTimeWorkRequest: Run once, optionally with initial delay
- PeriodicWorkRequest: Repeat at intervals (min 15 minutes)
Q: How do you chain work?
workManager
.beginWith(downloadWork)
.then(processWork)
.then(uploadWork)
.enqueue()
Parallel work uses beginWith(listOf(work1, work2)).
Quick Reference
| Topic | Key Points |
|---|---|
| Threading Rule | Never block main thread; use background threads for I/O and computation |
| Dispatchers | Main (UI), IO (blocking I/O), Default (CPU), Unconfined (testing only) |
| Structured Concurrency | Parent-child hierarchy; automatic cancellation; use viewModelScope/lifecycleScope |
| Exceptions | launch propagates immediately; async stores until await; use SupervisorJob for isolation |
| Cancellation | Cooperative; check isActive or use suspending functions; cleanup in finally |
| Thread Safety | Mutex for critical sections; immutable data; single-threaded confinement |
| WorkManager | Guaranteed execution; survives process death; respects constraints |
Performance Optimization
1. Memory Management
Q: What causes memory leaks in Android?
A memory leak occurs when objects that should be garbage collected are still referenced. Common causes:
| Cause | Example | Fix |
|---|---|---|
| Static reference to Context | Singleton holding Activity | Use Application Context |
| Non-static inner class | Handler, AsyncTask referencing Activity | Make static, use WeakReference |
| Unregistered listeners | Broadcast receiver not unregistered | Unregister in onStop/onDestroy |
| Long-lived references | ViewModel holding View reference | Never store View in ViewModel |
| Binding not cleared | Fragment binding outlives view | Null binding in onDestroyView |
Q: How do you detect memory leaks?
- LeakCanary: Automatic detection in debug builds
- Android Studio Profiler: Memory allocation tracking
- Heap dump analysis: Find objects that should be GC’d
- StrictMode: Detect leaked closeable objects
Q: What is the memory hierarchy on Android?
Registers → L1 Cache → L2 Cache → RAM → Disk
(fastest) (slowest)
Allocations are cheap, but GC pauses affect UI. Reduce allocations in hot paths (onDraw, adapters).
2. ANR Prevention
Q: What causes ANR (Application Not Responding)?
- Main thread blocked for >5 seconds for input events
- BroadcastReceiver not completing within 10 seconds
- Service not starting within 20 seconds
Q: How do you prevent ANRs?
| Don’t | Do Instead |
|---|---|
| Database queries on main thread | Use Room with coroutines/Flow |
| Network calls on main thread | Use Retrofit suspend functions |
| Heavy computation on main thread | Use withContext(Dispatchers.Default) |
| Complex View inflation | Use ViewStub for lazy inflation |
| Synchronous file I/O | Use withContext(Dispatchers.IO) |
Q: How do you debug ANRs?
- Check
traces.txtin/data/anr/ - Look for main thread stack trace
- Find blocking operation
- Use StrictMode in development to catch early
3. UI Performance
Q: What causes jank (dropped frames)?
Each frame must complete in 16ms for 60fps. Causes of jank:
- Overdraw (drawing same pixel multiple times)
- Complex view hierarchies (nested layouts)
- Heavy onDraw operations
- Layout thrashing (measure/layout during animation)
- Main thread blocking
Q: How do you reduce overdraw?
- Remove unnecessary backgrounds
- Use
clipRect()andquickReject()in custom views - Flatten view hierarchy
- Use
tools:showOverdrawto visualize
Q: How do you optimize view hierarchies?
| Problem | Solution |
|---|---|
| Nested LinearLayouts | Use ConstraintLayout (flat hierarchy) |
| Views that aren’t always shown | Use ViewStub for lazy inflation |
| Complex item layouts | Simplify, use merge tag |
| Frequent layout changes | Use TransitionManager for batched changes |
Q: RecyclerView optimization?
- Use
DiffUtilfor efficient list updates - Set
setHasFixedSize(true)if size doesn’t change - Use
RecycledViewPoolfor nested RecyclerViews - Avoid inflation in
onBindViewHolder - Use stable IDs for item animations
4. Rendering Pipeline
Q: Explain Android’s rendering pipeline.
- Measure: Calculate view sizes
- Layout: Position views in hierarchy
- Draw: Render to canvas
- Sync & Upload: Transfer to GPU
- Issue Draw Commands: GPU renders
- Swap Buffers: Display on screen
Double buffering ensures smooth display while next frame prepares.
Q: What is hardware acceleration?
GPU handles rendering instead of CPU. Enabled by default. Benefits:
- Faster for most operations
- Frees CPU for other work
Some operations aren’t hardware-accelerated (complex Path operations). Use software layer for those.
5. Startup Performance
Q: What affects app startup time?
| Phase | Bottlenecks |
|---|---|
| Cold start | Process creation, class loading, Application init |
| Warm start | Activity creation, but process exists |
| Hot start | Activity resumes, already in memory |
Q: How do you optimize cold start?
- Minimize work in
Application.onCreate() - Use lazy initialization for non-essential services
- Defer heavy initialization with
App Startuplibrary - Use splash screen (Android 12+ SplashScreen API)
- Reduce class loading (smaller APK, fewer libraries)
- Enable baseline profiles (ART optimization)
Q: What are Baseline Profiles?
AOT-compiled methods for common user journeys. Reduces JIT compilation on first run. Create using Macrobenchmark, include in release APK.
6. Battery Optimization
Q: What drains battery on Android?
| Component | Impact |
|---|---|
| CPU wakeups | High—keeping CPU active |
| Network | High—radio power state |
| GPS | Very high—continuous location |
| Sensors | Medium—depends on sensor |
| Screen | Highest—display on |
Q: How do you reduce battery usage?
- Batch network requests (don’t wake radio repeatedly)
- Use
WorkManagerwith constraints (wait for charging) - Request coarse location when fine isn’t needed
- Use JobScheduler/WorkManager over AlarmManager
- Reduce polling; use push notifications
- Follow Doze and App Standby guidelines
Q: What are Doze and App Standby?
Doze: When device is stationary and screen off, system defers jobs, syncs, alarms. Apps get periodic maintenance windows.
App Standby: Apps not recently used get restricted network and job access.
Handle by using WorkManager with appropriate constraints—system manages execution.
7. Network Optimization
Q: How do you optimize network usage?
| Strategy | Benefit |
|---|---|
| Caching | Reduce redundant requests |
| Compression | Smaller payloads (gzip) |
| Pagination | Load only needed data |
| Connection pooling | Reuse connections (OkHttp default) |
| Prefetching | Load before needed (WiFi) |
| Batching | Combine multiple requests |
Q: How does HTTP caching work?
OkHttp respects Cache-Control headers. Configure cache directory and size. Server controls caching with headers:
max-age: Cache durationno-cache: Revalidate each timeno-store: Never cache
8. Profiling Tools
Q: What tools are available for performance analysis?
| Tool | Purpose |
|---|---|
| Android Studio Profiler | CPU, memory, network, energy |
| Systrace / Perfetto | System-wide tracing |
| Layout Inspector | View hierarchy, Compose recomposition |
| GPU Profiler | Rendering performance |
| Macrobenchmark | Startup and runtime benchmarks |
| StrictMode | Detect I/O on main thread |
Q: How do you use StrictMode effectively?
Enable in debug builds only:
if (BuildConfig.DEBUG) {
StrictMode.setThreadPolicy(
StrictMode.ThreadPolicy.Builder()
.detectAll()
.penaltyLog()
.build()
)
}
Catches disk/network access on main thread early in development.
Quick Reference
| Area | Key Points |
|---|---|
| Memory Leaks | No static Context; null bindings; unregister listeners; use LeakCanary |
| ANR | No I/O on main thread; use coroutines; check traces.txt for debugging |
| UI Performance | Flatten hierarchies; reduce overdraw; use DiffUtil; profile with Systrace |
| Startup | Lazy init; defer work; baseline profiles; minimize Application.onCreate() |
| Battery | Batch requests; use WorkManager; respect Doze; prefer push over polling |
| Network | Cache responses; compress payloads; paginate; batch requests |
Testing
1. Testing Pyramid
Q: What is the testing pyramid and how does it apply to Android?
| Layer | Speed | Scope | Tools |
|---|---|---|---|
| Unit Tests | Fast (ms) | Single class/function | JUnit, Mockito, Turbine |
| Integration Tests | Medium | Multiple components | Robolectric, Room testing |
| UI Tests | Slow (s) | Full app/flows | Espresso, Compose Testing |
Aim for: Many unit tests, some integration tests, few UI tests.
Q: What should each layer test?
- Unit: ViewModel logic, Repository, Use Cases, data transformations
- Integration: Room DAOs, Repository with real database, navigation
- UI: Critical user flows, screen state rendering, accessibility
2. Unit Testing
Q: How do you test ViewModels?
- Create ViewModel with fake dependencies
- Trigger actions
- Assert on exposed state
Key considerations:
- Use
TestDispatcherfor coroutine control - Use
runTestfor test coroutine scope - Use
Turbinefor Flow testing
Q: How do you test coroutines?
Use kotlinx-coroutines-test:
runTest: Test coroutine scope with virtual timeStandardTestDispatcher: Queues coroutines, runs withadvanceUntilIdle()UnconfinedTestDispatcher: Runs coroutines eagerly
Inject TestDispatcher via constructor or Hilt.
Q: How do you test Flows?
Use Turbine library:
viewModel.state.test {
assertEquals(Loading, awaitItem())
assertEquals(Success(data), awaitItem())
cancelAndIgnoreRemainingEvents()
}
Or use first(), take(), toList() for simpler cases.
3. Mocking
Q: When should you mock vs use fakes?
| Fakes | Mocks |
|---|---|
| Real implementation for testing | Configured to return specific values |
| Easier to maintain | Can verify interactions |
| Better for repositories, DAOs | Better for third-party dependencies |
| More readable tests | Tests coupled to implementation |
Prefer fakes for your own code, mocks for things you don’t control.
Q: What mocking libraries are common?
- Mockito-Kotlin: Most popular, mature
- MockK: Kotlin-first, coroutine support
- Fake implementations: Hand-written, no framework
Q: What should you NOT mock?
- Data classes / value objects
- Your own simple classes (use real instances)
- Android framework classes (use Robolectric or real device)
4. Testing Room
Q: How do you test Room DAOs?
Use in-memory database:
@Before
fun setup() {
db = Room.inMemoryDatabaseBuilder(
context, AppDatabase::class.java
).allowMainThreadQueries().build()
dao = db.userDao()
}
@After
fun teardown() {
db.close()
}
Test actual SQL queries—don’t mock Room.
Q: What should DAO tests verify?
- Insert and query return correct data
- Update modifies existing records
- Delete removes records
- Queries filter correctly
- Flow emits on data changes
5. UI Testing
Q: What’s the difference between Espresso and Compose Testing?
| Espresso | Compose Testing |
|---|---|
| View-based UI | Compose UI |
| Find by ID, text, content description | Find by semantic properties |
onView(withId(...)).perform(click()) | onNodeWithText(...).performClick() |
Q: How do you structure Compose UI tests?
@Test
fun showsLoadingThenContent() {
composeTestRule.setContent {
MyScreen(viewModel = fakeViewModel)
}
// Assert loading state
composeTestRule.onNodeWithTag("loading").assertIsDisplayed()
// Trigger loaded state
fakeViewModel.setLoaded(data)
// Assert content
composeTestRule.onNodeWithText("Title").assertIsDisplayed()
}
Q: What makes UI tests reliable?
- Use
waitFor/waitUntilfor async operations - Use test tags for stable identifiers (not user-facing text)
- Inject controlled ViewModels/repositories
- Disable animations in test device/emulator
- Keep tests focused on one scenario
6. Testing Strategies
Q: What’s the “Given-When-Then” pattern?
@Test
fun `user login with valid credentials succeeds`() {
// Given: preconditions
val fakeRepo = FakeAuthRepository(validCredentials = true)
val viewModel = LoginViewModel(fakeRepo)
// When: action
viewModel.login("user@email.com", "password123")
// Then: expected result
assertEquals(LoginState.Success, viewModel.state.value)
}
Makes tests readable and self-documenting.
Q: How do you test error handling?
@Test
fun `network error shows error state`() = runTest {
fakeRepository.setShouldThrowError(true)
viewModel.loadData()
val state = viewModel.state.value
assertTrue(state is UiState.Error)
assertEquals("Network error", (state as UiState.Error).message)
}
Test both happy path and error paths.
Q: What about testing navigation?
Options:
- Mock NavController, verify
navigate()called - Test with TestNavHostController
- For Compose: use
NavHostin test, assert current destination
Keep navigation tests integration-level (not unit tests).
7. Test Doubles
Q: Explain different test doubles.
| Type | Purpose |
|---|---|
| Fake | Working implementation with shortcuts (in-memory DB) |
| Stub | Returns predetermined values |
| Mock | Records calls, verifies interactions |
| Spy | Wraps real object, tracks calls |
| Dummy | Placeholder, never actually used |
Q: Example of a Fake Repository?
class FakeUserRepository : UserRepository {
private val users = mutableListOf<User>()
override suspend fun getUsers(): List<User> = users
override suspend fun addUser(user: User) {
users.add(user)
}
fun clear() = users.clear()
fun seed(list: List<User>) = users.addAll(list)
}
Simple, controllable, no mocking framework needed.
8. Testing Best Practices
Q: What makes a good unit test?
- Fast: Milliseconds, not seconds
- Independent: No order dependency
- Repeatable: Same result every time
- Self-validating: Pass or fail, no manual inspection
- Timely: Written alongside production code
Q: What’s code coverage and how much is enough?
Code coverage measures which code paths are exercised by tests. 70-80% is typical target. But:
- 100% coverage doesn’t mean good tests
- Behavior coverage matters more than line coverage
- Focus on critical paths and edge cases
Q: How do you test legacy code without tests?
- Write characterization tests (document current behavior)
- Identify seams for dependency injection
- Extract testable components
- Add tests before refactoring
- Refactor with test safety net
Quick Reference
| Topic | Key Points |
|---|---|
| Pyramid | Many unit, some integration, few UI tests |
| Unit Tests | Fast, isolated, test logic not frameworks |
| Coroutines | runTest, TestDispatcher, Turbine for Flows |
| Mocking | Prefer fakes for own code; mocks for third-party |
| Room | In-memory database; test real SQL |
| UI Tests | Compose Testing for Compose; Espresso for Views; disable animations |
| Best Practices | FIRST principles; Given-When-Then; test behavior not implementation |
Security
1. Secure Data Storage
Q: Where should sensitive data be stored on Android?
| Data Type | Storage | Notes |
|---|---|---|
| Encryption keys | Android Keystore | Hardware-backed on supported devices |
| User credentials | EncryptedSharedPreferences | Keys in Keystore |
| Auth tokens | EncryptedSharedPreferences or DataStore | Never plain SharedPreferences |
| Sensitive files | Encrypted files with Keystore-derived keys | |
| Database | SQLCipher or Room with encryption |
Q: What is Android Keystore?
A system-level secure container for cryptographic keys. Keys can be:
- Hardware-backed (TEE/Secure Element) on supported devices
- Bound to user authentication (biometric/PIN required)
- Non-exportable (key material never leaves secure hardware)
Q: How does EncryptedSharedPreferences work?
Uses two keys from Keystore:
- Key encryption key (KEK) encrypts data keys
- Data encryption keys encrypt actual values
Both key names and values are encrypted at rest.
2. Network Security
Q: What is SSL/TLS pinning and why use it?
Pinning validates the server’s certificate against a known value, not just the certificate chain. Protects against:
- Compromised Certificate Authorities
- Man-in-the-middle attacks with rogue certs
- Malicious proxies with user-installed certs
Q: How do you implement certificate pinning?
OkHttp CertificatePinner:
val pinner = CertificatePinner.Builder()
.add("api.example.com", "sha256/AAAAAAA...")
.add("api.example.com", "sha256/BBBBBBB...") // Backup pin
.build()
Network Security Config (preferred):
<domain-config>
<domain includeSubdomains="true">api.example.com</domain>
<pin-digest algorithm="SHA-256">base64hash=</pin-digest>
</domain-config>
Q: What’s the risk of certificate pinning?
If pins expire or rotate without app update, the app breaks. Mitigations:
- Always include backup pins
- Use Network Security Config (updatable)
- Plan for emergency pin rotation
- Monitor certificate expiration
3. Authentication & Authorization
Q: How should auth tokens be handled?
- Store in EncryptedSharedPreferences
- Never log tokens
- Use short-lived access tokens + refresh tokens
- Clear tokens on logout
- Handle token expiration gracefully
Q: How do you implement secure biometric authentication?
Use BiometricPrompt with:
setAllowedAuthenticators()to specify biometric classCryptoObjectto tie authentication to cryptographic operation- Keystore key that requires user authentication
Key is unusable until biometric succeeds—actual security, not just UI.
Q: What authentication classes exist?
| Class | Security Level | Examples |
|---|---|---|
| Class 3 (Strong) | Highest | Fingerprint, face with depth sensing |
| Class 2 (Weak) | Medium | Some face recognition |
| Class 1 | Lowest | Pattern, PIN (not biometric) |
For sensitive operations, require Class 3.
4. App Security
Q: How do you protect against reverse engineering?
| Technique | Purpose |
|---|---|
| ProGuard/R8 | Code shrinking, obfuscation |
| String encryption | Hide hardcoded secrets |
| Anti-tampering | Detect modified APK |
| Root detection | Detect compromised device |
| Emulator detection | Detect analysis environment |
Q: What shouldn’t be in the APK?
- API keys with billing access
- Private encryption keys
- Hardcoded passwords
- Production database credentials
These can be extracted. Use server-side validation for sensitive operations.
Q: How do you handle API keys?
- Store in
local.properties(gitignored) - Inject via BuildConfig at build time
- For sensitive keys, proxy through your backend
- Use Play App Signing for signing keys
5. Input Validation
Q: What input validation is necessary?
- Validate all user input on client AND server
- Sanitize content for WebView (
JavaScriptEnabledcarefully) - Validate deep links and intent data
- Escape SQL if not using Room (Room parameterizes)
- Validate file paths to prevent path traversal
Q: What is a SQL injection and how does Room prevent it?
Malicious SQL in user input can modify queries. Room prevents by:
- Parameterized queries (values can’t become SQL)
- Compile-time SQL verification
- Type-safe query parameters
Q: What about WebView security?
- Disable JavaScript unless necessary
- Use
WebViewAssetLoaderfor local content - Don’t enable
setAllowFileAccessFromFileURLs - Validate URLs before loading
- Handle
shouldOverrideUrlLoadingcarefully
6. Inter-Process Communication
Q: How do you secure exported components?
| Component | Protection |
|---|---|
| Activity | android:exported="false" or permission |
| Service | Use bound service with permission |
| BroadcastReceiver | Custom permission, signature level |
| ContentProvider | Read/write permissions, path permissions |
Q: What are signature-level permissions?
Only apps signed with the same key can obtain the permission. Use for communication between your own apps.
Q: How do you validate incoming Intents?
- Check
callingPackageif applicable - Validate all extras (don’t trust input)
- Use
PendingIntent.FLAG_IMMUTABLEwhen possible - Don’t pass sensitive data in Intent extras
7. Runtime Security
Q: What is SafetyNet/Play Integrity?
Server-side API to verify:
- Device is genuine (not rooted/emulated)
- App is legitimate (from Play Store)
- Device passes security checks
Use for sensitive operations; response should be verified server-side.
Q: How do you detect rooted devices?
Check for:
- su binary existence
- Root management apps
- System property modifications
- Dangerous permissions granted
Note: Determined attackers can bypass; use as defense in depth, not sole protection.
8. Common Vulnerabilities
Q: What are OWASP Mobile Top 10 concerns for Android?
| Risk | Mitigation |
|---|---|
| Insecure Data Storage | Encrypted storage, avoid logs |
| Insecure Communication | TLS everywhere, pinning |
| Insecure Authentication | Strong auth, biometrics |
| Insufficient Cryptography | Use Android Keystore, standard algorithms |
| Insecure Authorization | Server-side validation |
| Client Code Quality | Code review, static analysis |
| Code Tampering | ProGuard, integrity checks |
| Reverse Engineering | Obfuscation, server-side logic |
| Extraneous Functionality | Remove debug code in release |
| Insufficient Binary Protection | Native code obfuscation |
Quick Reference
| Topic | Key Points |
|---|---|
| Data Storage | Keystore for keys; EncryptedSharedPreferences for secrets; never plain text |
| Network | HTTPS only; certificate pinning with backups; validate server responses |
| Auth | Short-lived tokens; secure storage; biometric with CryptoObject |
| APK Protection | R8 obfuscation; no secrets in code; server-side sensitive logic |
| Input Validation | Client and server validation; Room for SQL safety; careful WebView |
| IPC | Minimal exported components; signature permissions; validate intents |
| Runtime | Play Integrity for verification; defense in depth against rooting |
Build System
1. Gradle Basics
Q: What is Gradle and how does it work in Android?
Gradle is the build automation tool for Android. It:
- Compiles source code
- Manages dependencies
- Creates APK/AAB packages
- Runs tests
- Signs and optimizes releases
Android uses the Android Gradle Plugin (AGP) which adds Android-specific tasks.
Q: What are the key Gradle files?
| File | Purpose |
|---|---|
| settings.gradle.kts | Defines modules in project |
| build.gradle.kts (project) | Project-wide config, plugin versions |
| build.gradle.kts (module) | Module-specific config, dependencies |
| gradle.properties | Build settings, JVM config |
| local.properties | Local machine config (SDK path, secrets) |
Q: Groovy DSL vs Kotlin DSL?
| Groovy | Kotlin |
|---|---|
| Older, more examples | Type-safe, IDE support |
| Dynamic typing | Static typing, autocomplete |
.gradle extension | .gradle.kts extension |
Kotlin DSL is now recommended for new projects.
2. Build Variants
Q: What are build types and product flavors?
Build types define how the app is built:
debug: Debuggable, no optimization, debug signingrelease: Optimized, minified, production signing
Product flavors define different versions:
free/paiddemo/fullstaging/production
Variants = Build Type × Product Flavors (e.g., freeDebug, paidRelease)
Q: What are common build type configurations?
| Property | Debug | Release |
|---|---|---|
isDebuggable | true | false |
isMinifyEnabled | false | true |
isShrinkResources | false | true |
signingConfig | debug | release |
applicationIdSuffix | .debug | (none) |
Q: How do you configure different API endpoints per flavor?
Use buildConfigField:
productFlavors {
create("staging") {
buildConfigField("String", "API_URL", "\"https://staging.api.com\"")
}
create("production") {
buildConfigField("String", "API_URL", "\"https://api.com\"")
}
}
Access via BuildConfig.API_URL in code.
3. Dependencies
Q: Explain dependency configurations.
| Configuration | Meaning |
|---|---|
implementation | Compile and runtime, not exposed to consumers |
api | Compile and runtime, exposed to consumers |
compileOnly | Compile only, not in runtime |
runtimeOnly | Runtime only, not for compilation |
testImplementation | For unit tests |
androidTestImplementation | For instrumented tests |
Q: implementation vs api?
implementation: Dependency is internal, doesn’t leak to consumersapi: Dependency is part of public API, consumers can use it
Prefer implementation—faster builds, better encapsulation.
Q: How do you handle dependency conflicts?
- Gradle uses highest version by default
- Use
constraintsto force specific version - Use
excludeto remove transitive dependencies - Check conflicts with
./gradlew dependencies - Use version catalogs for consistency
4. Version Catalogs
Q: What are version catalogs?
Centralized dependency management in libs.versions.toml:
[versions]
kotlin = "1.9.20"
compose = "1.5.4"
[libraries]
compose-ui = { group = "androidx.compose.ui", name = "ui", version.ref = "compose" }
[bundles]
compose = ["compose-ui", "compose-material", "compose-foundation"]
[plugins]
kotlin = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
Usage: implementation(libs.compose.ui)
Benefits: Single source of truth, type-safe, IDE support.
5. ProGuard/R8
Q: What is R8 and what does it do?
R8 is Android’s code shrinker (replaced ProGuard). It:
- Shrinks: Removes unused code
- Optimizes: Inlines methods, removes dead branches
- Obfuscates: Renames classes/methods to short names
- Reduces APK size: Significant size reduction
Q: What are ProGuard/R8 rules?
Rules tell R8 what to keep:
-keep class com.example.Model: Don’t remove or rename-keepnames class ...: Don’t rename (can still remove if unused)-keepclassmembers: Keep members, not class itself
Q: When are keep rules needed?
- Reflection (classes loaded by name)
- Serialization (Gson, JSON parsing)
- JNI (native methods)
- Custom views in XML
- Libraries without rules
Most libraries include their own rules via consumerProguardFiles.
6. Signing
Q: What is APK/AAB signing?
Digital signature that:
- Proves app comes from you
- Ensures APK hasn’t been modified
- Required for Play Store and device installation
- Same key required for updates
Q: Debug vs Release signing?
| Debug | Release |
|---|---|
| Auto-generated debug keystore | Your production keystore |
| Same on all machines | Must be protected |
| Can’t publish to Play Store | Required for Play Store |
Q: What is Play App Signing?
Google manages your app signing key:
- You upload with upload key
- Google re-signs with app signing key
- If upload key compromised, can reset it
- Key never leaves Google’s servers
Recommended for all new apps.
7. Build Performance
Q: How do you speed up Gradle builds?
| Setting | Effect |
|---|---|
org.gradle.parallel=true | Build modules in parallel |
org.gradle.caching=true | Cache task outputs |
org.gradle.daemon=true | Keep Gradle process running |
kapt.incremental.apt=true | Incremental annotation processing |
| Increase JVM heap | org.gradle.jvmargs=-Xmx4g |
Q: What is incremental compilation?
Only recompile files that changed. Kotlin supports it, but annotation processors (KAPT) can break it. Consider KSP (Kotlin Symbol Processing) for better incremental support.
Q: How do you analyze build performance?
./gradlew --profile: Generate HTML report- Build Analyzer in Android Studio
--scanfor detailed Gradle build scan- Check configuration phase time (runs every build)
8. Modularization
Q: Why modularize an Android project?
- Faster builds: Only changed modules rebuild
- Better encapsulation: Clear boundaries,
internalvisibility - Parallel development: Teams work independently
- Reusability: Share modules across apps
- Dynamic delivery: Feature modules on demand
Q: What module types exist?
| Type | Purpose |
|---|---|
| app | Main application module |
| library | Shared code, produces AAR |
| feature | Self-contained feature |
| core | Common utilities, base classes |
| data | Repository, data sources |
| domain | Business logic, use cases |
Q: What’s dynamic feature delivery?
Feature modules delivered on-demand or by condition:
- Install-time: Included in base APK
- On-demand: Downloaded when needed
- Conditional: Based on device features
Uses App Bundle format, Play Store handles delivery.
Quick Reference
| Topic | Key Points |
|---|---|
| Gradle Files | settings.gradle.kts (modules), build.gradle.kts (config), gradle.properties (settings) |
| Build Variants | Build types × Flavors; debug/release × free/paid |
| Dependencies | implementation (internal), api (exposed), version catalogs for consistency |
| R8 | Shrink, optimize, obfuscate; keep rules for reflection/serialization |
| Signing | Debug auto-generated; release keystore protected; Play App Signing recommended |
| Build Speed | Parallel, caching, daemon; avoid KAPT, use KSP; analyze with –profile |
| Modularization | Faster builds, encapsulation, parallel development; app/library/feature/core/data modules |
System Design
1. Approaching Design Questions
Q: How do you approach a mobile system design question?
Follow this structure:
-
Clarify Requirements (2-3 min)
- Functional: What features?
- Non-functional: Offline? Scale? Battery?
- Constraints: Platform? API limitations?
-
High-Level Design (5-10 min)
- Architecture diagram
- Main components
- Data flow
-
Deep Dive (10-15 min)
- Specific component design
- Data models
- Key algorithms
-
Trade-offs & Extensions (5 min)
- What you’d do differently
- Scalability considerations
- Edge cases
Q: What non-functional requirements matter for mobile?
| Requirement | Considerations |
|---|---|
| Offline Support | Local caching, sync strategy, conflict resolution |
| Performance | Startup time, scroll performance, memory usage |
| Battery | Network batching, background work limits |
| Bandwidth | Compression, pagination, delta updates |
| Security | Data encryption, secure communication |
2. News Feed / Social Feed
Q: Design a news feed like Twitter or Instagram.
Key Components:
- Feed Repository: Single source of truth, coordinates remote/local
- Paging: Infinite scroll with Paging 3
- Caching: Room database as cache
- Real-time: WebSocket or polling for new items
Architecture:
UI (LazyColumn) → ViewModel → Repository
├── RemoteMediator → API
└── PagingSource → Room
Key Decisions:
- Cursor vs Offset pagination: Cursor is stable (items don’t shift)
- Cache invalidation: Time-based or on pull-to-refresh
- Optimistic updates: Show locally before server confirms
- Image loading: Preload next page images
Trade-offs:
- More caching = better UX, more storage/complexity
- WebSocket = real-time, more battery/complexity
3. Messaging App
Q: Design a chat/messaging app like WhatsApp.
Key Components:
- Message Repository: Handles send/receive/sync
- WebSocket/XMPP: Real-time message delivery
- Local Database: All messages stored locally
- Background Service: Receive messages when app closed
Data Model:
Conversation: id, participants, lastMessage, unreadCount
Message: id, conversationId, sender, content, timestamp, status
Key Decisions:
- Delivery guarantees: At-least-once with deduplication
- Message status: Sent → Delivered → Read
- Offline messages: Queue locally, sync when online
- End-to-end encryption: Signal Protocol pattern
Sync Strategy:
- On app start, fetch missed messages since last sync
- WebSocket for real-time during session
- Push notification triggers sync when app closed
4. Image Loading Library
Q: Design an image loading library like Coil or Glide.
Key Components:
- Request Queue: Prioritize visible images
- Memory Cache: LruCache for decoded bitmaps
- Disk Cache: DiskLruCache for downloaded files
- Decoder: BitmapFactory with sampling
- Transformations: Resize, crop, rounded corners
Flow:
Request → Memory Cache? → Disk Cache? → Network
↓ ↓ ↓ ↓
Done Decode Decode Download
↓ ↓ ↓
Cache in Cache in Cache both
Memory Memory
Key Decisions:
- Memory cache size: ~1/8 of available memory
- Disk cache size: ~50-250MB configurable
- Request cancellation: Cancel when view recycled
- Placeholder/Error: Immediate feedback
Optimizations:
- Downsampling: Load at target size, not full resolution
- Request coalescing: Same URL = single request
- Preloading: Load upcoming images in list
5. Offline-First App
Q: How do you design an offline-first application?
Principles:
- Local database is source of truth
- All operations work offline
- Sync when connectivity available
- Handle conflicts gracefully
Architecture:
UI → ViewModel → Repository (SSOT)
↓
Local DB ←→ SyncManager → API
Sync Strategies:
| Strategy | Approach | Use Case |
|---|---|---|
| Last-write-wins | Latest timestamp wins | Simple, some data loss ok |
| Server-wins | Server always correct | Authoritative server data |
| Client-wins | Client always correct | User edits are precious |
| Manual resolution | User chooses | Collaborative editing |
| CRDT | Automatic merge | Complex, no conflicts |
Key Decisions:
- Change tracking: Track local modifications since last sync
- Conflict detection: Compare versions/timestamps
- Partial sync: Only sync changed data
- Background sync: WorkManager with network constraint
6. Video Streaming
Q: Design a video streaming app like YouTube/Netflix.
Key Components:
- Player: ExoPlayer with adaptive streaming
- Caching: Pre-cache upcoming segments
- Download: Background download for offline
- CDN: Multiple quality levels, nearby servers
Streaming Protocol:
- HLS/DASH: Adaptive bitrate streaming
- Segments downloaded progressively
- Quality adjusts to network conditions
Key Decisions:
- Buffer size: Balance memory vs smooth playback
- Quality selection: Auto (ABR) or user choice
- Preloading: Start loading next video early
- Resume position: Save and restore playback position
Offline Viewing:
- Download in background with WorkManager
- Encrypt downloaded content (DRM)
- Track download progress, allow pause/resume
- Expire after license period
7. Location-Based App
Q: Design a ride-sharing or delivery app.
Key Components:
- Location Service: Foreground service with updates
- Map Integration: Google Maps or Mapbox
- Real-time Updates: Driver location via WebSocket
- Geofencing: Trigger events at locations
Battery Considerations:
- Use significant location change (not continuous)
- Batch location updates
- Reduce frequency when stationary
- Foreground service required for background location
Key Decisions:
- Location accuracy: Fused Location Provider, balance power/accuracy
- Update frequency: Every 5-10 seconds active, less in background
- Predictive routing: Update ETA based on traffic
- Offline maps: Cache tiles for core areas
8. E-Commerce App
Q: Design a shopping app like Amazon.
Key Components:
- Product Catalog: Search, browse, filter
- Cart: Local + synced
- Checkout: Payment integration
- Order Tracking: Status updates
Architecture Decisions:
- Search: Debounce input, paginate results
- Cart: Optimistic local updates, sync to server
- Product Cache: Cache popular items, invalidate on changes
- Deep Linking: Open product from notification/share
Key Features:
- Recently Viewed: Local history
- Recommendations: Personalized from server
- Wishlist: Local with sync
- Reviews: Lazy load, paginate
9. Design Patterns for Mobile
Q: What patterns come up repeatedly in mobile system design?
| Pattern | Problem | Solution |
|---|---|---|
| Repository | Coordinate data sources | Single source of truth |
| Cache-aside | Reduce network calls | Check cache, fallback to network |
| Retry with backoff | Handle transient failures | Exponential backoff |
| Optimistic update | Responsive UI | Update local, sync later |
| Event sourcing | Audit, undo, offline | Store events, derive state |
| CQRS | Different read/write needs | Separate models |
Quick Reference
| Scenario | Key Considerations |
|---|---|
| News Feed | Paging, caching, cursor pagination, image preloading |
| Messaging | WebSocket, local DB, delivery status, encryption |
| Image Loading | Memory/disk cache, cancellation, downsampling |
| Offline-First | Local SSOT, sync strategy, conflict resolution |
| Video | Adaptive streaming, buffering, DRM, background download |
| Location | Battery, foreground service, update frequency |
| E-Commerce | Search, cart sync, deep linking, recommendations |
AOSP & Android Internals
1. Android Architecture
Q: Explain the Android system architecture.
Android is a layered system stack:
| Layer | Components | Language |
|---|---|---|
| Applications | System apps, third-party apps | Kotlin/Java |
| Framework | ActivityManager, PackageManager, WindowManager | Java |
| Native Libraries | libc, SSL, SQLite, Media | C/C++ |
| Android Runtime | ART, core libraries | C++/Java |
| HAL | Camera, Audio, Sensors | C/C++ |
| Linux Kernel | Drivers, Binder, power management | C |
Q: What does each layer do?
- Framework: Provides APIs developers use (Activities, Views, Services)
- ART: Executes app code, manages memory, garbage collection
- HAL: Abstracts hardware differences—same API for different devices
- Kernel: Core OS—process scheduling, memory, drivers
2. Boot Process
Q: Describe the Android boot sequence.
- Boot ROM: First code, loads bootloader
- Bootloader: Initializes hardware, loads kernel
- Kernel: Initializes drivers, mounts filesystem, starts init
- Init: First user process, parses init.rc, starts daemons
- Zygote: Preloads classes, spawns app processes
- System Server: Starts all system services (AMS, PMS, WMS)
- Launcher: Home screen app starts
Q: What is Zygote and why does it exist?
Zygote is the parent of all app processes. It:
- Preloads ~4000 common classes
- Preloads common resources
- Forks to create new app processes
Benefits:
- Fast app startup (classes already loaded)
- Memory sharing (Copy-on-Write)
- Consistent runtime environment
Q: What does System Server do?
Starts and manages all system services:
- ActivityManagerService (AMS): Manages activities, apps
- PackageManagerService (PMS): Installs, queries packages
- WindowManagerService (WMS): Manages windows, input
- 100+ other services
Each service runs in a Binder thread pool within System Server process.
3. Binder IPC
Q: What is Binder and why does Android use it?
Binder is Android’s inter-process communication mechanism. Apps run in isolated processes; Binder allows them to communicate with system services and each other.
Why not traditional IPC?
- Security: Built-in caller UID/PID verification
- Performance: Single copy (not two like pipes)
- Object-oriented: RPC semantics, pass object references
- Reference counting: Automatic cleanup
Q: How does a Binder call work?
- Client calls method on Proxy object
- Proxy marshals parameters into Parcel
- Binder driver transfers to server process
- Stub unmarshals and calls real implementation
- Result marshaled back through Binder
- Proxy returns result to client
All app-to-system communication uses Binder (getSystemService → Binder).
Q: What is AIDL?
Android Interface Definition Language. Generates Proxy/Stub code for Binder communication. You define the interface, AIDL generates the IPC boilerplate.
4. Activity Manager Service
Q: What is ActivityManagerService (AMS)?
The core system service managing:
- Activity lifecycle and task stacks
- Process lifecycle and LRU management
- App launch and Intent resolution
- Recent apps list
- Foreground/background state
Q: How does app launch work internally?
- Launcher calls
startActivity(Intent) - Intent goes to AMS via Binder
- AMS resolves Intent to target Activity
- AMS checks if process exists
- If not, asks Zygote to fork new process
- New process starts
ActivityThread.main() - App attaches to AMS, AMS schedules Activity creation
- ActivityThread creates Activity, calls
onCreate()
Q: How does AMS decide which processes to kill?
Maintains LRU list with process importance:
- Foreground (visible Activity)
- Visible (partially visible)
- Service (background service)
- Cached (background activities)
- Empty (no components)
Kills from bottom up when memory needed.
5. PackageManagerService
Q: What does PackageManagerService (PMS) do?
- Installs/uninstalls packages
- Parses and stores package metadata
- Resolves Intent to components
- Manages permissions
- Maintains package database
Q: What happens during APK installation?
- APK copied to staging area
- Signature verification
- Manifest parsing
- Native library extraction
- DEX optimization (dex2oat)
- Package added to PMS database
- Broadcast
PACKAGE_ADDED
Q: How does Intent resolution work?
- App calls
startActivity(intent) - PMS scans registered IntentFilters
- Matches action, category, data against filters
- Returns matching components
- If multiple, shows chooser
- AMS launches chosen component
6. Android Runtime (ART)
Q: What is ART and how does it differ from Dalvik?
| Dalvik | ART |
|---|---|
| JIT (Just-In-Time) | AOT + JIT (Ahead-Of-Time) |
| Compiled at runtime | Compiled during install |
| Slower startup | Faster runtime |
| Less storage | More storage (compiled code) |
ART also has:
- Better garbage collection (concurrent)
- Improved profiling and debugging
- Profile-guided compilation
Q: What is DEX and why is it used?
DEX (Dalvik Executable) is Android’s bytecode format:
- Optimized for memory-constrained devices
- More compact than Java bytecode
- Register-based (not stack-based)
- Single DEX = all classes (vs separate .class files)
dex2oat compiles DEX to native code during install.
Q: How does garbage collection work in ART?
- Concurrent: Most work while app runs
- Generational: Young objects collected more often
- Compacting: Reduces fragmentation
- Card marking: Track cross-generation references
GC pauses are minimal (<5ms typically).
7. Window System
Q: How does the Android window system work?
WindowManagerService (WMS) manages:
- Window creation and positioning
- Z-ordering of windows
- Input routing to correct window
- Surface allocation
SurfaceFlinger (native):
- Composites surfaces from apps
- Sends to display hardware
- Handles VSync timing
Q: What is a Surface?
Buffer that apps draw to. Each window has a Surface:
- App draws to Surface via Canvas or OpenGL
- Surface is shared memory with SurfaceFlinger
- SurfaceFlinger composites all Surfaces
- Result sent to display
Q: How does hardware acceleration work?
- App builds display list (recording of draw operations)
- RenderThread processes display list on GPU
- Canvas operations translated to OpenGL
- Frees main thread for other work
8. HAL and Treble
Q: What is HAL?
Hardware Abstraction Layer—interface between Android framework and device hardware. Standardized API regardless of underlying hardware:
- Camera HAL: Same API, different camera chips
- Audio HAL: Same API, different audio hardware
- Sensor HAL: Same API, different sensors
Q: What is Project Treble?
Architecture change in Android 8.0:
- Separates vendor implementation from framework
- HAL implementations become HIDL services
- Framework can update without touching vendor code
- Enables faster Android updates on devices
Before: HAL directly linked into framework After: HAL as separate processes, communication via HIDL/AIDL
9. Security Architecture
Q: How does Android’s security model work?
App Sandbox:
- Each app runs as separate Linux user
- Private data directory per app
- Can’t access other apps’ data
- SELinux provides mandatory access control
Permissions:
- Declared in manifest
- User grants at install or runtime
- System enforces at API level
Verified Boot:
- Each boot stage verifies next stage
- Detects system modification
- Rollback protection
Q: What is SELinux on Android?
Mandatory Access Control—even root can’t bypass:
- Policies define what each domain can access
- Apps run in app domain, limited access
- System services have specific domains
- Blocks many exploit techniques
10. Common Interview Topics
Q: What’s asked in AOSP/internals interviews?
| Topic | Focus Areas |
|---|---|
| Boot Process | Sequence, Zygote role, System Server |
| Binder | How IPC works, AIDL, Parcelable |
| AMS | App launch, process management, task stack |
| PMS | Installation, Intent resolution, permissions |
| ART | DEX, AOT/JIT, garbage collection |
| Window System | WMS, Surface, SurfaceFlinger |
| Security | Sandbox, permissions, SELinux |
Q: Why do companies ask about internals?
- Platform engineering roles require it
- Debugging complex issues needs system knowledge
- Performance optimization requires understanding internals
- Security work needs deep knowledge
- Shows depth of understanding beyond SDK
Quick Reference
| Topic | Key Points |
|---|---|
| Architecture | Apps → Framework → Native/ART → HAL → Kernel |
| Boot | ROM → Bootloader → Kernel → Init → Zygote → System Server → Launcher |
| Binder | IPC mechanism; Proxy/Stub pattern; single-copy, secure |
| AMS | Manages activities, processes, app lifecycle; LRU for killing |
| PMS | Installs packages, resolves intents, manages permissions |
| ART | AOT+JIT compilation; concurrent GC; DEX bytecode format |
| Windows | WMS manages windows; SurfaceFlinger composites; Surface is draw buffer |
| Security | App sandbox (Linux users); SELinux; verified boot |