For the complete documentation index, see llms.txt. This page is also available as Markdown.

Interview Prep 1

Kotlin & Language

Q1. What are the key differences between inline, noinline, and crossinline functions in Kotlin?

inline copies the function body at the call site, eliminating lambda allocation overhead and allowing non-local returns. Use it for higher-order functions with lambda parameters to reduce runtime cost. noinline marks specific lambda parameters of an inline function that should not be inlined, keeping them as regular function objects for storage or repeated use. crossinline forbids non-local returns inside the lambda, ensuring the inlined code doesn't return from an enclosing function; useful when the lambda is executed in a different execution context like a coroutine or callback. All three control how lambdas behave at the bytecode level. inline is powerful but increases code size if overused. noinline sacrifices performance for flexibility. crossinline enforces safety when passing lambdas to nested scopes. Choose based on performance needs and control flow requirements.

inline fun measure(crossinline action: () -> Unit, noinline callback: () -> Unit) {
    val start = System.currentTimeMillis()
    action() // can't return from here
    println("Took ${System.currentTimeMillis() - start}ms")
    callback() // stored/called later
}

Read more: https://kotlinlang.org/docs/inline-functions.html


Q2. How do coroutines differ from RxJava in terms of cancellation, backpressure, and thread management?

Coroutines use structured concurrency where cancellation propagates automatically through parent-child hierarchies via Job trees. RxJava requires explicit Disposable management and composite disposables for cleanup. Backpressure in RxJava is explicit via strategies like onBackpressureBuffer or drop; Kotlin Flow handles it naturally through suspending emission—consumers pull at their own pace since collectors suspend until ready. Thread management in coroutines is declarative via Dispatchers (Main, IO, Default) and can be switched within the same function using withContext. RxJava uses subscribeOn and observeOn schedulers, which are more rigid and harder to follow. Coroutines are lighter, using fewer objects per operation. RxJava offers richer operator ecosystems but steeper learning curves. Both can interoperate via kotlinx-coroutines-rx3 adapters.

viewModelScope.launch {
    val data = withContext(Dispatchers.IO) { repository.fetch() }
    _state.value = data
} // auto-cancelled when ViewModel clears

Read more: https://kotlinlang.org/docs/coroutines-basics.html


Q3. Explain the difference between Flow and LiveData—when would you choose one over the other?

Flow is a cold stream built on coroutines that emits values sequentially and supports complex transformations via functional operators like map, filter, and flatMapLatest. LiveData is an observable data holder class aware of Android lifecycle, automatically stopping observation when the lifecycle owner is inactive. Use Flow for data layer and business logic where you need backpressure, threading control, and composable async streams. Use LiveData in the presentation layer when you need lifecycle-aware UI updates without manual subscription management. StateFlow and SharedFlow are Flow variants that can replace LiveData in ViewModels while offering more configuration. LiveData is simpler for basic UI cases but lacks operators and coroutine integration. Modern Android recommends Flow in repositories and LiveData or StateFlow in ViewModels depending on team conventions.

Read more: https://developer.android.com/kotlin/flow


Q4. What are reified type parameters and what limitations do they solve?

Reified type parameters, declared with inline fun <reified T>, make generic type information available at runtime inside the function body. Normally, JVM type erasure removes generic types after compilation, so you cannot check if (x is T) or access T::class in regular generic functions. reified solves this by inlining the function, allowing the compiler to substitute the actual type argument at each call site. This enables type checks, reflection, and instantiation patterns without passing Class<T> tokens manually. Common uses include JSON deserialization (fromJson<T>()), ViewModel creation by class, and type-safe routing. The limitation is that reified only works with inline functions, increasing code size. It cannot be used with anonymous objects or non-inline contexts.

Read more: https://kotlinlang.org/docs/inline-functions.html#reified-type-parameters


Q5. How does Kotlin's delegation pattern (by) work under the hood, and where have you used it in production?

The by keyword implements the delegation pattern, generating a hidden property that stores the delegate instance and forwarding all interface methods to it. For by lazy, Kotlin creates a Lazy instance that synchronizes initialization on first access. For class delegation class MyList by ArrayList<String>(), the compiler generates forwarding methods for every interface member to the delegate property. In production, use by lazy for expensive ViewModel or repository initialization. Use class delegation to add behavior to existing classes without inheritance—e.g., decorating a Repository with caching or logging. Use by viewModels() and by activityViewModels() for Hilt ViewModel injection. The generated bytecode is efficient but increases class method count. It enforces composition over inheritance.

Read more: https://kotlinlang.org/docs/delegation.html


Q6. What are the risks of using suspend functions inside non-suspending lambdas?

Passing a suspend function to a regular lambda (e.g., setOnClickListener { mySuspendFun() }) requires wrapping it in a coroutine builder like lifecycleScope.launch, which creates a new coroutine with potentially uncontrolled scope. This risks structured concurrency violations—if the lambda outlives the UI component, the coroutine may leak or access destroyed views. Without proper scope, exceptions propagate differently and cancellation is manual rather than automatic. Fire-and-forget patterns in callbacks bypass parent job hierarchies, making cleanup difficult. Nested suspend calls inside non-suspending contexts often lead to callback hell if not properly structured. Always ensure the coroutine builder uses a scope tied to the lifecycle. Using suspendCoroutine or callbackFlow is safer for bridging callbacks to coroutines.

Read more: https://kotlinlang.org/docs/coroutines-basics.html


Q7. How do you handle structured concurrency when launching coroutines in custom ViewModel scopes?

Extend ViewModel and use its built-in viewModelScope, which follows the ViewModel lifecycle and cancels automatically on onCleared(). For custom scopes, implement CoroutineScope with a SupervisorJob plus a dispatcher, then cancel it manually in the lifecycle teardown. Use supervisorScope or coroutineScope builders inside ViewModel methods to enforce parent-child relationships where child failures don't cancel siblings. Launch work using launch or async within these scopes, never with GlobalScope. For shared operations across multiple ViewModels, delegate to a UseCase or Repository scoped to a higher-level component. Always handle exceptions with CoroutineExceptionHandler attached to the scope. Test by injecting TestDispatcher and verifying cancellation behavior.

Read more: https://developer.android.com/topic/libraries/architecture/coroutines


Q8. Explain the difference between SharedFlow and StateFlow with respect to configuration and replay behavior.

StateFlow is a conflated SharedFlow specialized for state representation—it always holds a single current value, emits only when the value changes (distinctUntilChanged behavior), and requires an initial state. SharedFlow is more configurable: you set replay cache size, extraBufferCapacity, and onBufferOverflow strategy (DROP_OLDEST, DROP_LATEST, SUSPEND). StateFlow has no replay buffer beyond the current state and never suspends emitters. SharedFlow can replay multiple past values to new subscribers, making it ideal for events like navigation or snackbars. StateFlow is read synchronously via .value, while SharedFlow is only observed. Both are hot flows—active regardless of collectors. Configure SharedFlow carefully to avoid memory leaks from large replay caches.

Read more: https://kotlinlang.org/docs/flow.html#stateflow-and-sharedflow


Q9. What is the purpose of sealed classes versus sealed interfaces in Kotlin, and how do they affect exhaustive when expressions?

sealed classes restrict inheritance to a known set of subclasses declared in the same package or module, enabling exhaustive when expressions without an else branch. sealed interfaces (Kotlin 1.5+) extend this to interfaces, allowing a type to have multiple sealed supertypes and enabling more flexible composable hierarchies. Both guarantee at compile time that all possible implementations are handled, improving type safety and enabling sealed class serialization. sealed classes can hold state and have constructors; sealed interfaces cannot hold state but support multiple inheritance. In when expressions, the compiler checks exhaustiveness for both, but sealed interfaces require all implementing classes to be visible. Use sealed classes for closed state hierarchies (Result, UiState). Use sealed interfaces for defining closed capability contracts across unrelated types.

Read more: https://kotlinlang.org/docs/sealed-classes.html

Q10. How do you prevent memory leaks when using coroutines in custom views or long-lived components?

Never use GlobalScope; always tie coroutines to a lifecycle-bound scope like LifecycleScope or a custom scope cancelled in onDetachedFromWindow(). For custom views, store a Job or CoroutineScope field and cancel it when the view detaches. Use LifecycleOwner extensions to auto-cancel. Avoid capturing strong references to the view in coroutine lambdas; use WeakReference or check isAttachedToWindow before updating UI. For Flow collection, use flowWithLifecycle or repeatOnLifecycle to pause collection when the lifecycle is inactive. In ViewModels, rely on viewModelScope. Use callbackFlow with awaitClose for callback-based APIs to ensure cleanup. Profile with LeakCanary and Android Studio Memory Profiler to detect retained coroutine contexts. Prefer suspendCancellableCoroutine for cancellable bridge code.

Read more: https://developer.android.com/topic/libraries/architecture/coroutines


Android Architecture & Design Patterns

Q11. Describe the evolution of your app's architecture from MVP to MVVM to MVI—what drove each transition?

MVP separated presentation logic via interfaces but created verbose boilerplate and tight coupling between Presenters and Views through contract interfaces. MVVM replaced this with data binding and ViewModel from AAC, surviving configuration changes and reducing manual view updates via LiveData/StateFlow. The transition was driven by Google’s AAC recommendations, elimination of presenter lifecycle management, and easier testing through observable state. MVI added unidirectional data flow and immutable state objects, solving the problem of scattered state mutations in complex screens where multiple LiveData sources caused inconsistent UI. We adopted MVI for screens with complex user interactions, time-travel debugging, and predictable state reduction. Each step reduced framework-specific code and increased testability. The driver was always reducing bugs from state inconsistency and improving developer velocity.

Read more: https://developer.android.com/topic/architecture

Q12. How do you enforce unidirectional data flow in a large-scale app with multiple feature modules?

Define a single State data class per screen representing the entire UI snapshot. Expose only one observable state stream (StateFlow<<State>) from the ViewModel. All user actions flow through a single onEvent() method dispatching to processors. Use a reducer pattern: (State, Event) -> State to compute new states immutably. Prevent views from mutating state directly—only emit events. Share common state via a centralized state holder or mediator if cross-feature, but keep screen states local to avoid tight coupling. Use Kotlin's copy() for immutable updates. Enforce via lint rules banning public mutable state properties in ViewModels. Document the pattern in architecture guidelines. Review PRs for state mutations outside reducers. Modularize by feature with each module owning its state, event, and reducer contracts.

Read more: https://developer.android.com/topic/architecture/ui-layer/events

Q13. What is your approach to designing a clean architecture boundary between domain and data layers?

The domain layer contains pure Kotlin UseCases and Repository interfaces with no Android dependencies, making it testable with JVM unit tests. The data layer implements repositories using Room, Retrofit, or local data sources, mapping DTOs/entities to domain models. Define Repository interfaces in domain; implement them in data. Use dependency inversion so domain never imports data layer classes. Models crossing the boundary should be immutable data classes. Use mappers (not extension functions on domain models) to convert between network/DB models and domain models to avoid leaking serialization logic. Keep domain logic framework-agnostic—no Context, no ViewModel, no coroutine dispatchers. Inject dispatchers into data layer implementations, not domain. This boundary ensures business rules survive framework changes and can be reused across platforms.

Read more: https://developer.android.com/topic/architecture/domain-layer

Q14. How do you handle navigation in a multi-module app—do you prefer the Navigation Component, custom routers, or something else?

Use the Navigation Component with a single activity architecture and feature-module graphs. Each feature module defines its own navigation.xml graph with deep links, while the app module hosts the root NavHostFragment and handles cross-feature navigation via deep link URIs or a shared navigation interface. This decouples features—they don't depend on each other's fragments directly. For complex conditional navigation (A/B flows, auth gates), wrap Navigation Component in a Navigator interface defined in a core module, implemented using the component internally. Avoid custom routers unless you need transitions unsupported by the component or shared element animations across modules. Use type-safe navigation with Kotlin DSL or Safe Args for compile-time route verification. Test navigation with FragmentScenario and Espresso.

Read more: https://developer.android.com/guide/navigation

Q15. What are the trade-offs of using a monolithic ViewModel versus splitting state into multiple smaller ViewModels?

A monolithic ViewModel centralizes state and logic for a screen, reducing inter-ViewModel communication overhead and making the full UI state visible in one place. However, it grows unwieldy for complex screens, violates single responsibility, and complicates testing. Multiple smaller ViewModels scoped to sub-screens or components improve separation of concerns and allow independent testing, but require event delegation or a shared state owner to coordinate. They increase lifecycle complexity if not aligned with the host lifecycle. For Compose, prefer smaller ViewModels tied to specific screen regions or flows, using a shared parent ViewModel for cross-cutting state. In Fragments, monolithic is often simpler due to lifecycle coupling. The trade-off is cohesion versus maintainability—split when the ViewModel exceeds ~500 lines or handles unrelated domains.

Read more: https://developer.android.com/topic/libraries/architecture/viewmodel

Q16. How do you manage shared state across features without creating tight coupling?

Introduce a shared core module containing state interfaces and lightweight data classes that all features depend on. Implement the state holder in a dedicated module or the app module, injecting it via DI. Use reactive streams (StateFlow, BroadcastChannel) for state distribution so consumers observe without knowing the producer. Avoid direct feature-to-feature imports; route communication through the app module or a mediator using deep links or a navigation interface. For global user state (auth, profile), use a UserSession repository in core that features observe. Never let Feature A directly call Feature B's ViewModel. If features must react to each other, publish domain events to a lightweight event bus scoped to the app. Keep shared state minimal—most state should be feature-local. Modular architecture with clear API boundaries prevents coupling.

Read more: https://developer.android.com/topic/modularization

Q17. Explain how you would architect a feature that needs to work both online and offline with eventual consistency.

Use a local database (Room) as the single source of truth for the UI. The repository checks connectivity: if online, fetch remote data, map to local entities, persist, then expose local data via Flow. If offline, expose cached data immediately. Write operations go to local DB first for instant UI feedback, then sync to remote via a background WorkManager task queue. Handle conflicts with timestamps, version vectors, or server-wins/last-write-wins strategy depending on business rules. Expose sync status (pending, synced, error) through the repository so UI can show indicators. Use exponential backoff for retry. Implement a sync engine that batches changes to reduce API calls. Ensure idempotency on the server for retried requests. Test with network link conditioner and airplane mode toggling.

Read more: https://developer.android.com/topic/architecture/data-layer

Q18. What strategies do you use to prevent ViewModel bloat when a screen has complex business logic?

Delegate business logic to UseCases in the domain layer, keeping ViewModels as thin orchestrators that only map UseCase outputs to UI state. Group related operations into cohesive UseCases rather than one per action. Use helper classes or state machines for complex validation or form logic. Extract Compose UI state calculation into state holders (remember or dedicated classes) when not business logic. Avoid putting navigation logic, resource strings, or Context references in ViewModels. If a screen has multiple independent regions (e.g., header, list, footer), split into nested ViewModels or state holders. Move data formatting (dates, currencies) to UI mappers or StringRes providers injected into ViewModels. Enforce a maximum line count per ViewModel in code review. Refactor proactively when cyclomatic complexity rises.

Read more: https://developer.android.com/topic/architecture/domain-layer

Q19. How do you approach error handling architecture—do you use Result types, exceptions, or domain-specific error models?

Use a sealed class Result<<out T> with Success, Error, and Loading states at the repository and UseCase boundaries. Inside the domain layer, use domain-specific error sealed classes (e.g., NetworkError, AuthError, ValidationError) rather than raw exceptions to enable exhaustive handling. Catch platform exceptions (IOException, HttpException) at the data layer and map them to domain errors. Avoid using exceptions for control flow in business logic. In ViewModels, reduce errors to UI state fields (errorMessage, isRetryable). For one-shot operations, expose Result via SharedFlow events. For streams, embed error state in the UI state object. Never propagate unhandled exceptions to the UI layer. Log unexpected errors centrally via a crash reporter interface. This approach makes error paths explicit and testable.

Read more: https://developer.android.com/topic/architecture/ui-layer

Q20. Describe how you would structure a feature module to be reusable across multiple apps in a product suite.

Build the feature as a self-contained dynamic or library module with no app-level dependencies. Define its public API as a minimal interface module (API module) containing UseCases, models, and entry points; hide implementation details in an internal module. Use dependency injection with a feature-specific component that the host app initializes. Avoid hardcoded resources—accept them via constructor or configuration objects. Provide default themes that can be overridden. Expose navigation via deep link contracts or a feature launcher interface, not direct Fragment classes. Keep networking and database schemas internal; expose only domain models. Version the module independently using semantic versioning. Document integration steps and required permissions. Test the feature in isolation using a demo app module before integrating into production apps.

Read more: https://developer.android.com/topic/modularization


Jetpack Compose & UI

Q21. How does Compose's recomposition model differ from the traditional View system's invalidate/measure/layout cycle?

Traditional Views use an imperative object tree where mutations trigger invalidate(), leading to a measure/layout/draw pass on the UI thread managed by ViewRootImpl. Compose uses a declarative function-based model where the framework automatically recomposes only functions reading changed state, skipping unaffected subtrees. Recomposition is optimistic and concurrent—Compose can execute composable functions in parallel on multiple cores, then apply changes atomically. There is no separate measure/layout phase for simple cases; layout is part of composition. State reads are tracked at the composition level via snapshots, not via listener patterns. Compose eliminates findViewById and manual view updates but requires thinking in state and side-effect boundaries. The system is more efficient for dynamic UIs but has a learning curve for custom layouts.

Read more: https://developer.android.com/jetpack/compose/mental-model

Q22. What techniques do you use to optimize recomposition in deeply nested Composable hierarchies?

Use remember to cache expensive calculations across recompositions. Hoist state to the lowest common ancestor to minimize the recomposition scope. Use derivedStateOf to prevent recompositions when derived values haven't actually changed. Pass stable types (immutable data classes, primitives) as parameters; unstable types cause unnecessary recompositions. Apply @Stable or @Immutable annotations to custom classes if they meet contracts. Use key to help Compose identify list items and skip unchanged rows. Avoid passing lambdas that capture unstable references; use remember with keys to stabilize callbacks. Use LaunchedEffect and DisposableEffect with precise keys to control side-effect granularity. Profile with Layout Inspector's recomposition counts. For large lists, use LazyColumn with content types and keys. Keep composables skippable by avoiding non-local state reads.

Read more: https://developer.android.com/jetpack/compose/lifecycle

Q23. How do you handle state hoisting when a Composable is used across multiple screens with different state requirements?

Design the Composable to accept state and event lambdas as parameters (stateless pattern), pushing state ownership to callers. Define a reusable stateless Composable like MyComponent(state: MyState, onEvent: (Event) -> Unit). For each screen, create a wrapper or use the Composable directly with screen-specific ViewModel state. If the component needs internal transient UI state (e.g., animation progress), keep it internal with remember, but expose all user-meaningful state. Use composition locals sparingly—they obscure the state source. For shared behavior, create a rememberMyComponentState() helper that returns a state holder, allowing callers to optionally hoist or use defaults. Document which state is required vs optional. This maximizes reusability while preserving testability and predictable behavior across screens.

Read more: https://developer.android.com/jetpack/compose/state-hoisting

Q24. Explain the difference between remember, rememberSaveable, and derivedStateOf with concrete use cases.

remember caches a value in Composition for the lifetime of the composable; use it for UI calculations, objects, or mutable state (remember { mutableStateOf(0) }) that should survive recompositions but not configuration changes. rememberSaveable persists state across process death and configuration changes using the saved instance state mechanism; use it for user input, scroll position, or navigation state that must survive rotation. derivedStateOf creates a state object computed from other states, but only triggers recomposition when the derived result actually changes; use it for expensive filtering or boolean flags derived from lists (e.g., derivedStateOf { items.isNotEmpty() }). Without derivedStateOf, every list change recomposes the consumer even if isNotEmpty hasn't flipped. Choose remember for ephemeral UI, rememberSaveable for process-critical UI, and derivedStateOf for computed observables.

Read more: https://developer.android.com/jetpack/compose/state

Q25. How do you integrate Compose into an existing large codebase using the legacy View system?

Adopt Compose incrementally by wrapping Composables in ComposeView within existing Fragments or Activities. Start with new screens or isolated UI components (lists, cards) rather than rewriting entire flows. Use AndroidView to embed legacy Views inside Compose when needed for gradual migration. Maintain shared ViewModels to bridge state between legacy and Compose screens. Use a common theme adapter to map existing View-based theme attributes to Compose MaterialTheme. Set isolatedFragments or use single-fragment containers to contain Compose adoption. Ensure your navigation solution supports both fragment destinations and composable destinations. Keep business logic in framework-agnostic layers so it doesn't need rewriting. Train the team with focused workshops on state hoisting and side effects to prevent anti-patterns.

Read more: https://developer.android.com/jetpack/compose/interop

Q26. What is your strategy for theming and design system adoption in Compose at scale?

Build a centralized design system module exposing MaterialTheme with custom color, typography, and shape schemes. Define tokens (design primitives) as immutable Kotlin objects generated from Figma or design specs. Avoid hardcoding colors or dimensions in feature modules—require all UI to consume theme values. Use composition locals for semantic colors (e.g., primaryContainer, onSurfaceVariant) rather than literal hex values. Create reusable component composables (buttons, chips) in the design system module that enforce accessibility and interaction specs. Use PreviewParameterProvider to preview components in all theme variants (light, dark, dynamic). Automate design token generation with a code generator to prevent drift. Enforce via lint or code review that feature modules don't import androidx.compose.ui.graphics.Color directly for literals. Document the system with a catalog app.

Read more: https://developer.android.com/jetpack/compose/themes

Q27. How do you test Composables that rely on LaunchedEffect or DisposableEffect?

Use createComposeRule() from androidx.compose.ui:ui-test-junit4 to set the Composition's CoroutineContext to a TestDispatcher you control. For LaunchedEffect, advance time or run pending coroutines via testDispatcher.scheduler.runCurrent() or advanceUntilIdle(). Mock or stub dependencies injected into the composable so effects don't hit real APIs. For DisposableEffect, verify cleanup by disposing the composition via composeTestRule.disposeComposition() or navigating away. Use CompositionLocalProvider to override dependencies in tests. Test the side effect's outcome (state change, callback invocation) rather than the effect itself. For time-based effects, use kotlinx-coroutines-test to advance virtual time. Keep effects small and delegate logic to testable suspend functions or classes. Avoid testing the framework; test your code inside the effect.

Read more: https://developer.android.com/jetpack/compose/testing

Q28. What are the pitfalls of using Modifier chains extensively, and how do you avoid them?

Excessive Modifier chaining creates deeply nested lambda allocations and can hurt readability. Reusing modifiers via remember or extracting them to variables reduces allocation overhead. Order matters significantly—padding before background yields different visual results than after, causing subtle UI bugs. Avoid conditional modifier application that changes the chain structure; use then() or conditional values within a single chain. Don't pass large modifier objects through many layers—apply them at the leaf composables. Using Modifier as a parameter for every custom composable is good practice, but document whether the modifier replaces or appends to internal modifiers. Performance-wise, prefer Modifier over custom Layout when possible, but profile if chains exceed ~10 modifiers. Extract reusable patterns into extension functions for consistency.

Read more: https://developer.android.com/jetpack/compose/modifiers

Q29. How do you handle animations in Compose while maintaining 60fps on low-end devices?

Use animate*AsState for simple value animations and AnimatedVisibility/AnimatedContent for container transitions—these are optimized by the framework. Avoid animating large lists or complex layouts simultaneously. Use LazyColumn item animations sparingly. Prefer Modifier.graphicsLayer for transformations (scale, alpha, rotation) since it renders off the main thread on supported devices. Minimize allocations inside animation frames by hoisting animatables and remembering update lambdas. Use snapTo for instant state changes that don't need interpolation. Profile with Android Studio's GPU profiler and Macrobenchmark to identify dropped frames. Reduce animation complexity based on device capabilities using WindowInsets or system settings (reduced motion). Test on physical low-end devices, not just emulators. Use SideEffect or DerivedState to precompute animation targets.

Read more: https://developer.android.com/jetpack/compose/animation

Q30. Describe your approach to building accessible Composables—what tools and testing practices do you enforce?

Use semantic properties like contentDescription, heading(), and stateDescription to communicate meaning to TalkBack. Ensure touch targets are at least 48dp using minimumInteractiveComponentSize. Use Modifier.semantics to merge or clear child semantics when a group acts as a single component. Test with TalkBack enabled on physical devices, navigating via swipe gestures. Use the Accessibility Scanner tool to catch missing labels or small touch targets. Write Compose UI tests that query semantics nodes (onNodeWithContentDescription, onNodeWithText) rather than implementation details. Support keyboard navigation with focusable() and onKeyEvent. Respect AccessibilityManager.isEnabled to adapt behavior if needed, but don't remove functionality. Test color contrast ratios against WCAG guidelines. Document accessibility requirements in component specs and block PRs that fail scanner checks.

Read more: https://developer.android.com/jetpack/compose/accessibility


Performance & Optimization

Q31. How do you diagnose and fix ANRs in production, not just during development?

Integrate Firebase Performance Monitoring or custom ANR watchdog libraries to capture stack traces and main thread states from production. Analyze the traces.txt or data/anr logs via Play Console or Crashlytics to identify the blocked thread. Look for long-running operations on the main thread: database queries on UI thread, heavy bitmap decoding, JSON parsing, or lock contention. Use StrictMode in debug builds to catch disk/network access early. Fix by moving work to background threads with coroutines (Dispatchers.IO), using AsyncLayoutInflater for heavy layouts, or breaking transactions into smaller chunks. If the ANR is in system code (Binder timeout), reduce IPC calls or batch them. Profile with Systrace to visualize thread blocking. Communicate fixes through staged rollouts to verify resolution.

Read more: https://developer.android.com/topic/performance/vitals/anr

Q32. What is your process for investigating memory leaks using the Memory Profiler and Heap Dumps?

Capture a heap dump in Android Studio after reproducing the suspected leak path. Convert the HPROF file using hprof-conv if needed, then analyze with the Memory Profiler or Eclipse MAT. Look for retained sizes of Activity or Fragment classes; if instances survive after onDestroy, there's a leak. Use dominator trees to find what keeps them alive—common culprits are listeners, anonymous inner classes, or static fields holding Views. Check for ViewModel leaks via retained Job references. Use LeakCanary in debug builds for automatic leak detection with reference chain analysis. In production, use androidx.metrics or Firebase Performance to track heap growth trends. Fix by removing static references, using WeakReference, cancelling coroutines in lifecycle teardown, or unregistering listeners. Re-dump after fixes to confirm retained count drops to zero.

Read more: https://developer.android.com/studio/profile/memory-profiler

Q33. How do you optimize RecyclerView scrolling performance when dealing with heterogeneous view types and images?

Use RecycledViewPool to share view holders across multiple RecyclerViews if applicable. Ensure onCreateViewHolder and onBindViewHolder do minimal work—no data formatting or image decoding inside bind. Use Glide or Coil with appropriate resize()/override() targets to load scaled bitmaps matching view dimensions. Enable setHasFixedSize(true) when the adapter content doesn't change RecyclerView's own size. For heterogeneous types, ensure view type integers are stable and don't create excessive holder variations. Use setItemViewCacheSize for preloading offscreen items. Move heavy image loading to background threads via the image library's built-in threading. Avoid nested RecyclerViews or use ConcatAdapter instead. Profile with GPU rendering profile bars to identify jank. Use AsyncListDiffer for efficient diffing and animations without full dataset refreshes.

Read more: https://developer.android.com/topic/performance/rendering

Q34. What strategies do you use to reduce APK size in a multi-module app with heavy native dependencies?

Enable R8 full mode and ProGuard with aggressive shrinking and obfuscation. Strip debug symbols from native libraries using android:extractNativeLibs="true" and android:useLegacyPackaging="false" in Gradle 3.6+. Use Android App Bundles (AAB) to deliver only required ABI splits to devices. Move heavy assets to dynamic feature modules or download at runtime via Play Asset Delivery. Compress images to WebP (lossy or lossless) and use vector drawables for icons. Audit dependencies with ./gradlew app:dependencies to remove unused libraries or replace heavy ones with lighter alternatives. Use resConfigs to strip unused language resources. Enable code shrinking per module. For native code, build separate .so files per ABI rather than universal binaries. Use android:allowBackup="false" cautiously, but primarily focus on asset optimization and dynamic delivery.

Read more: https://developer.android.com/topic/performance/reduce-apk-size

Q35. How do you benchmark app startup time, and what specific optimizations have you implemented?

Use the Jetpack Macrobenchmark library with StartupTimingMetric to measure cold, warm, and hot startup times consistently. Identify bottlenecks via method tracing and Systrace during Application.onCreate and first Activity launch. Optimizations include: lazy-initializing DI graph using lazy or provider patterns rather than eager singletons. Moving heavy WorkManager initialization to background. Using ContentProvider initialization sparingly—libraries like Firebase often add hidden providers; remove or defer them. Implementing splash screens via SplashScreen API to mask loading. Preloading critical classes with AppComponentFactory or Baseline Profiles. Reducing onCreate layout complexity by inflating only the initial visible portion. A/B testing startup improvements via staged rollouts. Measuring real-user startup times via Firebase Performance Monitoring.

Read more: https://developer.android.com/topic/performance/benchmarking/macrobenchmark-overview

Q36. Explain how you would optimize battery consumption for an app using location services in the background.

Use the Fused Location Provider with balanced power accuracy (PRIORITY_BALANCED_POWER_ACCURACY) rather than high accuracy unless necessary. Batch location updates by setting the smallest displacement and interval margins appropriately. Use the Passive provider when the app doesn't need to actively request but can receive updates triggered by other apps. Implement geofencing via GeofencingClient instead of polling location for region detection. Move location work to a foreground service only when the user explicitly expects tracking; otherwise use WorkManager with constraints. Avoid wake locks; let the system schedule work in Doze-friendly windows. Use BroadcastReceiver for significant motion detection to start location updates only when the user is moving. Cache and deduplicate location data before network transmission. Test battery drain with Android Studio's Energy Profiler.

Read more: https://developer.android.com/training/location

Q37. What are the trade-offs between using ProGuard, R8, and DexGuard in a commercial app?

ProGuard is the legacy shrinker/obfuscator with broad community rules but slower build times and no D8 integration; Google deprecated it in favor of R8. R8 is the default Android shrinker, combining shrinking, desugaring, obfuscating, and dexing in one step for faster builds and smaller DEX output. It supports ProGuard rules but has slightly different optimization semantics that can cause subtle runtime bugs if rules are incomplete. DexGuard is a commercial extension of ProGuard with stronger string encryption, reflection replacement, asset encryption, and runtime application self-protection (RASP) for anti-tampering. Trade-offs: R8 is free, fast, and officially supported but offers basic obfuscation. DexGuard adds security layers valuable for finance/DRM apps but increases build complexity and cost. For most apps, R8 with proper rules is sufficient. Choose DexGuard only if reverse engineering is a critical business threat and you need runtime protection beyond obfuscation.

Read more: https://developer.android.com/studio/build/shrink-code

Q38. How do you handle large bitmaps without running into OutOfMemoryError?

Load bitmaps via BitmapFactory.Options with inJustDecodeBounds=true to read dimensions first, then calculate inSampleSize to downscale to view dimensions. Use Glide or Coil, which handle subsampling and caching automatically. Store Bitmaps in BitmapPool (Glide) for reuse rather than allocating new ones per image. Use inBitmap on Android 3.0+ to reuse memory from recycled bitmaps. Prefer RGB_565 over ARGB_8888 for opaque images to halve memory usage. Never hold bitmap references in static fields or long-lived objects. Recycle bitmaps manually only when not using a managed library. For very large images (maps, documents), use tile-based rendering with BitmapRegionDecoder or libraries like Subsampling Scale Image View. Monitor heap size with Runtime.getRuntime().maxMemory() before loading unknown-size images.

Read more: https://developer.android.com/topic/performance/graphics

Q39. What tools do you use to profile jank and frame drops, and what are the most common causes you've fixed?

Use Systrace/Android Studio CPU Profiler to capture traces and identify frames exceeding 16ms (60fps). Enable GPU rendering profile bars on device to visualize frame times in real time. Use Jetpack Macrobenchmark with FrameTimingMetric for automated regression detection. Common causes: overdraw from redundant backgrounds (fix by removing unnecessary android:background and using clipToPadding="false" wisely), nested LinearLayouts causing excessive measure passes (replace with ConstraintLayout), heavy work on main thread during scroll (move to background), bitmap decoding without resizing, frequent garbage collection from object churn (use object pools), and unnecessary requestLayout calls triggering full view hierarchy remeasure. Complex shadows and elevation also increase render thread workload. Fixing usually involves flattening layouts, caching calculations, and offloading I/O.

Read more: https://developer.android.com/topic/performance/rendering

Q40. How do you optimize Room database queries for screens that display complex relational data?

Use @Relation with @Embedded and @Transaction for one-to-many fetches, but be aware they run as separate queries wrapped in a transaction. For complex joins, write custom @Query with SQLite JOINs to fetch flattened data in a single query rather than multiple entity lookups. Use database views (@DatabaseView) to precompute complex relational results. Add indices on foreign keys and frequently filtered columns to avoid full table scans. Use LIMIT and OFFSET (or Room's Paging 3 integration) for large datasets rather than loading all rows. Select only needed columns instead of SELECT *. Use Flow queries for reactive updates but ensure they emit only when relevant tables change. Profile slow queries with EXPLAIN QUERY PLAN in SQLite. Avoid nesting transactions. Use RoomDatabase.Builder.setQueryCallback in debug to log and analyze query execution times.

Read more: https://developer.android.com/training/data-storage/room


Networking, Data & Storage

Q41. How do you design a network layer that gracefully handles flaky connectivity and retries?

Use OkHttp with an Interceptor that detects network errors and applies exponential backoff retries via RetryInterceptor. Implement a NetworkMonitor using ConnectivityManager to queue requests offline and flush when online. Use Retrofit with coroutines and wrap calls in Result types. Apply Resilience4j or a custom circuit breaker pattern to stop hammering failing endpoints. Use WorkManager for guaranteed eventual delivery of mutations. Set reasonable timeouts (connectTimeout, readTimeout, writeTimeout) rather than defaults. Cache GET responses with Cache-Control headers and OkHttp cache for stale-while-revalidate behavior. Provide user feedback via UI state (skeletons, offline banners) rather than silent failures. Log network quality metrics to analytics. Test with Charles Proxy throttling and airplane mode toggling.

Read more: https://developer.android.com/training/basics/network-ops

Q42. What is your strategy for caching API responses—do you prefer OkHttp cache, Room, or a custom solution?

Use OkHttp cache for simple HTTP-level caching of GET requests with standard cache headers; it's fast and requires no code changes beyond interceptor setup. Use Room for structured caching where you need relational queries, offline-first architecture, or cache invalidation logic tied to business rules rather than HTTP semantics. Use a custom in-memory LRU cache (via LruCache) for ephemeral session data or computed results. In practice, combine all three: OkHttp for network cache, Room for offline source-of-truth, and LRU for UI-level memoization. Define cache policies per endpoint—immutable reference data gets long OkHttp cache, user-specific data gets Room with timestamp-based invalidation. Avoid caching sensitive data in OkHttp cache unless encrypted. Use Cache-Control: max-age and ETag for efficient revalidation. Document cache boundaries to prevent stale data bugs.

Read more: https://developer.android.com/topic/performance/network

Q43. How do you handle API versioning and backward compatibility when the backend ships breaking changes? Use API versioning in URL paths (/v1/, /v2/) or headers (Accept: application/vnd.v2+json). Maintain separate Retrofit service interfaces per version. Map responses to domain models in the data layer so version changes don't leak into UI. Use Moshi polymorphic adapters or JsonQualifier to handle field renaming or type changes gracefully. Ship app updates with migration logic that handles both old and new payloads during transition windows. Use feature flags to toggle between API versions remotely without app releases. Implement fallback defaults for missing fields to prevent NullPointerException. Communicate deprecation schedules with backend teams. Use integration tests against mock servers with both schema versions. Never parse raw JSON in ViewModels—always isolate version handling in repository mappers.

Read more: https://developer.android.com/training/basics/network-ops/connecting

Q44. Explain your approach to syncing local database state with remote server state in a conflict-prone environment. Use a sync queue table in Room tracking local changes with status (PENDING, SYNCED, CONFLICT). Apply optimistic UI updates by writing locally first, then enqueue a WorkManager task to push to server. On pull, fetch server state, compare version vectors or modified_at timestamps, and apply a conflict resolution strategy (last-write-wins, server-wins, or custom merge). Flag conflicts in the UI for user resolution when automatic merge fails. Use UUIDs for primary keys to prevent collision during offline creation. Batch sync operations to reduce API calls. Implement exponential backoff with jitter for retry. Maintain a sync_token or ETag for incremental sync. Test conflict scenarios with multiple devices offline then online. Log sync metrics to detect drift. Ensure the server API supports idempotent updates for retried requests.

Read more: https://developer.android.com/topic/architecture/data-layer

Q45. How do you manage API keys and sensitive configuration in a way that resists reverse engineering? Never hardcode API keys in BuildConfig or XML resources; they appear as plain strings in APK/AAB. Store keys in native code (JNI/C++) compiled to .so files, which raises the reverse engineering barrier though doesn't prevent it. Use remote configuration (Firebase Remote Config or backend endpoint) to deliver keys at runtime, reducing static exposure. Implement certificate pinning to prevent MITM key interception. Rotate keys frequently and support revocation via remote kill switches. Use OAuth 2.0 with short-lived access tokens and refresh tokens stored in EncryptedSharedPreferences or Keystore. For high-security apps, use white-box cryptography or hardware security modules. Apply R8/DexGuard obfuscation to string literals. Monitor API key usage server-side for anomalous patterns. Defense is layered—no single method is sufficient.

Read more: https://developer.android.com/training/articles/security-key-attestation

Q46. What is your experience with GraphQL on Android, and how does it compare to REST in production? GraphQL reduces over-fetching and under-fetching by letting the client specify exact fields, which is efficient for mobile bandwidth. Apollo Kotlin generates type-safe models and coroutine-based APIs from schemas, ensuring compile-time contract validation. However, it adds complexity: caching is client-managed (normalized cache) rather than simple HTTP caching, error handling includes partial data scenarios, and file uploads require multipart extensions. In production, REST is simpler for teams, has better tooling, and leverages existing HTTP infrastructure. GraphQL shines when backend is microservices-based or when mobile needs vastly different data shapes than web. Use persisted queries to prevent arbitrary query attacks and improve performance. Monitor query complexity server-side. For Android, Apollo's normalized cache and watchers provide reactive UI updates comparable to Room + Flow but with GraphQL semantics.

Read more: https://www.apollographql.com/docs/kotlin/

Q47. How do you implement pagination at the data layer—Paging 3, custom solutions, or a hybrid? Use Jetpack Paging 3 for standard list pagination with PagingSource backed by Room or network. It handles loading states, error boundaries, and RecyclerView integration via PagingDataAdapter. For complex cases (bi-directional pagination, non-list UIs, or custom caching), extend PagingSource or use RemoteMediator for network+database combined sources. Custom solutions are acceptable when Paging 3's assumptions (linear list, single source) don't fit—e.g., paginated grids with complex headers or pagination within nested structures. Hybrid approaches use Paging 3 for the main list but custom Flow-based state machines for auxiliary data. Always expose LoadState to UI for skeletons and retry. Use cachedIn(viewModelScope) to survive configuration changes. Avoid PagingSource for small static lists where complexity outweighs benefit.

Read more: https://developer.android.com/topic/libraries/architecture/paging

Q48. Describe how you would migrate a production database schema using Room without data loss. Use Room's Migration classes implementing migrate() with SQLite ALTER TABLE and INSERT INTO ... SELECT for structural changes. For destructive migrations that preserve data, create a new table with the desired schema, copy data from the old table via SQL, drop the old table, and rename the new one. Version migrations sequentially—never skip versions in production. Test migrations with Room's MigrationTestHelper on an actual database file from the previous app version. Ship migrations in debug builds to team members using old versions before release. For complex transformations, use RoomDatabase.Callback or AutoMigration (Room 2.4+) with @DeleteTable, @RenameTable, @DeleteColumn annotations to reduce boilerplate. Always back up user data to cloud or export before risky migrations. Provide a fallback strategy if migration fails (clear and re-sync).

Read more: https://developer.android.com/training/data-storage/room/migrating

Q49. How do you handle large JSON payloads efficiently without blocking the UI thread? Parse JSON on Dispatchers.IO using coroutines or Retrofit's built-in suspend support which already offloads to background threads. Use streaming JSON parsers (Moshi's JsonReader, Jackson streaming API) for payloads exceeding memory limits instead of loading entire DOM trees. Use Retrofit with @Streaming for raw response bodies processed line-by-line. For Room inserts of large parsed datasets, batch insertions in chunks (e.g., 500 rows per transaction) to avoid long SQLite locks. Expose parsing progress via Flow if the UI needs a progress bar. Avoid toString() on massive JSON for logging. Use gzip compression at the server level. Consider Protocol Buffers or FlatBuffers for structured data that doesn't need human readability—they parse faster and allocate less. Profile parsing with Android Studio CPU profiler.

Read more: https://developer.android.com/training/basics/network-ops/xml

Q50. What is your approach to data serialization—Kotlinx Serialization, Moshi, Gson, or Protobuf? Why? Prefer Kotlinx Serialization for Kotlin-first projects—it integrates with language features (sealed classes, null safety, default arguments) without reflection, supports Multiplatform, and generates code at compile time. Use Moshi for Java interoperability or when working with legacy codebases; its Kotlin codegen is reflection-free and fast. Avoid Gson in new projects—it uses reflection, ignores Kotlin null safety, and doesn't support default values reliably. Use Protobuf (Wire or protobuf-lite) for internal high-performance APIs where payload size and parsing speed matter more than human readability. For REST APIs with complex polymorphism, Moshi's polymorphic adapters are mature. For GraphQL, Apollo generates models automatically. The choice depends on team expertise, backend contract format, and performance requirements. Standardize on one per project to avoid dependency bloat.

Read more: https://kotlinlang.org/docs/serialization.html


Testing & Quality Assurance

Q51. What is your testing pyramid for an Android app, and what coverage thresholds do you enforce? The pyramid base is unit tests (70% coverage target) for ViewModels, UseCases, repositories, and mappers using JUnit5 and MockK/Mockito. The middle layer is integration tests (20%) for database DAOs, repository boundaries, and DI graph verification with Hilt testing. The top is UI/E2E tests (10%) using Espresso or Compose UI tests for critical user journeys (login, checkout). Enforce 80% line coverage on domain layer classes via JaCoCo or Kover; allow lower coverage on UI layer since visual tests supplement it. Run unit tests on every PR; integration tests on merge; E2E tests nightly. Use fakes over mocks for repository tests to validate real behavior. Exclude generated code, Dagger components, and data classes from coverage metrics. Block PRs that drop coverage below thresholds. Track flaky test rates and disable/repair tests that fail intermittently.

Read more: https://developer.android.com/training/testing

Q52. How do you write effective UI tests for flows that depend on biometric authentication or hardware sensors? Mock biometric results using BiometricPrompt test APIs or wrapper interfaces that can be faked in test builds. For hardware sensors (GPS, accelerometer), inject sensor managers via abstractions and provide fake implementations that emit controlled values during tests. Use Hilt to swap real dependencies for test doubles in @HiltAndroidTest scenarios. Avoid testing the actual biometric hardware—test your app's reaction to success/failure callbacks. For camera flows, use FakeCameraDevice or mock the camera repository. Structure tests around states: given biometric success, when user triggers action, then expected state occurs. Use IdlingResources to wait for asynchronous biometric callbacks. Run these tests on emulators with simulated fingerprint rather than physical devices to ensure consistency. Keep sensor-dependent tests in a separate test suite that runs on CI with emulator configurations.

Read more: https://developer.android.com/training/testing/integration-testing

Q53. What is your strategy for testing ViewModels that interact with multiple UseCases and Repositories? Inject all dependencies via constructor to enable easy mocking or faking. Use TestDispatcher (via Dispatchers.setMain) to control coroutine timing. Test state transitions by collecting StateFlow values with Turbine or testIn(backgroundScope). Verify that each user event triggers the correct UseCase invocation with expected parameters. Mock UseCase outputs to simulate success, error, and loading paths. Don't test the internal logic of UseCases in ViewModel tests—that belongs in UseCase unit tests. Verify navigation events or one-shot effects via SharedFlow collection. Use UnconfinedTestDispatcher for immediate execution or StandardTestDispatcher for fine-grained control. Reset Dispatchers.Main in @After to prevent test pollution. Keep ViewModel tests focused on orchestration, not business rule validation.

Read more: https://developer.android.com/topic/libraries/architecture/coroutines

Q54. How do you handle flaky Espresso tests in CI, and what alternatives have you evaluated? Flakiness usually stems from timing issues, animations, or unhandled system dialogs. Disable animations via adb shell settings put global window_animation_scale 0 in CI. Use IdlingResource to synchronize with background work rather than Thread.sleep(). Handle system dialogs (runtime permissions, Google Play popups) with UIAutomator or by mocking permission states. Run tests on consistent emulator snapshots with fixed API levels and hardware profiles. Evaluate alternatives: Compose UI tests are more deterministic due to synchronization with the UI thread. Use Firebase Test Lab or AWS Device Farm for broader device coverage and isolation. For CI stability, shard tests across multiple emulator instances and retry failed shards once before marking failure. Track flakiness metrics per test and quarantine consistently flaky tests for repair. Consider screenshot testing for visual regression as a complement to interaction tests.

Read more: https://developer.android.com/training/testing/espresso

Q55. What mocking framework do you prefer—MockK, Mockito-Kotlin, or fakes—and why? Prefer fakes (lightweight in-memory implementations) for repository and data source tests because they validate real integration behavior and don't break when internal implementation details change. Use MockK for Kotlin-specific features like coroutines, extension functions, and sealed classes where fakes are impractical; its DSL is idiomatic Kotlin. Use Mockito-Kotlin only in legacy Java-interoperable codebases where MockK adoption is too disruptive. Fakes shine for Room databases (in-memory Room.inMemoryDatabaseBuilder), network (MockWebServer), and shared preferences. Mocks are acceptable for external SDKs or complex objects with many methods where fake implementation is burdensome. The rule: if the dependency has behavior worth verifying (caching, query logic), use a fake. If it's a simple callback boundary, use a mock. MockK's relaxed mocks and coEvery make coroutine testing ergonomic.

Read more: https://mockk.io/

Q56. How do you test coroutine-based code that involves Dispatchers.IO or Dispatchers.Main? Never hardcode dispatchers in production code; inject them via constructor or use Dispatchers.Default as a default parameter. In tests, inject StandardTestDispatcher or UnconfinedTestDispatcher to control execution. Use Dispatchers.setMain(testDispatcher) in a JUnit @Before rule and Dispatchers.resetMain() in @After. For Dispatchers.IO, replace it with the test dispatcher so I/O-bound code runs immediately and deterministically. Use advanceUntilIdle() or runCurrent() from kotlinx-coroutines-test to pump the scheduler. Test timing-sensitive code by advancing virtual time with advanceTimeBy(). Avoid runBlocking in tests—it masks real async behavior. Use runTest from kotlinx-coroutines-test which provides a test scope and handles uncaught exceptions. Verify that coroutines are cancelled properly by checking job state or side effect cleanup.

Read more: https://kotlinlang.org/docs/coroutines-test.html

Q57. Describe your approach to screenshot testing and how you prevent visual regressions. Use Paparazzi or Shot to render Composables/Views in a JVM environment without physical devices or emulators, capturing bitmaps for comparison. Store reference screenshots in version control. Run diffs in CI on every PR; fail builds when pixel differences exceed a threshold. Focus screenshot tests on design system components and critical screens rather than every UI state to avoid maintenance burden. Handle dynamic data by mocking ViewModels with fixed states. Account for locale, font scale, and dark mode variations using parameterized tests. Review diffs manually when intentional design changes occur, updating references via a CI command or local task. Prevent false positives by disabling animations and using deterministic layouts. Integrate with Pull Request comments to display before/after images for designer review. Separate screenshot tests from functional tests for faster feedback.

Read more: https://developer.android.com/studio/test/gradle-managed-devices

Q58. How do you enforce code quality standards across a team of developers with varying skill levels? Automate enforcement via Detekt, KtLint, and Android Lint with custom rules integrated into CI; reject PRs with warnings treated as errors. Provide a comprehensive CONTRIBUTING.md and architecture decision records (ADRs) documenting patterns. Use code review checklists focusing on architecture, testing, and performance rather than style (which linters handle). Pair programming and mob reviews for complex features spread knowledge. Maintain a "golden path" sample module demonstrating approved patterns. Run static analysis on every commit with pre-commit hooks. Track technical debt in a backlog with estimated impact. Conduct regular architecture katas or refactoring dojos. Measure code quality metrics (cyclomatic complexity, test coverage, lint violations) in dashboards. Make standards educational rather than punitive—explain why rules exist in lint rule documentation.

Read more: https://developer.android.com/studio/write/lint

Q59. What is your process for conducting thorough code reviews on architectural changes? Require an ADR (Architecture Decision Record) or design doc before review, explaining context, alternatives, and trade-offs. Review in two passes: first for correctness and architecture alignment, second for nitpicks. Verify that new code follows established module boundaries and doesn't introduce circular dependencies. Check test coverage for new logic and question untested edge cases. Ensure state management follows unidirectional flow and lifecycle rules. Validate error handling paths and loading states. Review for performance: unnecessary allocations, main thread work, memory leaks. Verify that public APIs are minimal and well-documented. Use GitHub/GitLab review threads for substantive discussions, resolving before merge. For large changes, request a walkthrough meeting. Block merges on CI passing and at least two approvals for core architecture files. Follow up post-merge to validate metrics and crash rates.

Read more: https://developer.android.com/studio/write/lint

Q60. How do you test edge cases in background work like WorkManager tasks or foreground services? Use WorkManagerTestInitHelper to initialize a test driver that allows synchronous execution and observation of WorkInfo states. Test constraints (network, battery) by configuring the test driver to simulate constraint satisfaction. For CoroutineWorker, inject TestDispatcher and verify that doWork() returns Result.success(), failure(), or retry() under different conditions. Test retry policies by verifying BackoffPolicy and run attempts. For foreground services, test the service lifecycle with ServiceTestRule or Robolectric service tests. Mock system APIs (alarm manager, job scheduler) to test scheduling logic. Verify that work chaining (beginWith().then()) executes in correct order by observing output data propagation. Test idempotency by running the same work twice and verifying no duplicate side effects. Use TestListenableWorkerBuilder to instantiate workers directly in JVM tests.

Read more: https://developer.android.com/topic/libraries/architecture/workmanager


Security

Q61. How do you securely store OAuth tokens and refresh tokens on Android? Store tokens in EncryptedSharedPreferences (AES-256 encryption with keys backed by Android Keystore) for standard apps. For higher security, use the Keystore directly to store keys that encrypt a local database or file containing tokens. Never store tokens in plain SharedPreferences, external storage, or Logcat. Implement automatic token refresh with exponential backoff before expiry. Clear tokens securely on logout by overwriting memory before deletion and clearing EncryptedSharedPreferences. Use AccountManager only if integrating with system accounts; it's deprecated for general use. Bind token access to biometric authentication for high-sensitivity apps using BiometricPrompt + CryptoObject. Limit token scope to minimum required. Monitor for token leakage via certificate pinning and integrity checks. Rotate refresh tokens on every use if the backend supports it.

Read more: https://developer.android.com/training/articles/keystore

Q62. What is your approach to certificate pinning, and how do you handle certificate rotation? Pin the intermediate CA certificate rather than leaf certificates to reduce fragility during server certificate rotation. Use OkHttp's CertificatePinner with backup pins (pin multiple certificates or CAs) so the app continues working when the primary certificate changes. Implement a reporting mechanism to detect pin failures in production without crashing the app initially—use a "report-only" mode. Rotate certificates well before expiry and update app pins via remote config or app updates with overlap periods. For emergency rotation, have an out-of-band update channel (Firebase Remote Config) to disable pinning temporarily. Test pinning with tools like mitmproxy to ensure it blocks untrusted certificates. Document the certificate rotation schedule with DevOps. Use HPKP-like strategies adapted for mobile: short pin lifetimes and graceful degradation.

Read more: https://developer.android.com/training/articles/security-config

Q63. How do you protect against reverse engineering and tampering in a high-risk app? Apply multiple layers: R8/DexGuard obfuscation and string encryption to deter static analysis. Move critical logic to native code (JNI) compiled with obfuscation tools like O-LLVM. Implement root detection and emulator detection using SafetyNet/Play Integrity API to block compromised devices. Verify app signature and integrity at runtime using PackageManager and custom checks. Encrypt sensitive assets and configuration files. Use anti-debugging techniques (detecting ptrace, timing checks) in native code. Implement certificate pinning to prevent MITM-based dynamic analysis. Obfuscate network protocols beyond standard HTTPS. Use white-box cryptography for key protection. Monitor for tampering via server-side anomaly detection (unexpected API sequences, impossible travel). No layer is foolproof; defense in depth raises the cost of attack.

Read more: https://developer.android.com/google/play/integrity

Q64. Explain how Android Keystore System works and when you would use it over EncryptedSharedPreferences. The Keystore System generates and stores cryptographic keys in hardware-backed secure storage (TEE or StrongBox) if available, isolating keys from the app process. Keys can require user authentication (biometric/PIN) for each use via setUserAuthenticationRequired(true). Use Keystore directly when you need hardware-backed key protection, key attestation, or per-use authentication. Use EncryptedSharedPreferences for simpler encrypted storage of small strings (tokens, PII) where the encryption key is Keystore-backed but the data lives in preferences. Keystore is lower-level and requires more boilerplate for encrypt/decrypt operations. For high-value keys (payment keys, identity keys), use Keystore with attestation. For general sensitive app data, EncryptedSharedPreferences is sufficient and more ergonomic. StrongBox Keystore (dedicated secure hardware) is preferred on devices that support it (API 28+).

Read more: https://developer.android.com/training/articles/keystore

Q65. What measures do you take to prevent intent hijacking and exported component vulnerabilities? Set android:exported="false" explicitly on all components that don't need external access. For exported components (deep link activities, services), enforce permission checks with android:permission and validate caller identity via getCallingActivity() or checkCallingPermission(). Use explicit intents internally rather than implicit intents to prevent interception. Validate all incoming intent data (URIs, extras) before processing; reject malformed or unexpected schemes. For ContentProvider exports, use path permissions and grant URI permissions temporarily with FLAG_GRANT_READ_URI_PERMISSION. Remove debuggable exported components before release. Use android:protectionLevel="signature" for custom permissions between your apps. Audit the manifest with aapt or static analysis tools for accidental exports. Test with malicious intent fuzzing via Drozer or custom scripts.

Read more: https://developer.android.com/topic/security/risks/pending-intent

Q66. How do you handle root detection and emulator detection in financial or healthcare apps? Use Play Integrity API (modern replacement for SafetyNet) to attest device integrity and app legitimacy server-side. Check for root indicators: presence of su binary, busybox, Magisk files, or writable /system paths. Detect emulators via hardware fingerprinting (CPU info, device ID consistency, sensor availability, QEMU traces). However, root detection is an arms race—combine client-side checks with server-side behavioral analysis. If root is detected, don't immediately crash; degrade functionality (disable sensitive features) or require additional authentication. Obfuscate detection logic to hinder bypassing. Use native code for checks to raise the bar. Validate integrity tokens on your server, not client-side. For healthcare/finance, comply with regulatory requirements (PCI-DSS, HIPAA) which may mandate specific tamper resistance. Document your risk acceptance for rooted devices.

Read more: https://developer.android.com/google/play/integrity

Q67. What is your strategy for obfuscating sensitive business logic beyond standard R8 rules? Move critical algorithms to native libraries compiled with obfuscation and stripping to hide symbols. Use control flow flattening and string encryption via DexGuard or similar commercial tools. Split logic across multiple classes and modules to increase reconstruction difficulty. Use reflection sparingly as it complicates obfuscation but can be used to dynamically load classes. Implement server-side computation for the most sensitive logic (e.g., pricing algorithms, proprietary calculations) so the client is just a display layer. Use JNI to bridge Java/Kotlin to obfuscated C++ code. Apply name obfuscation to package structures. Remove logging and debug symbols from release builds. Use dynamic code loading (DEX from server) cautiously as it triggers Play Protect warnings. Combine obfuscation with runtime integrity checks that verify method signatures haven't been tampered.

Read more: https://developer.android.com/studio/build/shrink-code

Q68. How do you implement secure inter-process communication between your app and a SDK or partner app? Use AIDL with custom permissions (android:permission requiring signature-level protection) so only your signed apps can bind the service. Validate the calling package name and signature in onBind() before returning the binder. Use Messenger instead of raw AIDL for simpler message-based IPC with built-in Handler threading. Encrypt payloads passed via IPC with keys exchanged through a secure channel (Keystore-backed). Avoid passing file descriptors or sensitive URIs to untrusted processes. Use ContentProvider with path permissions and temporary URI grants for data sharing. For high-security scenarios, use android:isolatedProcess="true" for sandboxed service execution. Document the IPC surface area and threat model. Use explicit service intents to prevent binding hijacking. Regularly audit IPC entry points with static analysis.

Read more: https://developer.android.com/guide/components/aidl

Q69. What are the risks of using WebView for sensitive flows, and how do you mitigate them? WebView runs in the app's process with extensive attack surface: JavaScript injection, XSS, SSL error handling bypasses, and file access vulnerabilities. Mitigate by disabling JavaScript unless required (setJavaScriptEnabled(false)). Use addJavascriptInterface only with @JavascriptInterface and validate all inputs; better yet, use WebMessage for communication. Handle onReceivedSslError properly—don't auto-proceed on errors. Restrict content to HTTPS only (setMixedContentMode(MIXED_CONTENT_NEVER_ALLOW)). Prevent file access with setAllowFileAccess(false). Use a separate process for WebView (android:process=":webview_process") to isolate crashes and memory leaks. Keep WebView updated via Google Play Services WebView implementation. Use WebViewAssetLoader for local content instead of file:// URLs. Monitor for known CVEs in the WebView component. For highly sensitive flows, prefer native implementation over WebView.

Read more: https://developer.android.com/develop/ui/views/layout/webapps

Q70. How do you ensure compliance with GDPR or CCPA data handling requirements in your Android app? Implement a consent management platform (CMP) that captures user consent before initializing analytics or advertising SDKs. Provide in-app privacy settings allowing users to view, export, and delete their data. Minimize data collection to what's necessary (data minimization). Use encryption for stored personal data and secure transmission (TLS 1.2+). Anonymize or pseudonymize identifiers where possible. Maintain a data processing inventory documenting what each SDK collects. Support "right to be forgotten" by clearing local data and invoking backend deletion APIs. Display a privacy policy before account creation. Use Firebase Remote Config or backend flags to toggle tracking SDKs off for users who deny consent. Audit third-party libraries for unauthorized data transmission. Document lawful basis for processing. Conduct privacy impact assessments for new features.

Read more: https://developer.android.com/privacy-and-security


Background Processing & System Integration

Q71. How do you choose between WorkManager, Foreground Service, AlarmManager, and JobScheduler for a given task? Use WorkManager for deferrable, guaranteed background work that needs to survive reboots and app restarts (sync, uploads). Use Foreground Service only for user-initiated long-running tasks that need immediate execution and a persistent notification (music playback, active navigation). Use AlarmManager (or AlarmManagerPlus with exact alarm permissions) only for time-critical alarms or reminders where WorkManager's flex periods are insufficient—be aware of Android 12+ exact alarm restrictions. JobScheduler is the system API underlying WorkManager; use it directly only if you need scheduling features WorkManager doesn't expose (API 21+). For immediate execution of important work, use Expedited Work in WorkManager (API 31+). Never use AlarmManager for polling; use WorkManager with constraints. Match the API to user expectations and system resource policies.

Read more: https://developer.android.com/topic/libraries/architecture/workmanager

Q72. What are the implications of Android 12+ restrictions on foreground services and exact alarms? Android 12 (API 31) requires FOREGROUND_SERVICE permission and restricts background app starts; foreground services must show a notification immediately and fall under specific use-case types (location, media, etc.). Apps targeting API 33+ need USE_EXACT_ALARM permission or SCHEDULE_EXACT_ALARM with user-granted alarm capability; the latter can be revoked by the user or system. These restrictions prevent apps from waking the device excessively and improve battery life. You must declare service types in the manifest (android:foregroundServiceType). For exact alarms, prefer inexact alarms or WorkManager unless the use case genuinely requires precision (alarm clock, calendar reminder). Handle ACTION_SCHEDULE_EXACT_ALARM_PERMISSION_STATE_CHANGED to react to permission revocation. Test behavior changes on API 31+ emulators and devices. Document service justifications for Play Store review.

Read more: https://developer.android.com/about/versions/12/behavior-changes-12

Q73. How do you implement reliable push notification delivery using FCM, especially for time-sensitive data? Use FCM high priority messages (priority: high) for time-sensitive data, but sparingly—abuse leads to app standby bucket demotion. Include collapse keys to replace pending notifications with updated content. Implement a local notification queue: if FCM is received while the app is in the background or killed, use a FirebaseMessagingService to process data payloads and trigger local notifications immediately. For guaranteed delivery, persist the message in Room upon receipt and show a notification; sync with server when online. Handle token rotation by updating your backend registration on onNewToken. Test delivery with Firebase Console and topic broadcasts. Monitor delivery receipts and BigQuery exports to diagnose drop-offs. Use notification channels with appropriate importance levels. Respect Do Not Disturb and notification permission states (Android 13+).

Read more: https://firebase.google.com/docs/cloud-messaging

Q74. Describe how you would design a background sync engine that respects Doze mode and App Standby. Use WorkManager with network and battery constraints to schedule syncs during maintenance windows when the device exits Doze. Implement exponential backoff for retries (BackoffPolicy.EXPONENTIAL) to avoid waking the device repeatedly. Batch sync operations into a single periodic work request rather than multiple separate jobs. Use setRequiresBatteryNotLow(true) and setRequiresCharging(true) for heavy syncs to defer until optimal conditions. For high-priority syncs, use expedited work (API 31+) which runs immediately if constraints are met. Listen for connectivity changes via ConnectivityManager to trigger opportunistic syncs when the device is active. Maintain a local queue of pending changes; process it during sync windows. Avoid holding wake locks—let the system manage execution. Test with adb shell dumpsys deviceidle to force Doze states and verify deferred execution.

Read more: https://developer.android.com/training/monitoring-device-state/doze-standby

Q75. How do you handle Bluetooth LE scanning and connections while managing battery impact? Use the BluetoothLeScanner with ScanSettings set to SCAN_MODE_LOW_LATENCY only during active user interaction; switch to SCAN_MODE_LOW_POWER for background monitoring. Filter scan results with ScanFilter by device name or service UUID to reduce processing. Batch scan results and process them off the main thread. Use PendingIntent-based scans (API 26+) for background scanning without keeping a service running. Connect using autoConnect=false for faster direct connections when the device is known to be available; use autoConnect=true for opportunistic reconnection. Disconnect and close BluetoothGatt instances promptly when not needed to free native resources. Avoid continuous scanning—scan in intervals with rest periods. Use BluetoothAdapter.getLeMaximumAdvertisingDataLength() to optimize payload sizes. Monitor battery drain via Android Studio profiler during extended BLE sessions.

Read more: https://developer.android.com/guide/topics/connectivity/bluetooth

Q76. What is your experience with Android's Camera2/CameraX APIs, and how do you handle device-specific quirks? CameraX is the modern abstraction over Camera2, providing use-case-based APIs (Preview, ImageAnalysis, ImageCapture) with automatic lifecycle management and device compatibility fixes. Use CameraX for most apps; drop to Camera2 only when you need manual sensor control, RAW capture, or burst modes unsupported by CameraX. Device quirks include: aspect ratio distortions, flash timing issues, and orientation bugs on Samsung and Xiaomi devices. Handle them by testing on a broad device lab (top 20 market devices) and using CameraX's CameraSelector with ExtensionMode for vendor features. For Camera2, maintain a compatibility matrix mapping device models to workaround parameters (preview sizes, flash modes). Use CameraCharacteristics to query hardware capabilities before configuring sessions. Always handle onError callbacks and recreate the camera session gracefully. Use Executor for callbacks to avoid blocking the camera thread.

Read more: https://developer.android.com/training/camerax

Q77. How do you implement deep linking that works consistently across notification taps, widgets, and external apps? Define deep links in the Navigation Component or manifest intent filters with android:scheme and android:host patterns. Use a single Activity as the entry point with NavHost handling routing, or a trampoline activity that validates and dispatches URIs. For notifications, use PendingIntent with NavDeepLinkBuilder or explicit task stack builders to preserve back stack. For widgets, use PendingIntent with setAction matching the deep link URI. For external apps, document the URI contract and validate incoming paths in onCreate before navigation. Handle invalid or malformed URIs gracefully with a fallback destination. Use android:autoVerify="true" for App Links to ensure domain verification. Test deep links via adb shell am start -W -a android.intent.action.VIEW -d "yourscheme://host/path". Ensure the same URI produces identical behavior regardless of entry point.

Read more: https://developer.android.com/training/app-links

Q78. What strategies do you use for handling media playback in the background with proper audio focus? Use MediaSession and MediaController from Jetpack Media3 for consistent playback architecture across foreground services and UI. Request audio focus via AudioManager.requestAudioFocus() or AudioFocusRequest (API 26+) with AUDIOFOCUS_GAIN and ducking behavior appropriate to your content type. Handle focus loss by pausing playback and releasing resources temporarily. Implement a foreground service with foregroundServiceType="mediaPlayback" and a persistent notification showing media controls. Use MediaBrowserServiceCompat to enable playback from Auto, Wear, and assistant surfaces. Manage audio focus changes and noisy intent (headphone disconnect) via BroadcastReceiver or AudioManager callbacks. Use ExoPlayer for adaptive streaming and background playback consistency. Test audio focus behavior with other media apps (YouTube, Spotify) to verify ducking and resume. Ensure proper notification media controls using MediaStyle notifications.

Read more: https://developer.android.com/guide/topics/media

Q79. How do you manage permissions across Android versions, especially for location, notifications, and Bluetooth? Use the Activity Result API (registerForActivityResult) for modern permission requests rather than onRequestPermissionsResult. For location, handle foreground (ACCESS_FINE_LOCATION) and background (ACCESS_BACKGROUND_LOCATION) permissions separately; request background only after foreground is granted, as per Play Store policy. For notifications, request POST_NOTIFICATIONS at runtime on Android 13+ using the same result API. For Bluetooth, on Android 12+ replace BLUETOOTH and BLUETOOTH_ADMIN with runtime permissions BLUETOOTH_SCAN, BLUETOOTH_CONNECT, and BLUETOOTH_ADVERTISE. Abstract permission logic into a reusable PermissionManager or PermissionHandler that checks API levels and permission states. Show rationale UI explaining why permission is needed before system dialogs. Gracefully degrade features when permissions are denied with "Don't ask again." Test permission flows on API 21, 33, and latest devices. Use shouldShowRequestPermissionRationale() to detect permanent denial.

Read more: https://developer.android.com/training/permissions

Q80. Describe your approach to implementing widgets and ensuring they update reliably without draining battery. Use Glance (Jetpack) for modern widget development with Compose-like declarative API, reducing boilerplate and improving reliability over traditional RemoteViews. Update widgets via WorkManager periodic work or AlarmManager with exactness scaled to importance, rather than frequent manual updates. Use updatePeriodMillis only for coarse updates (minimum 30 minutes); for real-time data, use setUpdateContent or push updates via AppWidgetManager when data changes. Implement onEnabled and onDisabled callbacks to register and unregister update observers. Use ListView or GridView with RemoteViewsFactory for collection widgets, caching data in onDataSetChanged. Avoid network calls directly in widget update methods; preload data into a cache or database. Respect battery saver mode by reducing update frequency. Test widget behavior during Doze and App Standby. Provide preview layouts and configure options for user customization.

Read more: https://developer.android.com/jetpack/glance


Build System, CI/CD & Tooling

Q81. How do you structure a Gradle build to support multiple build variants, flavors, and feature modules efficiently? Use the com.android.application plugin in the app module and com.android.library in feature modules. Define productFlavors for dimensions like environment (dev, staging, prod) and distribution (play, enterprise). Use buildTypes (debug, release) with initWith to share configuration. Place shared build logic in buildSrc or convention plugins (com.android.application convention script) to avoid duplication. Use api and implementation dependencies carefully—feature modules should expose minimal APIs. Configure sourceSets per flavor for environment-specific resources and BuildConfigField values. Use androidComponents DSL (AGP 7.0+) for variant-aware artifact manipulation. Keep build.gradle.kts declarative; move complex logic to custom plugins. Use Gradle's configuration cache and build cache to speed up variant builds. Document flavor dimensions in the project README.

Read more: https://developer.android.com/studio/build

Q82. What is your strategy for managing dependency versions across a large multi-module project? Use a centralized version catalog (gradle/libs.versions.toml) with TOML format to declare versions, libraries, and plugins. Reference them consistently across modules via implementation(libs.retrofit) syntax. For shared platform dependencies, use a platform() or enforcedPlatform() BOM (Bill of Materials) like compose-bom to align transitive versions. Define a buildSrc or convention plugin that applies common dependencies to all modules automatically. Pin critical dependency versions to avoid transitive upgrades breaking builds. Use Dependabot, Renovate, or custom scripts to automate version update PRs. Review changelogs for breaking changes before merging updates. Separate production and test dependency versions. Avoid + dynamic versions in production builds. Document the update cadence (e.g., monthly dependency refresh) and rollback procedures.

Read more: https://developer.android.com/build/migrate-to-catalogs

Q83. How do you optimize Gradle build times for a team of 10+ developers? Enable Gradle build cache (org.gradle.caching=true) and configure a remote build cache node (Gradle Enterprise or self-hosted) so CI and local builds share outputs. Enable configuration cache (org.gradle.configuration-cache=true) to skip configuration phase on subsequent builds. Increase heap size (org.gradle.jvmargs=-Xmx8g). Use modularization to enable parallel compilation and selective module builds (--parallel). Avoid heavy computation in build.gradle.kts during configuration; use lazy task APIs. Replace kapt with KSP for annotation processing (Room, Moshi, Hilt) for faster processing. Use R8 in full mode for release but disable for debug if incremental dexing is slower. Profile builds with Gradle Build Scan to identify bottlenecks (slow tasks, dependency resolution). Standardize JDK versions across the team. Use assembleDebug rather than build for daily development. Keep local.properties machine-specific settings out of version control.

Read more: https://developer.android.com/studio/build/optimize-your-build

Q84. Describe your CI/CD pipeline—what checks run on PR, merge, and release builds? On PR: run unit tests, KtLint, Detekt, Android Lint, and compile checks (assembleDebug) within 10 minutes. Require passing status checks before merge. On merge to main: run integration tests (Hilt, Room, network fakes), screenshot tests, and generate coverage reports. Store artifacts. On release tag: run full E2E tests on Firebase Test Lab across device matrix, security scans (MobSF), ProGuard/R8 validation, and sign the APK/AAB with CI-managed keystore. Upload to Google Play Internal Testing track automatically. Run automated smoke tests via orchestrator on staged rollout. Use GitHub Actions, GitLab CI, or Bitrise with Docker images containing Android SDK and emulator snapshots. Parallelize test suites across runners. Notify Slack/Teams on failures. Maintain a rollback script to revert staged releases. Gate production promotion on crash-free rate metrics.

Read more: https://developer.android.com/studio/build/building-cmdline

Q85. How do you automate app distribution to internal testers, beta users, and production? Use the Google Play Publishing API or Gradle Play Publisher plugin to upload AABs directly from CI to Play Console tracks (internal, alpha, beta, production). For internal testers, distribute via Firebase App Distribution with tester groups and release notes generated from commit messages. Automate versioning (versionCode from CI build number, versionName from Git tags). Use Fastlane for complex workflows (screenshots, metadata updates, phased releases) if Play API isn't sufficient. For enterprise distribution, host APKs on internal MDM or use private Google Play channels. Require QA sign-off before beta promotion via manual CI job triggers. Track rollout percentage programmatically and halt on crash threshold breaches. Use GitHub releases or GitLab packages for artifact archival. Automate Slack notifications to tester channels with download links. Maintain separate signing keys per track for security isolation.

Read more: https://developer.android.com/studio/publish

Q86. What is your approach to versioning and release management in a fast-paced delivery cycle? Use semantic versioning (MAJOR.MINOR.PATCH) where MAJOR indicates breaking changes, MINOR features, PATCH fixes. Automate versionCode as monotonically increasing integer (CI build number or Git commit count). Tag releases in Git with vX.Y.Z to trigger release pipelines. Maintain a CHANGELOG.md enforced via PR templates. Use trunk-based development with short-lived feature flags rather than long release branches. Release weekly or bi-weekly with automated canary (1%) → staged (10% → 50%) → full rollout via Play Console API. Hotfix critical bugs by cherry-picking to a release branch and fast-tracking through internal testing. Coordinate backend API versioning with mobile releases. Use feature flags to decouple app store releases from feature launches. Monitor crash rates and ANRs via Firebase for 24 hours before increasing rollout percentage. Document rollback procedures.

Read more: https://developer.android.com/studio/publish

Q87. How do you handle build failures caused by transitive dependency conflicts? Use ./gradlew app:dependencies --configuration implementation to visualize the dependency tree and identify conflicting versions. Force resolution via resolutionStrategy in build.gradle.kts: force("org.json:json:20231013") or substitute rules. Use strictly in version catalogs to enforce exact versions. Exclude conflicting transitive modules with exclude(group, module) when they're pulled in by multiple libraries. Use dependencyInsight to trace why a specific version is selected. Align versions using BOMs (e.g., compose-bom, kotlinx-coroutines-bom). For native dependencies (.so files), ensure only one module packages the library to avoid runtime linkage errors. Document resolved conflicts in a DEPENDENCIES.md file. Run ./gradlew buildHealth with the Dependency Analysis plugin to detect unused dependencies that might cause conflicts. Test thoroughly after forcing versions to ensure compatibility.

Read more: https://developer.android.com/studio/build/dependencies

Q88. What static analysis tools do you enforce—Detekt, KtLint, Android Lint, custom rules? Enforce KtLint for formatting (trailing commas, indentation, import ordering) integrated via ktlint-gradle plugin with autoCorrect on CI. Use Detekt for code smell detection (complexity, magic numbers, coroutine usage) with a custom config file (detekt.yml) tuned to team standards. Use Android Lint for framework-specific issues (missing translations, unsafe Intent usage, performance bugs). Write custom Detekt rules for architecture violations (e.g., "ViewModels must not import android.content.Context") and custom Lint rules for project-specific patterns. Run all three on every PR via GitHub Actions; block merge on failures. Generate HTML/SARIF reports for review. Suppress false positives with annotations (@Suppress) requiring PR justification. Track trend lines (violations over time) in CI dashboards. Run detektBaseline only temporarily during large refactorings, not as a permanent escape hatch.

Read more: https://developer.android.com/studio/write/lint

Q89. How do you manage secrets and signing configurations in CI without exposing them in repository history? Store signing keystores as base64-encoded secrets in CI environment variables (GitHub Secrets, GitLab CI/CD Variables, or AWS Secrets Manager) rather than in the repo. Inject them during CI by decoding into temporary files before the build step, then delete immediately after signing. Use local.properties for local development secrets (ignored by Git) and a stub file (local.properties.example) for documentation. For Google Play API access, use JSON key files stored as CI secrets. Rotate secrets regularly and audit access logs. Use Gradle's secrets-gradle-plugin to inject API keys into BuildConfig at build time from environment variables, not hardcoded. Enable branch protection so only authorized users can run release workflows. Scan repository history with git-secrets or TruffleHog to ensure no credentials were committed accidentally. Use short-lived tokens where possible.

Read more: https://developer.android.com/studio/publish/app-signing

Q90. Describe how you would set up a modular build system where features can be developed and tested in isolation. Structure the project with app, core, feature:*, and test modules. Each feature module contains api (public interface) and implementation (internal code) submodules or uses internal visibility to hide internals. Feature modules depend only on core and other feature api modules, never on implementation details. Provide a demo application module per feature (:feature:login:demo) that launches just that feature's entry point for isolated development and testing. Use Hilt's TestInstallIn to replace dependencies in feature tests. Define module boundaries via Gradle's implementation vs api strictly. Use the Dependency Analysis Gradle Plugin to detect leaks across boundaries. Set up CI to build and test only changed modules and their dependents (--changed-projects) for speed. Document module dependency rules in ARCHITECTURE.md. Enforce via custom Detekt rules or Gradle dependency checks.

Read more: https://developer.android.com/topic/modularization


Leadership, Team & Process

Q91. How do you onboard a junior developer and bring them up to speed on your team's architecture? Provide a structured onboarding checklist spanning environment setup, codebase walkthroughs, and architecture documentation. Pair them with a buddy mentor for the first month. Start with small, well-scoped tickets touching one layer (e.g., a UI mapper or a UseCase) to build confidence. Walk through the architecture diagram explaining data flow from UI to network and back. Have them shadow code reviews before submitting their own. Assign a "first feature" that spans multiple layers but is low-risk, with frequent check-ins. Share recorded architecture deep-dive sessions. Encourage questions in a dedicated Slack channel. Review their first PRs with extensive educational comments rather than just corrections. Set up a local sandbox where they can experiment without production consequences. Measure progress by their ability to independently complete a vertical slice feature within 6-8 weeks.

Read more: https://developer.android.com/topic/architecture

Q92. Describe a time you had to push back on a product requirement due to technical constraints—how did you handle it? When product requested real-time collaborative editing with offline support for a note-taking feature, I explained the complexity of operational transformation (OT) algorithms, conflict resolution, and the 6-month engineering timeline. I presented alternatives: optimistic locking with last-write-wins for MVP, delivering in 6 weeks, then iterating toward true collaboration. I framed the discussion around user value versus engineering cost, showing data that 90% of users don't need simultaneous editing. I proposed a phased roadmap with clear milestones and success metrics. I involved engineering leadership to align on trade-offs. We agreed to the simplified MVP with a commitment to revisit real-time collaboration if user feedback demanded it. The key was providing solutions, not just objections, and speaking in product's language (user impact, timeline, risk).

Read more: https://developer.android.com/topic/product-management

Q93. How do you balance shipping features quickly with maintaining long-term code health? Allocate 20% of sprint capacity to refactoring, test coverage, and technical debt reduction alongside feature work. Require that every feature PR includes tests and documentation updates. Use feature flags to ship incomplete features safely without blocking the codebase. Enforce architecture standards via code review so shortcuts don't accumulate. Track technical debt in a visible backlog with business impact estimates. When under pressure, accept tactical debt consciously—document it with a TODO and a ticket. Automate quality checks (lint, coverage) so they don't slow down developers. Break large features into incremental PRs that improve the codebase as they go (e.g., refactoring before adding logic). Measure code churn and complexity trends; intervene when metrics degrade. Communicate to stakeholders that sustained velocity requires maintenance investment.

Read more: https://developer.android.com/topic/architecture

Q94. What is your approach to resolving technical disagreements within the team? Facilitate a decision-making framework: define the problem, list options with pros/cons, and evaluate against agreed criteria (performance, maintainability, timeline, team knowledge). If opinions are split, request a time-boxed spike (1-2 days) where proponents build minimal prototypes to validate assumptions. Use architecture decision records (ADRs) to document the outcome and rationale, so dissent is recorded respectfully. As a lead, I don't dictate but guide the team toward consensus; if consensus is impossible, I make the call and take responsibility. Ensure the decision is reversible if new information emerges. Follow up post-implementation to validate the choice. Keep discussions technical, not personal. If a disagreement stems from knowledge gaps, arrange a learning session. Document patterns in team wiki to prevent recurring debates.

Read more: https://developer.android.com/topic/architecture

Q95. How do you mentor developers who are resistant to adopting new patterns like Compose or coroutines? Start with empathy—understand if resistance stems from fear, past bad experiences, or workload pressure. Demonstrate value with side-by-side comparisons: show how coroutines reduce callback hell or how Compose previews speed up UI iteration. Pair program on a small feature using the new pattern, letting them drive while you navigate. Provide internal workshops with hands-on labs, not just presentations. Create a "safe" feature branch or demo module for experimentation without production risk. Share success metrics from early adopters (fewer bugs, faster delivery). Avoid mandating immediate wholesale adoption; allow gradual migration with hybrid approaches. Address specific concerns directly (e.g., "Will this break our existing tests?" → show testing strategy). Recognize and celebrate first successes publicly. If fundamental skill gaps exist, fund external training or conference attendance.

Read more: https://developer.android.com/jetpack/compose

Q96. Describe how you would plan the migration of a legacy Java codebase to Kotlin while keeping the app shippable. Migrate incrementally file-by-file, starting with test files and data classes to build team confidence. Use Android Studio's automatic Java-to-Kotlin converter for boilerplate, then manually refine idioms. Establish a "Kotlin-first" rule: new code must be Kotlin; old Java is migrated when touched. Configure Gradle with apply plugin: 'kotlin-android' and interop settings. Ensure CI runs both Java and Kotlin tests without disruption. Migrate layer by layer: data/models first (safest), then repositories, then ViewModels, finally UI. Maintain binary compatibility—Kotlin compiles to JVM bytecode interoperable with Java. Use @JvmName and @JvmStatic annotations where Java callers need specific signatures. Run static analysis on both languages. Don't rewrite working logic unnecessarily; focus on classes with high churn or bug rates. Time-box migration work (e.g., 2 files per sprint). Celebrate milestones to maintain morale.

Read more: https://developer.android.com/kotlin

Q97. How do you ensure knowledge is distributed and no single developer is a bottleneck? Enforce pair programming or mob reviews for critical architectural changes so at least two people understand every major system. Require code review approval from someone outside the author's usual domain to spread familiarity. Maintain runbooks for deployment, incident response, and environment setup. Rotate on-call responsibilities and feature ownership every quarter. Document architecture decisions (ADRs) and complex algorithms in the team wiki. Conduct regular "lunch and learn" sessions where developers present their recent work. Avoid assigning the same person to the same feature area repeatedly; deliberately mix assignments. Use a shared Slack channel for questions rather than DMs so answers are visible. Mentor junior developers explicitly toward ownership roles. If a bottleneck emerges, immediately pair the expert with another developer on the next related task.

Read more: https://developer.android.com/topic/architecture

Q98. What is your process for evaluating new libraries or architectural patterns before team-wide adoption? Define evaluation criteria: maintenance status, community size, license compatibility, APK size impact, build time cost, and learning curve. Build a prototype in a feature branch or demo app integrating the library with our stack (DI, networking, threading). Measure performance benchmarks and binary size delta with APK Analyzer. Audit the library's dependencies for transitive bloat or security risks. Check GitHub issues for unresolved critical bugs and release cadence. Require two team members to review the prototype code and documentation. Run a team discussion or RFC (Request for Comments) document collecting concerns. If approved, migrate one low-risk feature first as a pilot. Monitor crash rates and build stability for one release cycle before broader rollout. Document the decision in an ADR. Maintain a policy to revisit evaluations annually; deprecate libraries that become unmaintained.

Read more: https://developer.android.com/jetpack

Q99. How do you communicate technical debt and its impact to non-technical stakeholders? Translate technical debt into business metrics they understand: "This legacy module causes 30% of our production crashes, affecting user retention" or "Refactoring the checkout flow will reduce page load time by 2 seconds, increasing conversion by X%." Use visual dashboards showing crash rates, build times, or deployment frequency trends. Frame debt as risk: "If we don't update this library, we lose Play Store compliance in 3 months." Propose concrete trade-offs: "We can ship Feature A in 2 weeks with shortcuts, or 4 weeks with proper foundation that enables Features B and C." Avoid jargon; use analogies like "paying interest on a loan." Include debt items in product roadmaps with estimated user impact. Secure dedicated sprint time (20% rule) by showing historical data that teams with zero maintenance time eventually halt entirely.

Read more: https://developer.android.com/topic/product-management

Q100. How do you handle a critical production crash when the root cause is unclear and pressure is high? First, mitigate user impact: use feature flags or remote config to disable the crashing feature immediately if possible. Roll back the release via Play Console if the crash is tied to a recent update. Gather data: analyze Crashlytics stack traces, device models, OS versions, and user actions leading to the crash. Reproduce on the exact device/OS combination if available. If unclear, add targeted logging or use a custom UncaughtExceptionHandler to capture more context in a hotfix. Form a war room with relevant engineers (Android, backend, QA) to parallelize investigation. Communicate transparently to stakeholders: "We are investigating, feature X is disabled, ETA for fix is Y hours." Avoid guessing fixes; validate hypotheses with reproduction. Once fixed, write a postmortem documenting root cause, detection lag, and prevention measures. Prioritize adding automated tests for the failure path.

Read more: https://developer.android.com/topic/performance


System Design & Cross-Functional

Q101. Design a real-time chat feature that supports offline messaging, media sharing, and end-to-end encryption. Use WebSocket for real-time message delivery with automatic reconnection and heartbeat pings. Queue outgoing messages in Room with status PENDING; sync via WorkManager when offline. For media, upload to encrypted object storage (S3 with SSE) generating pre-signed URLs; share URL + decryption key in the message payload. Implement E2E encryption using the Signal Protocol or libsodium with per-session keys exchanged via X3DH. Store private keys in Android Keystore. Use MessagePack or protobuf for compact wire format. Display messages via Paging 3 from local DB as source of truth. Handle read receipts and typing indicators as lightweight ephemeral events. Use WorkManager for guaranteed media upload retry. Implement key rotation periodically. Backup encrypted message history to user-controlled cloud storage. Test with multiple devices and airplane mode toggling.

Read more: https://developer.android.com/guide/topics/connectivity

Q102. How would you architect an app that needs to support both phone and tablet form factors with shared code? Use a single activity with Navigation Component and responsive layouts via WindowSizeClass (Compose) or resource qualifiers (sw600dp, sw720dp). Define a WindowState object computed from WindowMetrics that drives UI layout decisions (single pane vs dual pane). Use SlidingPaneLayout or Compose NavSuiteScaffold for list-detail patterns. Share all ViewModels and business logic; only the UI layer adapts to screen size. Use LazyVerticalGrid or LazyStaggeredGrid for content that reflows. Implement foldable support with WindowInfoTracker to detect posture (flat, half-opened). Test on emulators with varying screen sizes and physical tablets. Avoid separate phone/tablet modules—use responsive composables and alternate layout files. Use ConstraintLayout or BoxWithConstraints for precise adaptive positioning. Maintain one navigation graph with conditional destinations based on form factor.

Read more: https://developer.android.com/guide/topics/large-screens

Q103. Design a navigation and routing system for an app with 50+ screens and deep linking requirements. Use the Navigation Component with nested navigation graphs per feature module. Define a core-navigation module containing route constants and deep link URI contracts as sealed classes. Each feature module exposes its graph via a NavigationNode interface registered in the app module's NavHost. Use type-safe navigation with Kotlin DSL generated routes to prevent stringly-typed errors. Handle deep links by defining URI patterns in each graph; use a trampoline activity to validate authentication or feature flags before routing. For conditional flows (onboarding, paywalls), use a Navigator interface that intercepts destinations and redirects if conditions aren't met. Maintain a back stack policy per graph (e.g., auth graph pops to root on success). Use BottomNavigationView or NavigationRail with MultipleBackStacks to preserve state across tabs. Document the routing map in a Mermaid diagram.

Read more: https://developer.android.com/guide/navigation

Q104. How would you build a SDK that third-party developers integrate into their apps—what constraints matter most? Minimize transitive dependencies to avoid version conflicts with host apps; shade or repackage critical libraries if necessary. Keep the public API surface small and stable—every public class is a contract you can't break without major version bumps. Use semantic versioning strictly. Provide clear integration documentation with code samples and a demo app. Support minSdk that aligns with market data but don't lag too far behind. Handle lifecycle automatically using LifecycleObserver rather than requiring manual init/shutdown. Use weak references to host Activity/Context to prevent memory leaks. Respect the host app's theme and threading model; never assume main thread availability for callbacks. Provide ProGuard/R8 consumer rules in the AAR. Include an opt-out analytics mechanism. Test in popular host apps (conflict scenarios). Offer Maven Central distribution with POM metadata.

Read more: https://developer.android.com/studio/projects/android-library

Q105. Describe the architecture for an app that streams video with adaptive bitrate and offline download support. Use ExoPlayer with DashMediaSource or HlsMediaSource for adaptive streaming via DASH/HLS manifests. Implement TrackSelector to choose streams based on bandwidth meter (DefaultBandwidthMeter) and device capabilities. For offline, use ExoPlayer's DownloadManager with DownloadService to cache segments in a SimpleCache backed by CacheDataSource. Expose download progress via DownloadManager.Listener to UI. Use WorkManager to schedule large downloads during charging/WiFi. Store DRM licenses (Widevine) via OfflineLicenseHelper for protected content. Use MediaSession for background playback and integration with Android Auto/Wear. Implement a DataSource factory that switches between network and cache transparently. Handle network switches by re-evaluating tracks. Provide quality selection UI overriding automatic selection. Test adaptive behavior with network link conditioner throttling bandwidth.

Read more: https://developer.android.com/media

Q106. How would you design a feature flag system that works across Android, iOS, and backend? Use a centralized feature flag platform (LaunchDarkly, Firebase Remote Config, or custom) with REST/gRPC APIs serving flag configurations. Define flags in a shared schema (JSON or protobuf) consumed by all platforms. Cache flag values locally with TTL to ensure offline functionality. Use flag evaluation in the domain layer, not UI, to keep logic consistent. Implement user targeting (percentage rollout, user segments, device attributes) server-side. Provide a debug panel in internal builds to override flags for QA. Use consistent naming conventions (feature.checkout_v2_enabled). Ensure flags are type-safe by generating platform-specific accessors from the shared schema. Implement kill switches that disable features without app updates. Audit flag usage to remove stale flags after rollout completion. Coordinate backend API versioning with flag states to prevent mismatched behavior.

Read more: https://developer.android.com/topic/architecture

Q107. Design a local-first architecture where the app remains fully functional without network for days. Use Room as the single source of truth with a sync layer abstracted behind repository interfaces. Implement CRDTs (Conflict-free Replicated Data Types) or operational transformation for collaborative data, or use timestamp-based last-write-wins for single-user data. Queue all mutations in a pending_changes table with WorkManager constraints (network required) for background sync. Optimistically apply changes to UI immediately. Use Flows from Room to automatically update UI when local data changes. Implement incremental sync with sync_tokens to minimize data transfer when reconnecting. Handle conflict resolution automatically with server-wins for critical data and client-wins for user preferences. Provide visual indicators (sync status icons) for pending changes. Cap local storage to prevent unbounded growth; archive old data. Test by enabling airplane mode for extended periods and verifying all features work.

Read more: https://developer.android.com/topic/architecture/data-layer

Q108. How would you approach building a white-label Android app framework used by multiple brands? Create a core module containing all business logic, networking, and architecture; expose theming and configuration via an interface. Each brand module depends on core and provides brand-specific resources (colors, logos, fonts), API endpoints, and feature toggles via productFlavors or separate application modules. Use Gradle build variants to generate distinct APKs/AABs per brand from shared code. Inject brand configuration at runtime via a BrandConfiguration object loaded from assets or metadata. Use AndroidManifest placeholders for app name, icon, and authorities. Keep brand-specific code minimal—ideally just resources and a configuration class. Use resValue and buildConfigField for compile-time brand constants. Provide a white-label SDK approach if brands need to integrate into their own apps. Maintain a brand matrix CI job that builds all variants. Document customization boundaries to prevent core code divergence.

Read more: https://developer.android.com/studio/build

Q109. Describe how you would design an analytics pipeline that respects user privacy while providing product insights. Collect only anonymized event data with no PII; use hashed device IDs rather than advertising IDs where possible. Implement consent management requiring explicit opt-in before initializing analytics SDKs. Batch events locally and transmit over HTTPS with certificate pinning. Provide an in-app privacy dashboard showing collected data categories and allowing deletion. Use differential privacy techniques or aggregation to prevent individual user identification in reports. Store events in a local queue (Room) and flush via WorkManager to respect battery and network constraints. Support "do not track" signals and regional regulations (GDPR, CCPA). Audit third-party analytics libraries for unauthorized data collection. Use first-party analytics infrastructure (self-hosted) instead of third-party when privacy is paramount. Document data retention policies and auto-delete old local logs. Make analytics sampling configurable to reduce data volume.

Read more: https://developer.android.com/privacy-and-security

Q110. How would you architect an Android app that integrates with a complex existing backend built for web-first? Create an anti-corruption layer (adapter pattern) in the data layer that maps web-centric DTOs (deep nesting, snake_case, generic wrappers) to mobile-friendly domain models (flat, camelCase, typed). Use mappers to transform paginated web responses into Paging 3 sources. Handle web-specific auth (cookies, CSRF tokens) by storing them in CookieJar and injecting headers via OkHttp interceptors. Adapt web error formats (HTML error pages, non-JSON) into domain exceptions. If the backend sends large payloads, implement field filtering or separate mobile-optimized endpoints via BFF (Backend-for-Frontend) pattern. Cache aggressively on mobile to compensate for web-optimized API chattiness. Use GraphQL as an intermediary if the backend supports it, allowing mobile to request precise data shapes. Maintain API contract tests to detect backend changes early. Document mobile-specific requirements for backend teams.

Read more: https://developer.android.com/topic/architecture/data-layer


Emerging Tech & Future-Proofing

Q111. What is your assessment of Kotlin Multiplatform for sharing code between Android and iOS in production? KMP is production-ready for business logic (domain models, UseCases, validation) but still maturing for UI sharing (Compose Multiplatform). Share commonMain modules for networking, serialization, and data repositories using Ktor and kotlinx.serialization. Expect platform-specific implementations (actual declarations) for keychain (iOS) vs Keystore (Android), local storage (Room vs CoreData wrappers), and platform channels. Build times and IDE support have improved significantly but require Gradle expertise. The main risk is team composition—iOS developers must be comfortable with Kotlin and Gradle. For teams with strong native iOS UI skills, share only domain layer. For teams wanting maximum reuse, Compose Multiplatform offers shared UI but with native look-and-feel trade-offs. Evaluate based on team size, release cadence parity, and whether the shared logic is complex enough to justify the abstraction overhead.

Read more: https://kotlinlang.org/docs/multiplatform.html

Q112. How do you evaluate the maturity of Jetpack Compose for replacing an entire existing UI layer? Compose is mature for new projects and incremental adoption; full replacement depends on app complexity and team readiness. Evaluate: 1) Library ecosystem—are all critical dependencies (maps, charts, video players) available as Compose-friendly APIs or wrappers? 2) Performance—profile recomposition on low-end devices with your specific UI complexity. 3) Team expertise—ensure developers understand state hoisting and side effects. 4) Accessibility—verify TalkBack and keyboard navigation meet requirements. 5) Testing—confirm screenshot and UI test infrastructure supports Compose. For established apps, migrate screen-by-screen starting with new features or simple settings screens. Maintain hybrid architecture with ComposeView and AndroidView interop during transition. Full replacement is viable if the app is medium-sized, the team is trained, and the release cycle allows a 3-6 month migration window with feature freeze on legacy UI.

Read more: https://developer.android.com/jetpack/compose

Q113. What is your understanding of Android's declarative UI evolution beyond Compose—like Glance for widgets? Glance applies Compose's declarative paradigm to App Widgets, generating RemoteViews under the hood while exposing a type-safe Kotlin API. It simplifies widget development and reduces boilerplate compared to RemoteViews. Beyond widgets, Android is exploring declarative system UI and large-screen adaptations via WindowSizeClass and adaptive layouts. The trend is toward state-driven UI where the framework handles rendering differences across form factors. Wear OS and TV are adopting Compose-based APIs (WearCompose, Compose for TV). The evolution suggests a unified declarative toolkit across surfaces, though each maintains surface-specific constraints (widgets lack interactivity, Wear has circular layouts). Developers should expect to write business logic once and adapt UI declarations per surface. Invest in learning Compose fundamentals as they transfer across these emerging APIs.

Read more: https://developer.android.com/jetpack/glance

Q114. How do you stay current with Android platform changes and decide when to adopt new APIs? Follow official channels: Android Developers Blog, Android Developers YouTube, Google I/O sessions, and the AndroidX release notes. Subscribe to the "Now in Android" newsletter and the AOSP issue tracker for deep technical changes. Evaluate new APIs via a "innovation time" spike: build a prototype in a sandbox branch measuring stability, performance, and team learning curve. Adopt stable APIs (not alpha/beta) for production unless they solve critical blockers. Use Android Studio's "New Project" templates and API diff reports to understand changes. Maintain a tech radar document categorizing technologies into Adopt, Trial, Assess, Hold. Involve the team in quarterly architecture reviews to discuss platform updates. Balance early adoption (competitive advantage, Google support) against stability risks. Target latest targetSdk within 6 months of release to meet Play Store requirements, but feature adoption is prioritized by user value.

Read more: https://developer.android.com/studio/releases

Q115. What is your view on large language models or on-device AI integration in Android apps? On-device AI via TensorFlow Lite, MediaPipe, or Android's Gemini Nano offers privacy (data stays local), offline functionality, and low latency for specific tasks (text classification, image segmentation, smart reply). However, model size impacts APK and memory; use dynamic feature delivery for large models. LLMs on-device are emerging but limited by device capability; most apps should use cloud LLMs with strict data handling policies. Integrate AI as a feature enhancement, not core architecture dependency—ensure the app works if AI fails or is unavailable. Use MlKit for ready-made solutions (OCR, translation) rather than custom model training unless you have ML engineers. Respect user consent for AI-processed data. Monitor battery and thermal impact of sustained model inference. The trend is hybrid: lightweight on-device inference for real-time tasks, cloud for complex generative tasks.

Read more: https://developer.android.com/ml

Q116. How do you approach foldable and large-screen device support from an architectural standpoint? Architecture should be UI-layer agnostic to form factor. Use WindowSizeClass and WindowInfoTracker to drive layout decisions at the composition level, not in ViewModels. ViewModels expose state; UI decides whether to render single-pane, two-pane, or dual-screen layouts. Use SlidingPaneLayout or NavSuiteScaffold for list-detail patterns that adapt automatically. Handle fold posture (flat, half-opened, tabletop) via Jetpack WindowManager to adjust layouts (e.g., video on top, controls on bottom when folded). Test with emulators supporting foldable presets and physical devices (Samsung Galaxy Z, Pixel Fold). Ensure drag-and-drop works across screens using DropHelper. Don't maintain separate codebases for foldables; responsive design principles cover phones, tablets, and foldables. Use resizableActivity="true" and maxAspectRatio correctly in the manifest. Architect state to survive configuration changes during folding/unfolding.

Read more: https://developer.android.com/guide/topics/large-screens

Q117. What role do you see for Baseline Profiles and Macrobenchmark in your performance strategy? Baseline Profiles ship pre-compiled hot path data in the APK, allowing ART to optimize app startup and critical user journeys immediately rather than relying on JIT over days. They reduce cold start time by 20-40% on supported devices. Macrobenchmark measures real-world startup, scrolling, and animation performance with statistical significance, producing actionable trace files. Together they form a data-driven performance loop: benchmark current state → identify jank via traces → optimize code → write Baseline Profile rules for the improved paths → re-benchmark to validate. Integrate Macrobenchmark in CI to detect regressions on release builds. Generate Baseline Profiles using the Baseline Profile Gradle plugin and Macrobenchmark library. Update profiles with each significant release. These are essential for competitive apps where startup time directly impacts user acquisition and Play Store ranking.

Read more: https://developer.android.com/topic/performance/baselineprofiles

Q118. How do you think about Wear OS, TV, or Auto development in relation to your mobile app strategy? Treat them as specialized surfaces consuming the same domain layer and business logic via shared modules, but with distinct UI layers optimized for input modality and context. Wear OS needs glanceable, glanceable interactions—use complications and tiles with minimal data from the mobile app's repository layer. TV requires D-pad navigation, leanback UI, and audio focus handling; share media playback logic via Media3 but build separate fragments. Auto uses Android Automotive OS or Android Auto with MediaBrowserService for audio apps; again, shared media domain logic. Don't port mobile UI directly; redesign for the surface. Use dynamic feature modules or separate app modules to deliver surface-specific APKs from shared code. Maintain a core module with networking, auth, and data that all surfaces depend on. Prioritize surfaces based on user analytics and business goals.

Read more: https://developer.android.com/training/wearables

Q119. What is your experience with server-driven UI frameworks, and when are they appropriate? Server-driven UI (SDUI) allows the backend to control layout and components via JSON/XML schemas rendered natively. Appropriate for: rapidly changing UI (marketing screens, dashboards), A/B testing layouts without app releases, or apps with highly regional customization. Frameworks like Airbnb's Epoxy, LayoutDSLs, or custom JSON-to-Compose parsers enable this. Risks include: increased payload size, latency waiting for UI definitions, limited offline capability, and difficulty handling complex interactions. Security concerns arise if the server sends executable-like logic. Use SDUI for static or lightly interactive content; avoid it for core transactional flows requiring reliability and speed. Maintain a native component library that the server references by name/type. Cache schema definitions locally. Validate schemas with strong typing. Monitor parse failures and fallback to default layouts. SDUI is a tool for specific velocity problems, not a universal architecture.

Read more: https://developer.android.com/jetpack/compose/layouts/adaptive

Q120. How would you prepare your team's architecture for potential future platform shifts or form factors? Maximize framework-agnostic business logic in Kotlin multiplatform or pure Kotlin modules. Isolate Android-specific APIs (lifecycle, notifications, sensors) behind interfaces in a platform layer so they can be reimplemented. Use dependency injection extensively to swap implementations. Design UI state as platform-neutral models; only the presentation layer adapts to UI toolkit changes. Adopt modular architecture so entire layers can be replaced without affecting others. Follow open standards (HTTP, JSON, SQL) rather than proprietary solutions where possible. Maintain architecture decision records explaining why boundaries exist, making future refactoring informed. Invest in automated testing so platform migrations can be validated safely. Keep dependencies up-to-date to reduce future upgrade cliffs. Encourage the team to learn platform fundamentals rather than framework-specific magic. Build for change: expect that today's ViewModel may be tomorrow's something else.

Read more: https://developer.android.com/topic/architecture

Last updated