Can you describe what the Android operating system is, and explain the main ways it differs from competing mobile platforms like iOS?
KnownRevise
Android is Google’s open-source, Linux-kernel-based operating system for mobile and embedded devices, distinguished from rivals mainly by its permissive licensing, deep integration with Google’s services and cloud, manufacturer-driven hardware ecosystem, and a development model built around the Kotlin language and Jetpack Compose.
Read full answerHide full answer
Applies toAndroid 16 (API level 36), current stable release; statements about openness and licensing apply to all versions.
Architecture
Android is a software stack, not just a kernel:
Linux kernel: provides process isolation, memory management, power management, and hardware drivers (with vendor-specific driver modules on many devices).
Hardware Abstraction Layer (HAL): a stable contract that lets the framework talk to camera, audio, Bluetooth, and other vendor drivers without knowing their implementation.
Native libraries and Android Runtime (ART): pre-compiled C/C++ libraries (SQLite, OpenGL ES, Skia, media codecs) plus ART, which runs app bytecode ahead-of-time compiled and JIT-optimized.
Java API framework: the application framework — Activity, Window, Notification, Package Manager, Location Manager — that apps call into.
System apps and Google Play services: the user-facing apps, plus the proprietary Google Mobile Services layer (Play Store, Maps, Firebase, FCM) that most consumer devices license.
Apps are sandboxed: each runs under its own Linux user ID, gets its own VM, and needs explicit permissions for cross-app or hardware access.
Key Characteristics
Open source under the Apache 2.0 license: the Android Open Source Project (AOSP) is free to download, modify, and ship. Anyone can build a device — Amazon (Fire OS), Huawei (HarmonyOS-era forks), and various automotive and TV builds all derive from it.
Hardware diversity: one OS across hundreds of OEMs, screen sizes, SoCs (Snapdragon, Exynos, Tensor, MediaTek), and form factors — phones, tablets, foldables, Wear OS, Android TV, and Android Auto.
Kotlin-first development: Google officially recommends Kotlin, with Jetpack Compose for declarative UI, Coroutines/Flow for async, Hilt for DI, Room for persistence, and WorkManager for background work.
Distribution freedom: apps can be sideloaded or published to alternative stores, though Google Play is dominant on certified devices.
Android vs iOS vs Other Platforms
Aspect
Android
iOS / iPadOS
Others (HarmonyOS, KaiOS, Tizen)
Licensing
Open source (AOSP, Apache 2.0)
Closed source, Apple only
Mixed; HarmonyOS NEXT drops AOSP compatibility
Hardware
Many OEMs, wide price range
Apple devices only
Varies; KaiOS targets feature phones
Language
Kotlin (Java supported), Compose UI
Swift, SwiftUI
HarmonyOS: ArkTS; KaiOS: web technologies
App store
Google Play + sideloading
App Store only
HarmonyOS: AppGallery
Updates
OEM-controlled, slower rollout
Direct from Apple, fast
OEM or carrier controlled
Customization
Deep: launchers, default apps, ROMs
Limited
Varies
Security model
Sandbox + Play Protect + permission gates
Sandbox + strict App Store review
Varies
Advantages and Limitations
Advantages: enormous device reach and price diversity, flexible distribution, interoperability via Intents and sharesheets, strong background processing APIs (WorkManager, foreground services), and a large open ecosystem for automation and embedded uses.
Limitations: update fragmentation because OEMs and carriers control releases, testing cost across many screen sizes and Android versions, and inconsistent vendor skins that can alter battery and background behaviour. Google mitigates this partly by pushing critical fixes through Google Play system updates (Project Mainline, API 29+) and by backporting many libraries via Jetpack.
Best Practices for Interview Follow-ups
Name API levels when a behaviour is version-dependent, e.g. runtime permissions (API 23+), scoped storage (API 29/30), exact alarms (API 31+), POST_NOTIFICATIONS (API 33+), and foreground service types (API 34+).
Emphasize that Android’s openness is about the OS, not Google’s services: Google Mobile Services are proprietary and licensed separately from AOSP.
If asked about other mobile OSs, note that HarmonyOS NEXT (2024+) no longer runs Android APKs, which is a meaningful shift in the Chinese market.
2
Which programming languages are available for building Android apps?
KnownRevise
Kotlin is the primary, officially recommended language for Android development, but Android also supports Java and C++ through the Android SDK and NDK, and several other languages are usable through cross-platform frameworks.
Read full answerHide full answer
Applies toAndroid 16 (API 36) with Kotlin 2.x and Android Studio; Java and C++ support apply to the same release.
Officially Supported Languages
Language
How it is used
Notes
Kotlin
First-class language for app logic and UI with Jetpack Compose
Google’s preferred language since 2017; all new Android APIs and documentation target Kotlin first
Java
Compiles to the same Dalvik bytecode as Kotlin
Fully supported and interoperable with Kotlin, but newer APIs are Kotlin-first; use mainly for maintaining existing code
C/C++
Native code via the Android NDK and JNI
For performance-critical code, games, or reusing existing C/C++ libraries
Kotlin: Concise, null-safe, and coroutine-friendly, which fits the recommended architecture of ViewModel with StateFlow, coroutines and Flow, Hilt, Room, WorkManager and Retrofit/OkHttp. Interoperates fully with existing Java code, so teams can migrate gradually.
Java: Still compiles and runs, and Android Studio supports it, but it lacks Kotlin’s language-level coroutines and null safety. Java 8+ language features are available through desugaring, but new Jetpack libraries increasingly use Kotlin-only features such as suspend functions and inline value classes.
C++: Not a replacement for app-level development. You still need Kotlin or Java for the app entry points, activities and most framework interaction; the NDK is for hot paths like signal processing, physics engines or cross-platform game engines.
Cross-Platform and Third-Party Options
Dart, with the Flutter framework, compiles to native Android code and is a popular alternative for apps shared with iOS.
JavaScript and TypeScript, through frameworks such as React Native, run JavaScript logic while rendering native Android views.
Kotlin Multiplatform is officially supported by Google and lets you share Kotlin code, including with Compose Multiplatform, across Android, iOS and other targets while keeping platform-specific code in Kotlin.
Rust is available in the Android NDK for native code, which is useful for security-sensitive or performance-critical shared libraries; check the current NDK release notes for the supported toolchain version.
Choosing a Language
New Android app: Kotlin with Jetpack Compose. It is the default in Android Studio’s project templates and the language used by current platform guidance.
Existing Java codebase: Keep Java where it is, write new features in Kotlin, and migrate incrementally; the two languages compile together in the same module.
Games or heavy computation: Kotlin or Java for the app layer, plus C++ (or Rust where available) through the NDK for the performance-critical core.
Shared codebase across mobile platforms: Kotlin Multiplatform if you want to stay in the Android-recommended ecosystem, or Flutter/React Native if the team’s skills favor them.
Avoid starting new projects in Java unless there is a strong constraint, and do not treat C++ as a general-purpose app language: it adds build complexity and JNI overhead without benefits for typical business logic.
3
Describe the architectural layers and components that structure a modern Android application.
KnownRevise
A modern Android app is built around a layered architecture, most commonly MVVM (Model-View-ViewModel) combined with Clean Architecture principles, which separates the UI, business logic, and data concerns into distinct modules. This separation keeps the app testable, maintainable, and resilient to configuration changes such as screen rotations.
Read full answerHide full answer
Applies toAndroid 16 (API 36) with Kotlin 2.x, Jetpack Compose, and current Jetpack libraries
Layered Structure
UI layer: Built with Jetpack Compose, it observes state exposed by the ViewModel and renders screens. It handles user interactions and navigation events but contains no business logic.
Domain layer: Optional in smaller apps, it holds use cases (interactors) that encapsulate business rules. These are pure Kotlin modules with no Android framework dependencies.
Data layer: Provides data to the rest of the app through repositories. It combines local sources (Room database, DataStore), remote sources (Retrofit/OkHttp), and background work via WorkManager.
Core Components and Their Roles
ViewModel: Survives configuration changes, holds UI state, and exposes it as StateFlow or LiveData. It never holds an Android Context or a View reference.
Repository: Acts as the single source of truth, deciding whether to fetch from the network, cache, or local database.
Use case: Wraps a single business operation, making logic reusable and independently testable.
Dependency injection: Hilt provides scoped instances (@ViewModelScoped, @Singleton) and removes manual wiring of dependencies.
Recommended Architecture Pattern
The current recommendation is MVVM with unidirectional data flow (UDF). The UI sends events to the ViewModel, the ViewModel updates state, and the UI reacts to those state changes. Larger apps add a domain layer between UI and data following Clean Architecture.
Layer
Responsibility
Typical Technologies
UI
Display, user input, navigation
Compose, Navigation Compose
Domain
Business rules
Use cases, pure Kotlin
Data
Data sources and caching
Room, Retrofit, DataStore, WorkManager
Data Flow and State Management
Coroutines and Flow: Network and disk operations run on Dispatchers.IO or via WorkManager, never on the main thread. Results are exposed as Flow and collected in the ViewModel.
StateFlow in ViewModel: The ViewModel exposes immutable StateFlow via stateIn. Compose collects it with collectAsStateWithLifecycle(), which respects the lifecycle to avoid leaks.
Event handling: One-off events such as snackbars and navigation use a Channel or a SharedFlow configured without replay, rather than StateFlow, to avoid re-emission after a configuration change.
Best Practices and Common Pitfalls
Keep the UI layer thin: Compose functions should be stateless where possible and receive state as parameters.
Never call suspend functions or do I/O on the main thread; use viewModelScope.launch(Dispatchers.IO) or WorkManager for background work.
Avoid deprecated APIs: AsyncTask and IntentService are deprecated (API 30) — replace with coroutines and WorkManager; startActivityForResult is replaced by the Activity Result API; kotlin-android-extensions synthetics are removed — use View Binding or Compose.
Scope dependencies correctly with Hilt: @ViewModelScoped for objects injected into ViewModels, @Singleton for app-wide singletons like Retrofit.
Test each layer independently: ViewModel tests with runTest, repository tests with fake data sources, and UI tests with the Compose UI testing library.
4
Explain the stages of the Android application lifecycle and how the operating system manages an app's components from creation to destruction.
KnownRevise
The Android application lifecycle is the sequence of states and callback methods the system uses to create, make visible, interact with, and eventually destroy or kill an app’s components as resources and user actions demand. Unlike platforms with a single app entry and exit point, the Android application lifecycle is component-based and controlled by the operating system, not by your code.
Android does not expose one lifecycle for the entire application. Each component type manages its own:
Activity: a single screen of UI; the lifecycle most developers interact with daily
Service: performs long-running operations without a UI
BroadcastReceiver: reacts to system-wide broadcast announcements
ContentProvider: exposes shared app data to other apps
The system invokes lifecycle callbacks at the right moments; your code overrides them to respond. You never call these callbacks directly.
Activity Lifecycle Callbacks
An Activity passes through seven core callbacks:
onCreate(): called once when the Activity is first created; set up Compose content, initialize ViewModels, and restore saved state here
onStart(): the Activity becomes visible to the user
onResume(): the Activity has focus and the user can interact with it
onPause(): the Activity is partially obscured or another one is arriving; commit unsaved edits quickly
onStop(): the Activity is no longer visible; release heavy resources such as bitmap caches or camera sessions
onDestroy(): final cleanup before the Activity is finished; the system does not call this when it kills the process to reclaim memory
onRestart(): called after onStop() when the Activity returns to the foreground
Because the system may kill the process any time after onPause(), persist critical state in onSaveInstanceState() or in a Room-backed repository rather than relying on member fields.
Process Importance and System Killing
The system ranks each app process by importance and terminates lower-priority ones when memory is low:
Priority
Condition
Foreground
Activity in onResume() or a foreground Service
Visible
Activity in onStart() but not onResume()
Service
Service started via startService()
Cached
No active components; first to be killed
Starting at API 26 (Android 8.0), background execution limits restrict what a cached process can do. Long-running work should therefore use WorkManager or a foreground Service with the correct foregroundServiceType declared on API 34+.
App-Level Lifecycle
For lifecycle awareness across the whole app rather than a single screen, use ProcessLifecycleOwner from androidx.lifecycle. It reports ON_CREATE, ON_START, and ON_RESUME as the app enters the foreground, and ON_PAUSE, ON_STOP, ON_DESTROY as it backgrounds. This is the recommended approach for analytics sessions, periodic data refreshes, or releasing shared resources without coupling logic to a specific Activity.
Best Practices
Keep onCreate() and onResume() fast; run disk or network work in a coroutine on Dispatchers.IO or schedule it with WorkManager
Hold UI state in a ViewModel with StateFlow so it survives configuration changes and process death
Cancel Flow collectors and Compose side effects in onStop() or onDestroy() to prevent memory leaks
Never assume onDestroy() will be called; the system can kill the process without warning
Use the Activity Result APIs instead of startActivityForResult(), which is deprecated, for results from other screens
5
In Android development, how would you define an Activity component and explain the sequence of callback methods that govern its lifecycle states?
KnownRevise
An Activity is a fundamental Android component that represents a single screen with a user interface, acting as the entry point for user interaction. Its lifecycle consists of a specific sequence of callback methods invoked by the operating system as the component transitions between states such as creation, foreground visibility, and destruction.
Read full answerHide full answer
Applies toAndroid API 36 (Android 16), Kotlin 2.x, Jetpack Compose
Core Responsibilities
An Activity provides a window in which the app draws its UI and handles user input events. It serves as a Context for accessing resources and system services, and it manages the navigation stack via the task back stack. While Activities host the UI, business logic should reside in a ViewModel to survive configuration changes like screen rotations. In modern development, Activities primarily initialize Jetpack Compose content rather than inflating XML layouts. They also handle permission requests using the Activity Result APIs, replacing the deprecated onRequestPermissionsResult method.
Lifecycle States and Callbacks
The system drives the Activity through distinct states using callback methods. Developers must override these to manage resources appropriately and ensure a smooth user experience.
Callback
State
Description
onCreate()
Created
Initial setup; called once. Use for binding data or setting content.
onStart()
Started
Activity becomes visible to the user.
onResume()
Resumed
Activity is in the foreground and interactive.
onPause()
Paused
Partially obscured; commit unsaved changes here.
onStop()
Stopped
Hidden from view; release heavy resources.
onDestroy()
Destroyed
Final cleanup before the instance is killed.
onRestart()
Restarted
Called after onStop() when returning to the foreground.
Transient UI state should be saved in onSaveInstanceState() using a Bundle, while persistent data belongs in a database or repository. Understanding these transitions is critical for preventing crashes when the system kills the process to reclaim memory.
Integration with Jetpack Compose
In Compose-based apps, the Activity sets the content via setContent { } instead of setContentView(). Lifecycle awareness is often handled through LifecycleScope or composables like DisposableEffect, which automatically dispose of resources when the composition leaves the tree. You can observe lifecycle states directly in composable functions using collectAsStateWithLifecycle from androidx.lifecycle:lifecycle-runtime-compose, ensuring UI updates only happen when the Activity is at least started. This reduces the need for manual lifecycle observers in the Activity class itself.
Best Practices and Pitfalls
Avoid performing long-running operations on the main thread within lifecycle callbacks; use coroutines with Dispatchers.IO or WorkManager instead. Do not store UI state in the Activity instance itself, as it is destroyed and recreated during configuration changes. Always unregister listeners or stop animations in onPause() or onStop() to prevent battery drain and memory leaks. When targeting API 33+, ensure notification permissions are handled separately from the Activity lifecycle logic. Prefer Hilt for injecting dependencies into the Activity to avoid manual service locator patterns and improve testability.
6
Can you explain what Intents are in Android and describe the ways developers use them to communicate between components?
KnownRevise
An Intent is a messaging object in Android that lets you request an action from another app component, such as starting an Activity, launching a Service, or delivering a broadcast. It describes the operation to perform and carries any data the target needs, while the Android system resolves it to the component that should handle it.
Read full answerHide full answer
Applies toAndroid 16 (API 36), Kotlin 2.x
What an Intent Is
An Intent is an instance of android.content.Intent. It does not perform work itself; instead, it is a passive information bundle that the Android framework uses to locate and invoke the right component. Every Intent contains:
Component: the target class for explicit Intents, set via the constructor or setComponent().
Action: a string describing the general action, such as Intent.ACTION_VIEW or Intent.ACTION_SEND.
Data: a Uri describing the data to operate on, like a web URL or a contact URI.
Extras: a Bundle of key-value pairs for passing additional data between components.
Category and flags: metadata that influences how the target is launched or resolved.
Explicit vs Implicit Intents
Aspect
Explicit Intent
Implicit Intent
Target
Specific component class
Action + data; system resolves it
Scope
Usually within your own app
Can reach other apps or system apps
Declaration
Component class in constructor
Intent filter in the manifest
Example
Opening DetailActivity
Opening a URL in a browser
Use an explicit Intent when you know exactly which component in your app should handle the request. Use an implicit Intent when you want any app capable of the action to handle it, such as sharing content or viewing a web page.
Common Use Cases
Starting an Activity: startActivity(intent) launches a new screen. For example, an explicit Intent built as Intent(context, DetailActivity::class.java) with a "item_id" extra opens a detail screen; in Jetpack Compose you still call this from a Context, typically via LocalContext.current.
Implicit actions: an Intent with Intent.ACTION_VIEW and a Uri from Uri.parse("https://example.com") asks the system to open a URL in a browser.
Starting a Service: startService() or bindService() passes work to a Service.
Delivering a Broadcast: sendBroadcast() notifies any registered BroadcastReceiver.
Deferred execution: A PendingIntent wraps an Intent so another app or the system can execute it later, such as in a notification action; from API 31 you must supply FLAG_IMMUTABLE or FLAG_MUTABLE.
Best Practices and Pitfalls
Check resolution for implicit Intents: verify a handler exists before starting. resolveActivity() is deprecated in API 30; use queryIntentActivities() and respect the package visibility rules introduced in API 30 to avoid ActivityNotFoundException.
PendingIntent mutability: from API 31 (Android 12) you must specify FLAG_IMMUTABLE or FLAG_MUTABLE; prefer FLAG_IMMUTABLE unless the Intent must change.
Avoid deprecated APIs: startActivityForResult() and onRequestPermissionsResult() are deprecated (API 30+); use the Activity Result API (registerForActivityResult) instead.
Do not pass secrets: extras travel through Binder and can be read by receiving components; never send passwords or tokens across app boundaries.
Mind the size limit: Intent extras are bound by the ~1 MB Binder transaction limit; pass large data via a file, database, or ContentProvider reference instead.
7
Describe what a Service is in Android and how it is used.
KnownRevise
A Service in Android is an application component that performs long-running operations in the background without providing a user interface, allowing work to continue even when the user navigates away from your app. Services run in your app’s main process and main thread by default, so any intensive work must be offloaded to a separate thread, coroutine, or a dedicated background solution like WorkManager.
A Service is one of the four core Android components (with Activity, BroadcastReceiver, and ContentProvider). It is declared in the manifest and started or bound by other components. Because it has no UI, it is suited for work that should outlive a single screen, such as playing music, handling network sync, or communicating with a wearable device.
Started Service: launched via startService() (or Context.startForegroundService() for foreground), runs until it calls stopSelf() or is stopped, and does not return results to the caller.
Bound Service: components bind via bindService() and interact through an IBinder interface; it lives only while at least one client is bound.
Foreground Service: a started service that shows an ongoing notification, telling the system the work is user-visible and important; required for tasks like media playback, location tracking, or file downloads.
Type
How it starts
Lifetime
Typical use
Started
startService()
Until stopped
One-off background work
Bound
bindService()
While clients bound
Component-to-service communication
Foreground
startForegroundService() + startForeground()
Until stopped, higher priority
User-visible ongoing work
Lifecycle and Threading
The system calls lifecycle callbacks in a predictable order:
onCreate() — service is first created; initialize resources here.
onStartCommand() — called for each startService() call; return START_STICKY, START_NOT_STICKY, or START_REDELIVER_INTENT to control restart behaviour.
onBind() — return an IBinder for bound clients, or null for a purely started service.
onDestroy() — final cleanup before the service is removed.
A Service runs on the main thread, so launching a coroutine with lifecycleScope is not available; use CoroutineScope(Dispatchers.IO) or a supervisor scope, and cancel it in onDestroy().
Modern Guidance: Prefer WorkManager and Coroutines
Many historical uses of Services are now better handled by newer APIs:
IntentService is deprecated (API 30); replace it with coroutines plus WorkManager for deferrable, guaranteed work.
Use WorkManager for tasks that must run even if the app exits or the device reboots (sync, uploads, periodic jobs).
Use coroutines/Flow for in-process background work tied to a ViewModel or UI lifecycle.
Keep a Service only when you genuinely need a long-lived component, such as media playback via MediaSessionService (recommended over a plain Service for media) or active location tracking.
Code Example
// A minimal started service doing work off the main thread
class SyncService : Service() {
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
scope.launch {
// perform network or disk work here
stopSelf()
}
return START_NOT_STICKY
}
override fun onBind(intent: Intent?): IBinder? = null
override fun onDestroy() {
scope.cancel()
super.onDestroy()
}
}
<!-- Manifest declaration; exported="false" unless other apps must start it -->
<service
android:name=".SyncService"
android:exported="false" />
For a foreground service on API 34+, you must also declare android:foregroundServiceType (e.g. dataSync, location, mediaPlayback) and call startForeground() with a notification shortly after starting.
Best Practices and Common Pitfalls
Do not block the main thread: heavy work in a Service causes an ANR just like in an Activity.
Declare every Service in the manifest, or the system throws a SecurityException when starting it.
Set android:exported="false" unless another app explicitly needs to bind or start your Service.
Request POST_NOTIFICATIONS at runtime on API 33+ before posting foreground-service notifications.
Handle battery and Doze: for deferrable work, WorkManager respects Doze mode automatically; a plain Service does not.
Clean up resources in onDestroy() to avoid leaks, and cancel any coroutines or listeners you started.
8
What is a Broadcast Receiver in Android, and how is it typically used inside an Android application?
KnownRevise
A BroadcastReceiver is an Android application component that listens for and responds to broadcast Intent messages sent by the system or by other apps. It allows your app to react to system-wide events such as network changes, battery state, or device boot without keeping a component running continuously in the background.
Read full answerHide full answer
Applies toAndroid 16 (API 36) with Kotlin 2.x
What a Broadcast Receiver Is
A BroadcastReceiver is one of the four core app components, alongside Activity, Service, and ContentProvider.
It has a single lifecycle callback, onReceive(context, intent), which the system invokes when a matching broadcast arrives.
The system considers the receiver active only while onReceive is executing; once it returns, the component is no longer guaranteed to stay alive, so long-running work should be handed off to a coroutine, WorkManager, or a foreground Service.
Typical Use Cases
System state changes: reacting to ACTION_BATTERY_LOW, ACTION_AIRPLANE_MODE_CHANGED, or screen on/off events.
Connectivity monitoring: historically CONNECTIVITY_ACTION, though the current recommendation is ConnectivityManager.NetworkCallback instead.
Boot and unlock: ACTION_BOOT_COMPLETED or ACTION_LOCKED_BOOT_COMPLETED to reschedule work after a restart.
App-internal events: your own components sending local broadcasts via LocalBroadcastManager (deprecated) or, preferably, ContextCompat.registerReceiver with RECEIVER_NOT_EXPORTED for scoped communication.
Registration Options
Approach
How it is declared
Best for
Key limitation
Manifest-declared
<receiver> element in AndroidManifest.xml
System broadcasts that must arrive even when the app is not running
Since Android 8 (API 26), most implicit broadcasts cannot be delivered to manifest receivers except exempt ones like BOOT_COMPLETED
Context-registered
registerReceiver() in an Activity, Fragment, or Service scope
App-specific or lifecycle-bound events while the app is in the foreground
Must be unregistered to avoid leaks; on Android 14 (API 34)+ you must declare export behavior
Behavior by Android Version
API 26+ (Android 8): manifest-declared receivers are restricted for implicit broadcasts; use context registration or exempt broadcasts.
API 30+ (Android 11): any component with an intent filter must declare android:exported explicitly.
API 33+ (Android 13): registerReceiver overloads accept Context.RECEIVER_EXPORTED or Context.RECEIVER_NOT_EXPORTED.
API 34+ (Android 14): context-registered receivers must specify one of those export flags or the app crashes.
Code Example
// Context-registered receiver scoped to an Activity lifecycle
private var isReceiverRegistered = false
private val batteryReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
if (intent.action == Intent.ACTION_BATTERY_LOW) {
// Defer heavy work to WorkManager or a coroutine
}
}
}
override fun onResume() {
super.onResume()
if (!isReceiverRegistered) {
val filter = IntentFilter(Intent.ACTION_BATTERY_LOW)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
// API 33+ requires an explicit export flag
registerReceiver(batteryReceiver, filter, Context.RECEIVER_NOT_EXPORTED)
} else {
registerReceiver(batteryReceiver, filter)
}
isReceiverRegistered = true
}
}
override fun onPause() {
super.onPause()
if (isReceiverRegistered) {
unregisterReceiver(batteryReceiver)
isReceiverRegistered = false
}
}
Best Practices and Common Pitfalls
Always unregister context-registered receivers in onPause, onDestroy, or the matching lifecycle callback to prevent memory leaks and duplicate deliveries.
Keep onReceive fast: do not perform network or disk I/O there; hand off to coroutines on Dispatchers.IO or schedule WorkManager work.
Prefer context registration for broadcasts you only need while the UI is visible; reserve manifest registration for the small set of exempt system broadcasts.
Set android:exported deliberately in the manifest and use RECEIVER_NOT_EXPORTED for internal broadcasts to avoid unintentionally exposing your receiver to other apps.
Avoid deprecated APIs: LocalBroadcastManager and CONNECTIVITY_ACTION are deprecated; use scoped context registration and NetworkCallback respectively.
9
In an Android project, which directory contains the app's `AndroidManifest.xml` file?
KnownRevise
The app’s AndroidManifest.xml lives in the app/src/main/ directory of an Android project, at the path app/src/main/AndroidManifest.xml. This manifest file is required by every Android app and is read by the Android build tools and the operating system at install time.
Read full answerHide full answer
Applies toall versions of Android, including Android 16 (API 36), built with Kotlin 2.x and Android Studio.
Why the Path Looks Like That
Modern Android projects use the Gradle “source set” layout, where each module has its own src/ tree. The main source set holds code and resources shared across all build variants, which is why the manifest sits there:
app/ — the application module (as opposed to library modules or buildSrc).
src/main/ — the default source set for the module.
AndroidManifest.xml — the manifest itself, always at the root of the source set, never inside res/ or java/.
Build variants add their own manifests at sibling paths such as app/src/debug/AndroidManifest.xml or app/src/release/AndroidManifest.xml. These are optional and are merged with the main manifest by the manifest merger during the build, with variant-specific settings such as a debug-only uses-permission or a different applicationId suffix.
What the Manifest Contains
Package identity: the namespace in build.gradle.kts (AGP 7.0+), which replaced the deprecated package attribute in the manifest tag, plus android:label, the name shown to users.
App components: every activity, service, receiver and provider, including the launcher activity marked with MAIN and LAUNCHER intent filters.
Permissions: declarations like android.permission.INTERNET or runtime permissions such as POST_NOTIFICATIONS (API 33+).
Hardware and feature requirements: uses-feature entries like camera or Bluetooth LE, used by Google Play for device filtering.
App-wide configuration: theme, icon, allowBackup, largeHeap, and component-level attributes such as exported, which must be set explicitly for components with intent filters from API 31 onwards.
How It Relates to the Build
The manifest is not used in isolation. The Android Gradle Plugin merges it with manifests from library dependencies and build variants, then combines it with build.gradle.kts settings such as minSdk, targetSdk, versionCode and dependencies’ manifests. The merged result is packaged into the final APK or Android App Bundle, which is why a missing or malformed manifest causes the build to fail before any code runs.
Common Pitfalls
Putting the manifest in app/src/main/res/ or app/src/main/java/ — the build will not find it and reports a missing-manifest error.
Declaring the same component in main and a variant manifest without understanding merge rules — conflicts surface as build errors or unexpected exported values.
Editing the generated build/ copy — files under app/build/ are regenerated on every build; always edit the source manifest under src/main/.
10
Can you describe the architecture and rendering flow underlying Android's user interface view system?
KnownRevise
Android’s user interface traditionally operates on a hierarchical View system where components are organized in a tree and rendered through sequential measure, layout, and draw passes, although modern development prioritizes the declarative Jetpack Compose toolkit for new projects.
View and ViewGroup: The classic system relies on View and ViewGroup classes arranged in a parent-child hierarchy, typically defined in XML layouts.
Three rendering phases: During rendering, the framework traverses this tree in three passes: onMeasure determines dimensions, onLayout positions children, and onDraw renders content to a Canvas.
Imperative updates: Developers must manually fetch references using findViewById (legacy) or View Binding (recommended) and mutate state, which can lead to boilerplate and synchronization issues between UI state and logic.
Jetpack Compose Paradigm
Jetpack Compose replaces the imperative View model with a declarative approach using @Composable functions. Instead of managing a mutable view tree, developers describe the UI based on current state. When state changes, the framework intelligently recomposes only the affected components. This eliminates XML layouts and reduces boilerplate, aligning UI logic more closely with Kotlin code. Compose is the recommended UI toolkit for Android 16 and beyond, while Views remain supported for maintenance.
Rendering Pipeline Differences
Aspect
Traditional Views
Jetpack Compose
Definition
XML layouts (or programmatic inflation)
@Composable functions
Update model
Imperative, manual state mutation
Declarative recomposition on state change
Rendering
Hardware-accelerated display lists replayed by the GPU
Skia graphics engine via the Android NDK
Interop
N/A
AndroidView embeds Views in Compose; ComposeView embeds Compose in Views
Compose bypasses some legacy View overhead, offering better performance for complex animations and custom drawing, though the interop layers add some cost when mixed.
Best Practices and Migration
For new development on API 36, start with Compose and avoid introducing new XML layouts.
If maintaining legacy code, isolate View-based screens and migrate incrementally using ComposeView within existing Activities.
Never block the main thread during inflation or layout passes; offload heavy data processing to coroutines using Dispatchers.IO.
Ensure that any custom View implementations override onMeasure correctly to avoid layout crashes, whereas custom Compose components rely on modifier constraints.
Common Pitfalls
Mixing View and Compose hierarchies excessively can cause performance degradation due to measurement conflicts.
Treating Compose like Views (e.g., trying to access composables as objects) violates the declarative paradigm.
Always manage state hoisting correctly in Compose to ensure a single source of truth.
Context access inside composables should be handled via LocalContext rather than holding references.
11
In Android development, how do a File, a Class, and an Activity differ in terms of their purpose and relationship to one another?
KnownRevise
In Android development, a File is a physical storage unit on disk, a Class is a logical code blueprint defined within a file, and an Activity is a specific Android framework component that represents a single screen with a user interface. While every Android project consists of files containing classes, only certain classes subclass ComponentActivity to become Activities managed by the operating system.
File: A physical entity in the project directory structure (e.g., MainActivity.kt). It organizes code for the compiler and IDE but has no runtime behavior itself.
Class: A logical template for objects written inside a file. It defines properties and functions. A single file can contain multiple classes, though Kotlin conventionally prefers one public class per file matching the filename.
Activity: A subclass of androidx.activity.ComponentActivity that the Android OS instantiates and manages. It serves as an entry point for interacting with the user and hosts UI logic.
Comparison Overview
Aspect
File
Class
Activity
Nature
Physical disk unit
Logical code template
Android OS Component
Location
Project directory
Inside a file
Declared in Manifest
Purpose
Organization
Blueprint for objects
Screen management
Example
MainActivity.kt
class MainActivity
MainActivity : ComponentActivity
Lifecycle
None
Determined by instantiation
Managed by Android System
Structural Relationship
The hierarchy flows from physical to logical to functional: 1. The project contains Files in the src/main/kotlin directories. 2. Files define Classes, interfaces, or functions. 3. If a class extends ComponentActivity and is declared in the AndroidManifest.xml, it becomes an Activity. This means an Activity is always a Class, and that Class is always defined within a File, but not all Classes are Activities, and Files can exist without defining any Class (e.g., resource files or build scripts).
The Activity in Modern Compose Architecture
Historically, Activities managed XML layouts via setContentView. In modern Android development using Jetpack Compose, the Activity’s role is minimized to hosting the Compose composition tree. The UI is declared using composable functions rather than Views. - The Activity handles lifecycle events (onCreate, onResume). - It delegates UI rendering to setContent { }. - Business logic should reside in a ViewModel, not the Activity itself, adhering to separation of concerns. - Activities should remain thin, acting primarily as a bridge between the OS lifecycle and the UI layer.
Common Pitfalls
Confusing File and Class Names: In Kotlin, if a file is named MainActivity.kt, the public class inside should ideally match to avoid compilation warnings or Java interop issues.
Overloading the Activity: Treating the Activity as a generic utility class leads to tight coupling. Activities should coordinate UI and lifecycle, while data logic belongs in repositories or ViewModels.
Manifest Declaration: Defining a class that extends ComponentActivity is insufficient; it must be registered in AndroidManifest.xml to be launchable by the system.
Lifecycle Mismanagement: Accessing UI resources outside valid lifecycle states (e.g., after onDestroy) causes crashes. Use lifecycle-aware components like LifecycleObserver or coroutines scoped to lifecycleScope.
Summary
Understanding this distinction is crucial for navigation and debugging. When an error references a file path, inspect the disk structure. When it references a class method, inspect the code logic. When it references an Android component lifecycle, inspect the Activity implementation and manifest configuration.
12
How would you describe the Dalvik Virtual Machine and the role it played in the early Android platform?
KnownRevise
The Dalvik Virtual Machine (DVM) was the process-level runtime that executed Android applications on early versions of the platform. Java and Kotlin sources were compiled to standard JVM bytecode and then converted into Dalvik Executable (.dex) bytecode, which the DVM ran inside a sandboxed application process. It was designed specifically for constrained mobile hardware and was later replaced by the Android Runtime (ART).
Read full answerHide full answer
Applies toAndroid 1.0 through Android 4.4 (Dalvik); replaced by ART from Android 5.0 (API 21) onward.
How It Works
Compilation path: Java or Kotlin code was compiled by javac/kotlinc into .class files, and the dx tool (later replaced by d8) converted those into one or more .dex files containing Dalvik bytecode.
Register-based architecture: Unlike the stack-based JVM, Dalvik used a register-based instruction set, which reduced the number of instructions per operation and lowered memory and CPU overhead.
One instance per app process: Each application ran in its own Dalvik instance under its own Linux user ID, giving Android process-level sandboxing and isolation between apps.
Zygote forking: Android preloaded a warm Dalvik instance with common framework classes into a process called Zygote; new app processes were forked from it, which made app startup much faster.
JIT compilation: From Android 2.2 (Froyo, API 8), Dalvik added a just-in-time compiler that translated frequently executed bytecode into native code at runtime; earlier versions interpreted bytecode.
Key Features
DEX format: Multiple classes were compressed into a compact .dex file, which was well suited to limited storage and RAM.
Mobile-first design: Dalvik traded some peak throughput for lower memory usage, smaller binaries, and better battery behaviour.
Process isolation: Security in Android relied heavily on each app having its own Dalvik instance and UID.
Tight framework integration: The runtime worked with Android’s component model (activities, services, receivers, providers) and the application lifecycle.
Dalvik vs. ART
Aspect
Dalvik
ART
Introduced
Android 1.0
Preview in Android 4.4 (API 19)
Default runtime
Until Android 4.4
From Android 5.0 (API 21)
Execution model
Interpreted, then JIT from Android 2.2
Initially ahead-of-time compilation
App startup
Slower, with runtime JIT overhead
Faster startup from precompiled native code
Install/storage cost
Smaller installed footprint
Larger compiled binaries at install time
Later evolution
Replaced
Hybrid AOT/JIT with profile-guided compilation, from Android 7.0 (API 24)
Why It Was Replaced
ART initially used ahead-of-time compilation during installation, which significantly improved application launch performance and reduced JIT work during execution. It also added 64-bit support in Android 5.0, which became important as devices moved to 64-bit CPUs. ART later combined AOT with a JIT compiler and profile-guided compilation, so modern Android keeps fast startup while avoiding the full install-time cost of pure AOT.
What This Means Today
Modern Android, including Android 16 (API 36), uses ART, and Dalvik is a legacy runtime that is no longer part of the platform. However, the naming persists: Android still targets DEX bytecode, tools such as d8 are still called dexers, and terms like “Dalvik bytecode” remain common in Android documentation and tooling. In an interview, the key points are that Dalvik was a register-based, mobile-optimized process virtual machine, that it ran one instance per app, and that ART replaced it starting with Android 5.0.
13
Can you explain what an APK file is and what role it plays in Android?
KnownRevise
An APK (Android Package) is the file format Android uses to distribute and install applications. It is a ZIP archive that bundles the compiled code, resources, assets, and manifest of an app into a single file that the Android runtime and package installer can process.
Read full answerHide full answer
Applies toAndroid 16 (API 36); the format has been used since Android 1.0 and the details below apply to current stable releases unless noted.
Contents of an APK
AndroidManifest.xml: the app’s declaration file, compiled into binary XML. It states the package name, minimum and target SDK levels, requested permissions, declared components (activities, services, broadcast receivers, content providers), and metadata such as the launcher icon reference.
classes.dex: one or more Dalvik Executable files containing the app’s Kotlin/Java code compiled into bytecode for the Android Runtime (ART). Multidex apps split code across several .dex files.
res/: compiled resources such as layouts, strings, colors, dimens, and mipmap/drawable icons, packaged in a binary form and indexed by resources.arsc.
resources.arsc: the resource table that maps resource IDs to values and resolves configuration-specific resources (language, screen density, orientation).
assets/: raw files accessed through AssetManager, such as fonts, JSON data, or game data, which are not compiled.
lib/: native libraries (.so files) for architectures such as arm64-v8a and x86_64, organized in subdirectories per ABI.
META-INF/: contains the manifest of the archive and the app’s digital signatures (CERT.RSA, CERT.SF, CERT.MF for the v1 scheme), which verify integrity and developer identity.
How Android Uses It
The package installer (or a tool such as adb install) reads the APK and checks the signatures.
The system parses AndroidManifest.xml to register components and permissions.
ART compiles or interprets classes.dex, while the resource manager loads entries from res/ and resources.arsc according to the device configuration.
For Google Play distribution, apps are published as an AAB (Android App Bundle), which Play requires for new apps; the store then generates optimized APKs per device, excluding unused ABIs and densities (older devices receive a single full APK instead of splits).
APK Variants
Base APK: the main module of an app, required for installation.
Split APKs: APKs divided by ABI, density, or language, installed together with the base APK through PackageInstaller or adb install-multiple.
Standalone APK: a single APK containing all code and resources, produced from an AAB for devices without split support or for distribution outside Google Play.
Instant APK: used for Google Play Instant experiences. Google Play Instant is deprecated and being phased out, so it should not be used for new projects; standard APKs or app bundles are the replacement.
Signing and Security
Every APK must be signed before installation. The v1 scheme (JAR signing) is legacy, and apps targeting Android 11 (API 30) or later are treated as unsigned if they rely only on v1. The recommended schemes are APK Signature Scheme v2, v3 (which supports key rotation), and v4 (enables incremental installation, Android 11/API 30+). Signing keys should be kept in a secure keystore, such as Google Play’s app signing or a hardware-backed AndroidKeystore, never in the repository.
Inspecting an APK
Because an APK is a ZIP archive, you can inspect it with standard tools, which is useful for debugging resource or manifest issues:
List or extract contents: any ZIP tool works, for example unzip -l app-release.apk to list entries or unzip app-release.apk -d apk_contents to extract them.
Read the manifest and metadata: aapt dump badging app-release.apk prints the package name, SDK levels, permissions, and launcher activity in human-readable form; apkanalyzer (bundled with Android Studio) provides more detailed manifest, dex, and resource inspection.
Verify signing: run apksigner verify app-release.apk to confirm which signature scheme(s) the APK uses and whether the signature is valid.
Best Practices
Prefer AAB for distribution: Google Play requires app bundles for new apps, and they reduce download size through per-device APK optimization.
Verify signatures and integrity: use apksigner verify before publishing, and never distribute a debug-signed APK to users.
Keep APK size low: large APKs increase install failures and uninstalls; enable R8 shrinking and resource shrinking in build.gradle.kts.
Do not modify repackaged APKs: resigning third-party APKs breaks signature-based updates and can trigger Play Protect warnings.
14
What approaches do you use to control memory usage and avoid leaks when building Android apps?
KnownRevise
Android handles most reclamation automatically through its garbage collector, but you are still responsible for preventing leaks and keeping allocations reasonable. Effective memory management in Android means understanding the heap model, following lifecycle-aware patterns, and using the right tooling to detect problems before they reach users.
Read full answerHide full answer
Applies toAndroid 16 (API 36) with Kotlin 2.x and Jetpack Compose
How Android Memory Works
Each app runs in its own process with a dedicated heap for objects; when the heap fills or the system needs memory, the garbage collector (GC) frees objects that no longer have live references.
Android does not let you manually free memory; instead it enforces a per-app memory class limit and may kill your process if you exceed it.
The three common failure modes are leaks (objects kept alive past their useful life), allocation churn (creating many short-lived objects, triggering frequent GC and jank), and OOM (out-of-memory) crashes.
Common Leak Sources and Fixes
Static references to Context or Views: a static field holding an Activity keeps the entire view hierarchy alive. Store applicationContext instead, or move state into a ViewModel.
Non-static inner classes and anonymous listeners: they implicitly capture the outer class instance. Use static nested classes, top-level functions, or lifecycle-aware scopes.
Unregistered receivers, listeners, or callbacks: always unregister in onDestroy or onStop, or prefer lifecycle-aware APIs such as LifecycleCoroutineScope.
Coroutines launched without a scope: launch in viewModelScope or lifecycleScope so jobs cancel automatically when the component is destroyed.
Large Bitmaps: load downsampled images using BitmapFactory.Options.inSampleSize or an image loader like Coil, which pools and recycles bitmaps for you.
Detection Tools
Android Studio Profiler: inspect heap dumps, record allocations in real time, and identify retained objects and their reference chains.
LeakCanary: open-source library (com.squareup.leakcanary:leakcanary-android) that automatically detects leaks at runtime and displays the leak trace in a debug build.
Lint checks: catch known leak patterns (such as ViewHolder or Handler leaks) at build time.
Compose-Specific Considerations
Composables recompose frequently; avoid capturing heavy objects in lambdas whose identity changes on every recomposition, since that forces downstream recomposition and allocations.
Use remember and derivedStateOf to cache expensive results across recompositions.
Load images with Coil, which handles caching, resizing, and bitmap pooling automatically.
Best Practices
Prefer lifecycle-aware components (ViewModel, StateFlow, WorkManager) over manual background threads. A typical pattern is a @HiltViewModel that exposes StateFlow and performs work inside viewModelScope, so the UI observes state and the coroutine cancels with the UI.
Do not use AsyncTask or IntentService (both deprecated since API 30); replace them with coroutines and WorkManager.
Profile before optimizing; measure real allocation hotspots rather than guessing.
Test on low-memory devices and enable “Don’t keep activities” in developer options to surface leaks early.
Keep debug-only tools like LeakCanary out of release builds using a separate debug source set.
15
Which layout types does Android provide for arranging UI elements, and how do they differ?
KnownRevise
Android offers two distinct layout systems: the classic View-system layouts (XML ViewGroup subclasses such as LinearLayout, ConstraintLayout and FrameLayout) and the composable layouts of Jetpack Compose (Column, Row, Box and others), which are now the default way to build UI.
Read full answerHide full answer
Applies toAndroid 16 (API 36) with Jetpack Compose; XML View layouts are covered for View-based and legacy apps.
View-System Layouts (XML)
LinearLayout: stacks children in a single direction, controlled by android:orientation, and distributes space with android:layout_weight. Simple and predictable, but nested LinearLayout chains quickly deepen the hierarchy.
ConstraintLayout: the recommended layout for complex screens in the View system. Children are positioned with constraints to other views, the parent, guidelines, barriers and chains, letting you build large, flat hierarchies instead of nested ones. It ships via the androidx.constraintlayout:constraintlayout library and is the default in new View-based projects.
RelativeLayout: positions children relative to the parent or to sibling views. It is still supported but is effectively legacy — Google recommends ConstraintLayout for new layouts.
FrameLayout: stacks children on top of each other along the Z axis; each child is anchored from the top-left unless gravity is set. It is the traditional container for a single child or for hosting fragments.
GridLayout: places children in a rectangular grid of rows and columns with optional spanning; available as androidx.gridlayout:gridlayout.
TableLayout: arranges children into rows (TableRow) and columns. It is a legacy API with limited flexibility; GridLayout or ConstraintLayout are preferred.
CoordinatorLayout: a Material Design ViewGroup that coordinates dependent child views, enabling behaviors such as a collapsing AppBarLayout, a FloatingActionButton reacting to a Snackbar, and bottom-sheet dragging.
Specialized containers: DrawerLayout for navigation drawers, SlidingPaneLayout for master/detail panes, SwipeRefreshLayout for pull-to-refresh, and ViewPager2 for paged content.
Compose Layout Equivalents
Column / Row: the direct analogues of vertical and horizontal LinearLayout, with Arrangement and Alignment parameters and Modifier.weight for distribution.
Box: stacks children on top of each other, matching FrameLayout, with ContentAlignment.
FlowRow / FlowColumn: wrap children onto multiple lines when they exceed the available width or height (Compose Foundation).
LazyColumn / LazyRow: compose and recycle only the visible items, replacing RecyclerView for scrolling lists.
ConstraintLayout (Compose): available via androidx.constraintlayout:constraintlayout-compose for complex constraint-based designs, though standard Column/Row/Box nesting is idiomatic for most screens.
BoxWithConstraints and custom Layout: expose the measurement and placement pass when you need a bespoke layout algorithm.
Choosing a Layout
Need
View system
Compose
Simple linear stack
LinearLayout
Column / Row
Complex, flat design
ConstraintLayout
Column/Row/Box, or Compose ConstraintLayout
Overlapping views
FrameLayout
Box
Grid
GridLayout
LazyVerticalGrid / FlowRow
Scrolling list
RecyclerView
LazyColumn / LazyRow
Material coordination
CoordinatorLayout
Scaffold with TopAppBar and NestedScrollConnection
Deprecated or Legacy Layouts
AbsoluteLayout: deprecated since API 3; use FrameLayout, LinearLayout or ConstraintLayout instead.
PercentRelativeLayout / PercentFrameLayout: deprecated since API 26; use ConstraintLayout with guidelines, app:layout_constraintHorizontal_percent, or dimension ratios.
TableLayout and heavily nested RelativeLayout/LinearLayout structures: still functional, but ConstraintLayout or Compose produce flatter, faster hierarchies.
Best Practices
Keep hierarchies flat: every nested container adds measure and layout passes that cost frame time.
For new apps, build with Jetpack Compose; use View-system layouts only when maintaining or interoperating with existing XML code.
Reuse layouts with <include> and flatten the merge point with <merge> in View-based code.
Prefer ConstraintLayout chains and guidelines over deep LinearLayout nesting, and use Space or padding rather than empty placeholder views.
Verify results with Layout Inspector and check for overdraw, since layout complexity is a common source of jank on low-end devices.
16
What function do Adapters serve in Android, and when are they still relevant?
KnownRevise
An Adapter acts as the bridge between a data source (a list, database cursor, or paginated stream) and a ViewGroup that displays items: it creates the views, binds each item’s data into them, and reports how many items exist. In modern Android with Jetpack Compose, this role is handled internally by lazy list components such as LazyColumn, so adapters are mainly relevant when you work with the legacy View system or integrate Compose into an existing View-based screen.
Read full answerHide full answer
Applies toAndroid 16 (API 36) with Kotlin 2.x; adapter classes belong to the View-based toolkit, while the Compose equivalents apply to Jetpack Compose 1.x.
What an Adapter Does
Item count: reports how many items the data set contains so the container knows how much to render.
View creation: inflates or reuses a layout for a given item when none is available for recycling.
Data binding: copies fields from a data object into the appropriate widgets (for example, a string into a TextView).
View recycling: hands scrolled-off views back to the container so they can be reused, which keeps memory flat regardless of list length.
Identity and events: supplies stable IDs and item view types when requested, and is a common place to attach click listeners that route user interaction back to the view model.
Types of Adapters
Adapter
Used with
Notes
BaseAdapter
ListView, GridView, Spinner
Generic base class you subclass for full control.
ArrayAdapter
ListView, Spinner
Simplest option for a list of objects with a single TextView per row.
CursorAdapter
ListView backed by a database cursor
Legacy; prefer Room with RecyclerView or Compose.
RecyclerView.Adapter
RecyclerView
The standard View-based choice, with a required ViewHolder.
ListAdapter
RecyclerView
Subclass of RecyclerView.Adapter that computes diffs on a background thread via DiffUtil.ItemCallback.
FragmentStateAdapter
ViewPager2
Replaces the deprecated FragmentPagerAdapter and FragmentStatePagerAdapter.
PagingDataAdapter
RecyclerView with Paging 3
Handles paged loading from sources such as Room’s PagingSource or a Retrofit API.
AsyncTask-style loaders such as LoaderManager-backed cursor adapters are also legacy; use Kotlin coroutines and Flow with Room instead.
How It Works with RecyclerView
The RecyclerView never touches your data directly. It asks the adapter for the item count, requests a ViewHolder for a given position, then calls back to bind that position. Scroll-off views go into a RecycledViewPool, so onCreateViewHolder runs only a limited number of times while onBindViewHolder runs repeatedly as the user scrolls. Because binding can be called many times, keep it cheap: avoid inflation and heavy object allocation there, and never block the main thread with disk or network reads.
For lists that change over time, ListAdapter with a DiffUtil.ItemCallback is preferred over calling notifyDataSetChanged(): it computes the minimal set of insertions, moves and deletions, so only the affected rows animate. notifyDataSetChanged is still valid but invalidates the whole list and loses change animations.
Adapters in Jetpack Compose
Compose has no adapter classes. LazyColumn and LazyRow take an item lambda, and the composables that scroll out of view are disposed and their slots reused — the same recycling concept, expressed as a declarative API. When you need paging, LazyPagingItems from androidx.paging:paging-compose plays the role of PagingDataAdapter. To embed a View-based adapter inside Compose, use AndroidView with a RecyclerView; to embed Compose inside a RecyclerView, use ComposeView in the ViewHolder.
Best Practices and Common Pitfalls
Prefer Compose or RecyclerView over ListView/GridView for new View-based screens; ListView recycles views via convertView regardless, but without the ViewHolder pattern you still pay for repeated findViewById calls, which is easy to get wrong and hurts performance.
Use ListAdapter + DiffUtil for mutable lists instead of manual notifyItem* calls, and make areItemsTheSame/areContentsTheSame correct — wrong comparisons cause flicker or missing updates.
Set stable IDs (setHasStableIds(true)) only when IDs are genuinely stable; enabling it with unstable IDs causes position mismatches.
Keep data immutable in the adapter and expose mutation through the view model, so the adapter’s diffing sees consistent snapshots.
Do not do network or database work in onBindViewHolder; collect data with coroutines on Dispatchers.IO and push the result in via StateFlow.
Handle item view types through getItemViewType when a list mixes layouts, and never cast a ViewHolder to the wrong type.
17
What steps do you follow to build your own View and ViewGroup classes in Android?
KnownRevise
You create a custom View by subclassing View (or an existing widget) and overriding onDraw() and onMeasure(), and a custom Layout by subclassing ViewGroup and overriding onMeasure() and onLayout() to size and position child views. Both read custom XML attributes through a declare-styleable block and a TypedArray.
Read full answerHide full answer
Applies toAndroid 16 (API 36) with Kotlin 2.x and the View system
Custom Views
Subclass View: extend View directly for a blank canvas, or extend a concrete widget like AppCompatTextView to augment existing behaviour.
Constructors: provide at least (Context) and (Context, AttributeSet) so the view inflates from XML; add (Context, AttributeSet, Int) to handle style and theme attributes.
onMeasure(): resolve width and height from the incoming measure specs using MeasureSpec.getSize() and MeasureSpec.getMode(), then call setMeasuredDimension().
onDraw(): draw with the provided Canvas and Paint; never allocate new Paint or Path objects inside onDraw() because it runs every frame and causes jank.
Custom Layouts (ViewGroup)
Subclass ViewGroup: a custom layout is a ViewGroup that decides where its children go; you are responsible for measuring and laying out every child.
onMeasure(): iterate over childCount, call measureChildWithMargins() or measureChild() on each child, accumulate their sizes, add your own padding, then call setMeasuredDimension() for the parent.
onLayout(): call child.layout(left, top, right, bottom) for every child, offset by your padding; this is where you implement your arrangement logic such as stacking, flowing, or grid placement.
LayoutParams: supply your own LayoutParams subclass when children need extra layout information, such as a weight or custom positioning hint.
Declaring Custom Attributes
attrs.xml: add a <declare-styleable> block in res/values/attrs.xml listing each attribute name and its format (color, dimension, string, boolean).
Read attributes: call context.obtainStyledAttributes(attrs, R.styleable.MyView) in the constructor, extract values with getColor(), getDimension(), etc., and always call recycle() on the TypedArray.
class CircleView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {
private val paint = Paint(Paint.ANTI_ALIAS_FLAG)
private var radius = 0f
init {
val a = context.obtainStyledAttributes(attrs, R.styleable.CircleView)
paint.color = a.getColor(R.styleable.CircleView_circleColor, Color.BLUE)
radius = a.getDimension(R.styleable.CircleView_circleRadius, 50f)
a.recycle()
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
val size = (radius * 2).toInt()
setMeasuredDimension(
resolveSize(size, widthMeasureSpec),
resolveSize(size, heightMeasureSpec)
)
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
canvas.drawCircle(width / 2f, height / 2f, radius, paint)
}
}
class StackLayout @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : ViewGroup(context, attrs, defStyleAttr) {
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
var maxWidth = 0
var totalHeight = 0
measureChildren(widthMeasureSpec, heightMeasureSpec)
for (i in 0 until childCount) {
val child = getChildAt(i)
maxWidth = maxOf(maxWidth, child.measuredWidth)
totalHeight += child.measuredHeight
}
// Include the ViewGroup's own padding in the measured size
maxWidth += paddingLeft + paddingRight
totalHeight += paddingTop + paddingBottom
setMeasuredDimension(
resolveSize(maxWidth, widthMeasureSpec),
resolveSize(totalHeight, heightMeasureSpec)
)
}
override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
var top = paddingTop
val left = paddingLeft
for (i in 0 until childCount) {
val child = getChildAt(i)
child.layout(left, top, left + child.measuredWidth, top + child.measuredHeight)
top += child.measuredHeight
}
}
}
Best Practices and Pitfalls
Prefer Compose for new UI: Jetpack Compose is the modern recommended toolkit; build custom Views only when you need a reusable control that integrates with existing View-based code or when Compose lacks a specific primitive.
Avoid allocation in onDraw(): reuse Paint, Path, and Rect instances as fields to prevent garbage-collection stutter.
Respect padding and margins: include paddingLeft, paddingTop, etc. in onMeasure() and offset children by padding in onLayout(), otherwise children draw over the padded area; complex layouts should also honour child LayoutParams margins.
Support accessibility: set contentDescription or override onInitializeAccessibilityNodeInfo() so screen readers describe your custom control.
Handle RTL: respect layoutDirection and use resolveSize() so your layout behaves correctly in right-to-left locales.
Recycle TypedArray: forgetting recycle() leaks resources and can crash under memory pressure.
18
Which architectural pattern is predominantly adopted for modern Android UI development?
KnownRevise
Modern Android UI development predominantly adopts the Model-View-ViewModel (MVVM) pattern, typically enhanced with unidirectional data flow (UDF) principles associated with Model-View-Intent (MVI). The ViewModel owns and exposes UI state, Jetpack Compose renders that state declaratively, and events flow upward from the UI to the ViewModel.
Model: The data layer, including repositories, Room databases, and Retrofit services. It fetches and persists data without referencing the UI.
View: The Composable functions that observe state and render the screen. They should contain only presentation logic and emit user events to the ViewModel.
ViewModel: A lifecycle-aware intermediary that survives configuration changes, exposes immutable StateFlow<UiState>, handles user events, and orchestrates data operations through coroutines.
Declarative UI and Unidirectional Data Flow
Jetpack Compose treats the UI as a function of state, so when the ViewModel emits a new state, Compose recomposes only the affected components. UDF makes this predictable: user events travel upward from View to ViewModel, while state travels downward from ViewModel to View. Because the View cannot directly mutate the ViewModel’s state, there are no hidden side effects. The viewModel() composable comes from androidx.lifecycle:viewmodel-compose, while hiltViewModel() comes from androidx.hilt:hilt-navigation-compose; collectAsStateWithLifecycle() is provided by androidx.lifecycle:lifecycle-runtime-compose and automatically stops collection when the UI is not at least started.
How MVVM Relates to MVI
Aspect
MVVM
MVI-style UDF
State exposure
StateFlow or similar observable holders
Single immutable state stream
Event handling
Multiple per-UI event functions
All events funnel through one intent/event channel
Typical Android use
Default recommended architecture
Often implemented inside a ViewModel as a state-reducer
MVI is usually a refinement of MVVM rather than a replacement; most production Android code still describes its architecture as MVVM.
Best Practices and Pitfalls
State hoisting: Keep UI state in the ViewModel when it must survive recomposition or be shared across composables.
Configuration changes: Because the ViewModel is scoped to the activity or navigation graph, rotating the device does not require reloading data.
Avoid context leaks: Never pass Context or Activity into a ViewModel; inject resources or string providers instead.
Lifecycle-aware observation: Prefer collectAsStateWithLifecycle() over collectAsState() to avoid unnecessary work when the UI is in the background.
Testability: Keep ViewModel logic pure by injecting fake or test repositories, enabling JVM unit tests without Android instrumentation.
Thread safety: viewModelScope.launch runs on Dispatchers.Main by default, so network and disk work must happen inside repository functions that switch to Dispatchers.IO, never directly on the main thread.
Immutable state exposure: Expose StateFlow, not MutableStateFlow, so only the ViewModel can change state.
19
Explain how an Activity differs from a Fragment in Android development, including when you would choose one over the other.
KnownRevise
An Activity is an independent application component that provides a single screen with its own window and full lifecycle, whereas a Fragment is a modular, reusable portion of UI and behavior that must be hosted inside an Activity. An Activity can run on its own and must be declared in the AndroidManifest.xml; a Fragment cannot exist independently and is managed through a FragmentManager attached to its host.
Activity: Represents one screen of an app. It owns the window, handles system-level events such as permissions and configuration changes, and is the entry point the system uses to launch your app. It must be declared in AndroidManifest.xml with an <activity> element, and the system can instantiate it directly via an Intent.
Fragment: Represents a reusable section of UI or behavior. Multiple fragments can be combined into a single Activity to build flexible, multi-pane layouts that adapt to phones, tablets, and foldables. A fragment is not declared in the manifest; it is instantiated by your code or by the FragmentManager and always requires a host Activity (or a parent fragment for child fragments).
Lifecycle
Activity lifecycle: onCreate, onStart, onResume, onPause, onStop, onDestroy. The Activity is fully responsible for its own lifecycle.
Fragment lifecycle: onAttach, onCreate, onCreateView, onViewCreated, onStart, onResume, onPause, onStop, onDestroyView, onDestroy, onDetach. The Fragment lifecycle is tied to the host Activity — when the Activity stops, all its fragments stop. onDestroyView is called separately from onDestroy, which matters for cleaning up view bindings and avoiding memory leaks.
Communication and State Sharing
Fragments should not talk to each other directly. The recommended pattern is a shared ViewModel scoped to the host Activity using the activityViewModels() delegate, so both fragments observe the same StateFlow.
For one-off results between fragments, use the Fragment Result API from fragment-ktx instead of the deprecated startActivityForResult approach.
Navigation between fragments is handled by the Navigation Component, which manages the back stack through the FragmentManager.
Comparison Table
Aspect
Activity
Fragment
Independence
Can run alone
Must be hosted
Manifest entry
Required
Not required
Window
Owns a window
Uses host’s window
Lifecycle
Simpler, self-owned
Richer, tied to host
Reusability
Low
High across screens
Back stack
Task back stack
FragmentManager back stack
Context
Is a Context
Access via requireContext()
When to Choose
Choose an Activity for a distinct, top-level screen or task that the system or other apps can launch directly via an Intent, and for anything that needs its own window or task affinity.
Choose a Fragment for reusable, composable UI sections, multi-pane layouts on tablets and foldables, or swiping pages with ViewPager2, where a single Activity hosts several fragments.
With Jetpack Compose, the need for fragments decreases: Navigation Compose uses a single Activity with a NavHost and composable destinations, and UI reuse happens through composable functions rather than fragments. Fragments remain appropriate when integrating legacy XML views, permission flows, or features that rely on the Fragment lifecycle.
Best Practices
Prefer a single-Activity architecture with multiple fragments (or Compose destinations) for navigation.
Scope shared ViewModels to the Activity when fragments need to share state.
Clean up view resources in onDestroyView, not onDestroy, to avoid leaks.
Do not hold references to Views or Context outside the fragment’s view lifecycle.
Use the Fragment Result API or a shared ViewModel for fragment-to-fragment communication, never direct fragment references.
20
Can you describe what the UI thread is in Android and explain why keeping it unblocked is critical for app performance?
KnownRevise
The UI thread, commonly referred to as the main thread, is the single thread in an Android process responsible for rendering interface components and dispatching user input events to your app. Because all view updates and touch handling occur here, any long-running operation executed on this thread freezes the interface and triggers an Application Not Responding (ANR) error.
Read full answerHide full answer
Applies toAndroid 16 (API 36) and Kotlin 2.x
Core Responsibilities
Event Loop: The thread runs a Looper that processes a message queue, handling input events, lifecycle callbacks, and scheduled tasks sequentially.
Rendering: It works with the Choreographer to draw frames, typically targeting 60 frames per second, which gives roughly 16 milliseconds per frame to complete work.
Input Dispatching: Touches, clicks, and gestures are delivered here; if the queue stalls, the system cannot register user interaction.
The ANR Problem
Input Dispatching Timeout: If the thread cannot process an input event within about 5 seconds, the system shows an ANR dialog to the user.
Broadcast and Service Limits: A BroadcastReceiver has roughly 10 seconds, and a foreground service roughly 20 seconds, before the system considers the app unresponsive.
User Perception: Even short delays of 100 to 300 milliseconds feel like lag or jank to the user, degrading experience long before a hard ANR occurs.
Threading Model and Safety
Single-Threaded UI: Android UI toolkits, including both XML Views and Jetpack Compose, are not thread-safe. You must never modify UI objects from a background thread.
Compose State: In Jetpack Compose, updating state objects triggers recomposition, which must always happen on the UI thread.
Legacy APIs: Older approaches like AsyncTask (deprecated in API 30) managed threading manually; the modern replacement is Kotlin coroutines with structured concurrency.
Best Practices and Pitfalls
Use Coroutines: Launch work in viewModelScope or lifecycleScope and switch contexts using withContext(Dispatchers.IO) for disk or network operations.
Background Services: For work that must continue after the UI disappears, use WorkManager rather than trying to keep the UI thread alive.
Avoid Blocking Calls: Never call .await(), .get(), or synchronous network methods on the main thread, even inside a coroutine launched there.
Profiling: Use Android Studio Profiler to detect jank and main-thread violations during development.
21
Can you explain the differences between the Android dimension units dp, dip, sp, and px, and describe the appropriate use case for each one?
KnownRevise
These are the measurement units Android uses to define layout sizes, distances, and text dimensions on screen. dp (density-independent pixels) and sp (scale-independent pixels) are the recommended units for consistent rendering across devices, while px (physical pixels) should generally be avoided in layout code.
Read full answerHide full answer
Applies toAndroid 16 (API 36) and all previous API levels
What Each Unit Represents
px: Physical pixels on the screen. This is the actual hardware unit. Because screen densities vary wildly, a fixed number of px results in different physical sizes on different devices.
dp (or dip): Density-independent pixels. One dp equals one px on a 160 dpi screen. The system scales dp automatically based on the device’s density bucket, so a button defined as 100 dp wide appears roughly the same physical size on a low-density and high-density screen. dip is an exact alias for dp; use dp.
sp: Scale-independent pixels. This unit is based on dp but also scales according to the user’s system-wide font size preference. It is designed specifically for typography to support accessibility.
Layout dimensions (can break alignment if user scales fonts)
px
Almost nothing; occasionally low-level canvas drawing where physical precision is required
Layouts, text, or any user-facing dimension
dip
Legacy codebases
New code (use dp instead)
How the Units Are Expressed in Code
Jetpack Compose: dp and sp are strongly typed as Dp and TextUnit, so you cannot accidentally pass a raw number. You write 100.dp for sizes and 16.sp for font sizes, importing androidx.compose.ui.unit.dp and androidx.compose.ui.unit.sp.
XML Views: The units are string suffixes on attribute values, such as android:layout_width="100dp" and android:textSize="16sp".
Common Pitfalls
Hardcoding px: Defining a margin as 10px looks fine on an emulator but may appear microscopic on a high-density flagship phone. Always use dp.
Using dp for text: If you set fontSize in dp, users with vision impairments who increase the system font scale will not see your text grow. This violates accessibility best practices.
Using sp for layout: If a user sets their font scale to very large, a container sized in sp may expand unexpectedly, breaking your layout grid. Keep layout in dp.
Mixing units in calculations: In Compose, you cannot multiply a Dp by a raw Int without conversion. Use extension functions like dp.toPx() inside a Density scope if physical pixels are truly needed for canvas work.
These units have been stable since API 1, so behavior is consistent across all Android versions.
22
How should developers manage screen orientation changes and configuration updates in modern Android apps using Jetpack Compose?
KnownRevise
The recommended approach is to store UI state in a ViewModel, which survives configuration changes automatically, while Jetpack Compose handles UI recomposition based on that state. You should avoid manually saving instance state for complex UI data unless necessary for process death, and never prevent recreation using android:configChanges unless handling specific hardware scenarios.
When a device rotates, Android destroys and recreates the Activity and Compose composition by default to load alternative resources, such as layouts designed for landscape. Any state held solely in the Composable or Activity is lost during this process. The system triggers a configuration change, signaling the app to reload resources matching the new orientation, which is why retaining logic outside the UI lifecycle is critical.
ViewModel Survival Mechanism
The ViewModel class is scoped to the Activity or Fragment and retains its instance across configuration changes. Because it lives outside the UI lifecycle, data stored there persists through rotation. You expose state via StateFlow, allowing the UI to observe changes without holding the data itself. This separation ensures business logic and UI state remain intact regardless of screen orientation changes.
Jetpack Compose Integration
In Compose, you retrieve the ViewModel using the viewModel() function from androidx.lifecycle:lifecycle-viewmodel-compose. The Composable observes the state and recomposes automatically when orientation changes trigger a new composition. Since the ViewModel provides the same state instance, the UI appears unchanged to the user. You do not need to write explicit rotation handling code for standard state preservation.
When Orientation Logic Is Required
Occasionally, you must adapt the UI layout based on orientation, such as switching from a list to a grid. Use LocalConfiguration.current.orientation to detect the current mode. This value triggers recomposition when changed, allowing conditional UI logic without losing ViewModel state. Avoid storing this orientation value in the ViewModel unless it affects business logic, as it is purely a presentation concern.
Code Example
// ViewModel holding UI state using StateFlow
class ScreenViewModel : ViewModel() {
private val _uiState = MutableStateFlow<String>("Initial")
val uiState: StateFlow<String> = _uiState
fun updateText(text: String) {
_uiState.value = text
}
}
// Composable observing state and checking orientation
@Composable
fun ScreenScreen(viewModel: ScreenViewModel = viewModel()) {
val orientation = LocalConfiguration.current.orientation
val text by viewModel.uiState.collectAsState()
Column {
Text(
text = if (orientation == Configuration.ORIENTATION_LANDSCAPE)
"Landscape" else "Portrait"
)
Text(text = text)
Button(onClick = { viewModel.updateText("Updated") }) {
Text("Update")
}
}
}
Note that viewModel() ensures the same instance is returned after rotation. The LocalConfiguration access allows layout adjustments without persisting orientation in the ViewModel. Dependencies should be declared in libs.versions.toml using androidx-lifecycle-viewmodel-compose.
Best Practices and Pitfalls
Avoid android:configChanges: Preventing recreation stops resource reloading, such as alternative layouts, and is discouraged unless managing heavy resources like video players.
Process Death vs. Rotation: ViewModel survives rotation but not process death. Use rememberSaveable in Compose, or a SavedStateHandle inside the ViewModel, to restore simple UI state after the system kills the app process; use persistent storage (Room, DataStore) for anything that must survive a full app restart.
Main Thread Safety: Never perform disk or network operations directly in the Composable. Use coroutines with Dispatchers.IO inside the ViewModel.
Dependency Injection: Use Hilt to provide ViewModel instances, ensuring dependencies survive configuration changes alongside the ViewModel.
23
What approaches do you use to make an Android app adapt correctly to various screen sizes and pixel densities?
KnownRevise
You achieve this through responsive design built on density-independent units, adaptive layouts driven by window size classes, and alternative resources qualified by screen characteristics. Rather than hard-coding pixel values, you let Compose and the resource system select the right layout, dimensions and drawables for each device configuration.
Android classifies screens by density buckets (mdpi, hdpi, xhdpi, xxhdpi, xxxhdpi) and by smallest width in dp. To stay consistent across them:
dp (density-independent pixels): use for all layout dimensions, margins and padding; the system scales dp to physical pixels per density.
sp (scale-independent pixels): use for font sizes so text respects the user’s font-scale accessibility setting.
Never use px for layout; a value that looks right on one density will be too small or large on another.
Vector drawables: prefer ImageVector and XML vector assets over bitmaps so icons scale without separate assets per density.
Window Size Classes
The recommended way to adapt layout structure is window size classes from androidx.window:window, which categorize the current window as compact, medium or expanded based on width and height breakpoints. This replaces the older material3-window-size-class library, which is deprecated in favour of androidx.window.
Size class
Typical devices
Layout strategy
Compact
Phones, folded foldables
Single pane, bottom navigation
Medium
Small tablets, unfolded foldables
Two panes or side-by-side lists
Expanded
Tablets, desktops, unfolded foldables
Permanent navigation rail, multi-pane
You compute the WindowSizeClass from the Activity’s current window metrics and read its widthSizeClass and heightSizeClass properties, then pass the class down to screens so each screen decides its arrangement. This also handles foldables and multi-window mode, not just static device sizes.
Resource Qualifiers
For assets and dimensions that differ more than Compose can compute, use configuration qualifiers in res/:
values-sw<N>dp/: smallest-width qualifiers (e.g. values-sw600dp) for tablet-specific dimensions.
layout-sw<N>dp/: alternative layouts for wider screens (used mainly in View-based code).
drawable-<density>/: density-specific bitmaps when vectors are not an option.
mipmap-<density>/: launcher icons per density, or use adaptive icons with foreground/background layers.
Compose Layout Techniques
Within Compose itself, prefer BoxWithConstraints when a composable needs to react to the exact dp width available, and drive larger structural changes off the window size class. Use Modifier.fillMaxWidth(), weight() in Row/Column and flow layouts such as FlowRow and FlowColumn (from androidx.compose.foundation.layout) so items wrap instead of overflowing on narrow windows. Keep navigation in a NavigationBar on compact windows and switch to a NavigationRail or PermanentNavigationRail on expanded windows.
Best Practices and Pitfalls
Test with preview: use Compose @Preview with different Devices values (phones, tablets and foldables) to verify each size class.
Support configuration changes: window size classes update automatically on rotation, multi-window and fold state changes; do not cache a stale class.
Avoid fixed max widths on expanded screens unless intentional; let content use available width or center with a max width.
Check touch targets: keep interactive elements at least 48dp for accessibility across densities.
Views legacy note: if maintaining XML layouts, use ConstraintLayout with ConstraintSet and Guideline percentages instead of nested LinearLayout weights for better scaling.
24
What fundamental concepts define the Material Design system used in Android development?
KnownRevise
Material Design is Google’s comprehensive design system that standardizes UI components, interaction patterns, and visual language across Android applications. The current iteration, Material Design 3 (Material You), emphasizes personalization, adaptability, and accessibility to create cohesive and intuitive user experiences.
Read full answerHide full answer
Applies toAndroid 16 (API 36), Material Design 3, Jetpack Compose
Core Principles of Material Design 3
Material Design 3 shifts focus from static guidelines to dynamic systems that respond to user preferences and device contexts. - Personalized: Interfaces adapt to user choices, most notably through dynamic color, where a color scheme is extracted from the user’s wallpaper so each app feels unique to that individual (Android 12 / API 31 and above). - Adaptive: Layouts and components adjust across form factors, from phones to foldables, tablets, and desktops, using canonical window-size classes and adaptive scaffolds. - Accessible: Design decisions prioritize inclusivity, with built-in support for sufficient contrast, large touch targets, and motion preferences.
Design Tokens
The system is built on tokens — named, reusable values that define an application’s look and feel. Tokens are grouped into four categories. - Color: A structured palette of primary, secondary, tertiary, neutral, and error colors, each with tonal roles such as onPrimary (content drawn on top of primary) and surfaceVariant. On API 31+, dynamicLightColorScheme and dynamicDarkColorScheme generate these from the wallpaper. - Typography: A type scale of display, headline, title, body, and label styles, each with defined size, weight, and line height, using fonts such as Roboto or Google Sans to establish hierarchy. - Shape: Component shapes (small, medium, large, extra-large) convey elevation and interaction state consistently across buttons, cards, dialogs, and sheets. - Layout: Grid systems and spacing in 4dp or 8dp increments ensure alignment and visual rhythm.
Implementation in Jetpack Compose
In modern Android development, Material Design is applied through the MaterialTheme composable from the androidx.compose.material3 library. It takes three token sets — colorScheme, typography, and shapes — and exposes them to every composable in the composition hierarchy, so a Button, Card, or TopAppBar picks up the correct styling automatically. Apps typically define a light and a dark ColorScheme, and switch between them based on the system’s dark-theme setting, optionally overriding them with a dynamic scheme on supported devices. Because tokens are just ordinary Kotlin objects, themes can be versioned, branded, or generated at runtime while the components remain unchanged.
Accessibility and Interaction Standards
Adhering to accessibility guidelines is expected for production apps targeting modern Android versions. - Touch targets: Interactive elements should be at least 48dp by 48dp so they can be activated comfortably. - Contrast: Text should meet WCAG 2.1 AA contrast ratios (at least 4.5:1 for normal-sized text). - Content descriptions: Icons and meaningful images should carry a contentDescription (a parameter in Compose, an attribute in XML Views) for screen readers; pass null for purely decorative elements to avoid noise. - Motion: Animations should be meaningful and should respect the user’s “reduce motion” accessibility setting.
Best Practices
Follow platform conventions: Use standard components such as TopAppBar, FloatingActionButton, and NavigationRail unless there is a strong UX reason to deviate.
Avoid over-customization: Excessive deviation from Material tokens can confuse users accustomed to Android patterns and increases maintenance cost.
Test across devices: Verify layouts on foldables, tablets, and different screen densities so the adaptive principle holds in practice.
Use Material 3 components: Prefer androidx.compose.material3 over the older androidx.compose.material (Material 2) library, which is superseded and no longer receives new features.
25
Explain the purpose of SharedPreferences in Android development and describe the recommended approach for reading and writing data using this mechanism.
KnownRevise
SharedPreferences is a built-in Android framework for storing lightweight primitive data as key-value pairs in a private XML file. It is commonly used for simple settings such as user preferences or UI state, though Google now recommends Preferences DataStore for new development because it is asynchronous and supports coroutines and Flow.
Read full answerHide full answer
Applies toAndroid API 1+ (current stable Android 16, API 36, Kotlin 2.x)
Core Concept and Storage
SharedPreferences persists data locally inside the app’s private directory (/data/data/<package>/shared_prefs/). It supports the primitive types String, Int, Boolean, Float, Long, and Set<String>. Data survives app restarts and process death but is removed when the app is uninstalled. Because the file is private, other apps cannot read it without root privileges. It is ideal for small configuration flags such as a dark-mode toggle or an onboarding-completed flag, but unsuitable for relational data or large payloads.
Reading and Writing Operations
Access is gained via Context.getSharedPreferences(name, mode), where mode should always be Context.MODE_PRIVATE; the world-readable/writable modes were deprecated in API 17 and are no longer supported. Writing requires an Editor obtained through edit(). Two methods finalize writes:
apply(): asynchronous and returns Unit, so it is the preferred way to avoid blocking the UI thread.
commit(): synchronous and returns a Boolean for success, but it can cause ANRs when called on the main thread during heavy I/O.
Reading is synchronous: getString(key, default) and its typed siblings return the stored value immediately, or the supplied default when the key is missing. First access loads the file into memory, so reads should still be kept off the main thread when possible.
SharedPreferences vs Preferences DataStore
While functional, SharedPreferences is fundamentally synchronous and can cause main-thread contention. Preferences DataStore (Jetpack DataStore) is the modern replacement: it writes asynchronously with coroutines and exposes data as a Flow for reactive UI updates. Disk errors are not thrown directly; they are emitted through the flow, so callers can catch them. For new projects targeting Android 16, use DataStore, and migrate existing keys with SharedPreferencesMigration during DataStore initialization.
Code Example
The first snippet shows legacy SharedPreferences with all disk access moved off the main thread; the second shows the recommended Preferences DataStore equivalent.
// Legacy SharedPreferences usage (deprecated approach, kept off the main thread)
class LegacySettingsViewModel(private val context: Context) : ViewModel() {
private val prefs = context.getSharedPreferences("app_settings", Context.MODE_PRIVATE)
fun saveTheme(theme: String) {
// apply() is async, but the editor still touches disk first
viewModelScope.launch(Dispatchers.IO) {
prefs.edit().putString(KEY_THEME, theme).apply()
}
}
// Reads are synchronous, so emit them from an I/O-backed flow
fun loadTheme(): Flow<String> = flow {
emit(prefs.getString(KEY_THEME, "system") ?: "system")
}.flowOn(Dispatchers.IO)
companion object {
private const val KEY_THEME = "theme_key"
}
}
// Modern Preferences DataStore usage (recommended)
private object PreferencesKeys {
val THEME = stringPreferencesKey("theme")
}
class SettingsViewModel(private val dataStore: DataStore<Preferences>) : ViewModel() {
val themeFlow: Flow<String> = dataStore.data
.catch { exception ->
if (exception is IOException) emit(emptyPreferences()) else throw exception
}
.map { preferences -> preferences[PreferencesKeys.THEME] ?: "system" }
suspend fun saveTheme(theme: String) {
dataStore.edit { preferences -> preferences[PreferencesKeys.THEME] = theme }
}
}
Best Practices and Security
Never store secrets: do not save passwords, tokens, or PII in plain text. Use EncryptedSharedPreferences from the Security Crypto library or the Android Keystore system.
Define key constants: declare keys as const val in a companion object to avoid magic strings and typos.
Avoid large data: if data exceeds a few kilobytes or needs querying, use Room instead.
Prefer DataStore: it removes the main-thread pitfalls of SharedPreferences and integrates naturally with coroutines and Compose.
26
Can you describe the underlying SQLite storage mechanism on Android and the modern preferred method for accessing it?
KnownRevise
Android ships with a built-in SQLite relational database engine for local structured storage, but modern development abstracts it with the Room persistence library to cut boilerplate and add compile-time safety. Room sits on top of SQLite, handling database creation, schema migrations, and query validation while preventing disk access from blocking the main thread.
Read full answerHide full answer
Applies toAndroid API 36 (Android 16), Kotlin 2.x, Room 2.7.0
How SQLite Stores Data on Android
Embedded, serverless engine: SQLite is compiled into the platform (the android.database.sqlite package), so there is no separate database process. Each database is a single ordinary file under the app’s private storage (/data/data/<package>/databases/), which has been app-private on every API level, so no runtime permission is needed to read and write it.
Page-based B-tree storage: the file is divided into fixed-size pages (4 KB by default); each table and index is a B-tree, rows live in cells inside leaf pages, and large values spill into overflow pages. Because a page cache sits in the app’s process, reads are fast and writes are buffered until commit.
ACID and durability: transactions are atomic. SQLite commits either with a rollback journal (the default DELETE journal mode) or with Write-Ahead Logging (WAL), which lets readers run concurrently with a single writer. Room enables WAL by default, so this is the usual mode in modern apps.
Legacy direct access: the framework’s SQLiteOpenHelper and SQLiteDatabase classes expose raw SQL. They are not formally deprecated, but they are verbose, error-prone, and lack compile-time query validation, so Room is the officially recommended replacement.
Room’s Core Architecture
Room adds three components on top of SQLite. Entities are data classes annotated with @Entity that define table schemas. Data Access Objects (DAOs) are interfaces or abstract classes whose methods are annotated with @Insert, @Query, @Update, or @Delete. The Database class extends RoomDatabase, holds the DAOs, and carries configuration such as the schema version and migration strategy. The KSP annotation processor generates the SQLite plumbing at build time and verifies every @Query string against the schema, so a typo or a renamed column fails the build instead of crashing at runtime.
Concurrency and Coroutines
Disk I/O must never run on the main thread. Marking a DAO function as suspend lets Room execute it on Dispatchers.IO automatically when it is called from a coroutine scope such as viewModelScope. Queries can also return Flow, so the UI observes changes and recomposes reactively without polling.
Integration with the ViewModel
A ViewModel holds the business logic and exposes UI state as a StateFlow, collecting DAO Flows and launching suspend DAO calls inside viewModelScope.launch. Hilt injects the RoomDatabase instance, keeping lifecycle and dependency management clean and testable.
Code Example
Declare the libraries in the version catalog (libs.versions.toml):
[libraries]
room-runtime = { group = "androidx.room", name = "room-runtime", version = "2.7.0" }
room-ktx = { group = "androidx.room", name = "room-ktx", version = "2.7.0" }
room-compiler = { group = "androidx.room", name = "room-compiler", version = "2.7.0" }
Add the KSP plugin and the dependencies in build.gradle.kts:
@Entity
data class User(@PrimaryKey val id: Int, val name: String)
@Dao
interface UserDao {
@Query("SELECT * FROM user")
fun getAll(): Flow<List<User>> // reactive; emits on every change
@Insert
suspend fun insert(user: User) // runs off the main thread
}
@Database(entities = [User::class], version = 1)
abstract class AppDatabase : RoomDatabase() {
abstract fun userDao(): UserDao
}
Build the single instance with Room.databaseBuilder(context, AppDatabase::class.java, "app.db"), typically provided through Hilt.
Best Practices and Pitfalls
Migrations: supply explicit Migration objects whenever the schema version changes so existing data is preserved; treat fallbackToDestructiveMigration as a development-only convenience, never a production strategy.
Parameterized queries: bind query arguments as method parameters so Room parameterizes them, preventing SQL injection; never concatenate user input into a @Query string.
Testing: run DAO tests with AndroidJUnitRunner against an in-memory database built with Room.inMemoryDatabaseBuilder, which verifies SQL without touching device storage.
Legacy APIs: avoid hand-written SQLiteOpenHelper code in new projects; it lacks compile-time checks and migration support that Room provides.
27
How do you implement local data persistence in an Android application using the Room Persistence Library?
KnownRevise
The Room Persistence Library acts as an abstraction layer over SQLite, enabling robust local data management while ensuring SQL queries are verified at compile time. It integrates with Kotlin coroutines and Flow so database work never blocks the main thread, fitting the recommended Jetpack architecture of ViewModel, StateFlow, and Hilt.
Room relies on three major components working together: - Entity: Data classes annotated with @Entity represent database tables. Each field becomes a column unless marked with @Ignore. - DAO (Data Access Object): Interfaces annotated with @Dao define data access. Queries are written in SQL with @Query, or generated with @Insert, @Update, and @Delete. - Database: An abstract class extending RoomDatabase annotated with @Database is the main access point, listing entities and defining the schema version.
Gradle Configuration
Declare dependencies in build.gradle.kts using a version catalog (libs.versions.toml). Kotlin Symbol Processing (KSP) is the recommended annotation processor instead of kapt.
# libs.versions.toml
[plugins]
ksp = { id = "com.google.devtools.ksp", version = "2.0.20-1.0.24" }
[libraries]
room-runtime = { group = "androidx.room", name = "room-runtime", version = "2.6.1" }
room-ktx = { group = "androidx.room", name = "room-ktx", version = "2.6.1" }
room-compiler = { group = "androidx.room", name = "room-compiler", version = "2.6.1" }
Architecture Integration
DAO methods should be suspend functions or return Flow, letting Room run them off the main thread automatically without manual Dispatchers.IO. A Repository sits between the ViewModel and the DAO, exposing data as StateFlow so the UI reacts to changes. - ViewModel: Uses viewModelScope and stateIn to expose a StateFlow to the UI. - UI: Jetpack Compose collects the StateFlow and recomposes when data changes. - DI: Hilt provides the database and repository; annotate the ViewModel with @HiltViewModel.
Code Example
This snippet shows the full flow from entity to a Hilt-injected ViewModel.
@Entity(tableName = "users")
data class User(@PrimaryKey val id: Int, val name: String)
@Dao
interface UserDao {
@Query("SELECT * FROM users")
fun getAll(): Flow<List<User>>
@Insert
suspend fun insert(user: User)
}
@Database(entities = [User::class], version = 1)
abstract class AppDatabase : RoomDatabase() {
abstract fun userDao(): UserDao
}
// Build once, e.g. in a Hilt @Module or Application class
fun getDatabase(context: Context): AppDatabase =
Room.databaseBuilder(context.applicationContext, AppDatabase::class.java, "app.db")
.build()
class UserRepository(private val userDao: UserDao) {
val users: Flow<List<User>> = userDao.getAll()
suspend fun insert(user: User) = userDao.insert(user)
}
@HiltViewModel
class UserViewModel @Inject constructor(
private val repository: UserRepository
) : ViewModel() {
val users: StateFlow<List<User>> = repository.users.stateIn(
viewModelScope, SharingStarted.Lazily, emptyList()
)
}
Migrations and Best Practices
Schema changes require Migration objects that specify start and end versions with the SQL needed to alter tables. Avoid fallbackToDestructiveMigration in production because it deletes all user data. - Compile-time Verification: Room validates SQL at compilation, reducing runtime crashes. - Security: Use parameterized queries in @Query (e.g., :userId) to prevent SQL injection rather than string concatenation. - Threading: Never call suspend DAO methods from the main thread without a coroutine scope; Room handles the dispatcher for you. - Testing: Use Room.inMemoryDatabaseBuilder for isolated tests that do not persist data between runs.
28
Can you explain what the Android Loader framework was used for, and what has replaced it?
KnownRevise
A Loader was an Android framework component (introduced in API 11, Honeycomb) that asynchronously loaded data in an activity or fragment, survived configuration changes such as rotation, and delivered results back to the UI. The whole framework — LoaderManager, AsyncTaskLoader and CursorLoader — was deprecated in API 28 (Android 9 Pie) and is not recommended for new code; the officially supported replacement is a ViewModel with Kotlin coroutines and Flow/StateFlow, backed by Room, Retrofit/OkHttp or WorkManager depending on the data source.
Read full answerHide full answer
Applies toAndroid 16 (API 36), Kotlin 2.x; the deprecation described here applies since API 28.
What a Loader Did
Asynchronous loading: data was fetched on a background thread, so the main thread stayed responsive — the direct predecessor of this pattern was AsyncTask, itself deprecated in API 30.
Configuration-change survival: a loader kept its data when the activity was recreated on rotation, so the query did not run again.
Result delivery: callbacks (LoaderManager.LoaderCallbacks) notified the UI when data arrived or changed.
Lifecycle awareness: LoaderManager lived inside the activity/fragment lifecycle and managed loader instances automatically.
The Loader Family
Class
Purpose
Modern replacement
LoaderManager
Managed loader instances tied to a lifecycle owner
ViewModel + viewModelScope
AsyncTaskLoader
Generic background data loading
Coroutines on Dispatchers.IO
CursorLoader
Loaded a Cursor from a ContentProvider
Room DAO returning Flow<List<T>>
Why It Was Deprecated
Loaders were tightly coupled to the activity and fragment lifecycles, used a complex callback API, and were awkward to test and reuse. Coroutines and Flow provide the same guarantees — background execution, cancellation tied to the ViewModel scope, and streaming updates — with far simpler, testable code that works identically on phones, tablets and foldables.
The Modern Replacement
One-off loads: a ViewModel function launched in viewModelScope on Dispatchers.IO, exposing the result as a StateFlow — the ViewModel survives rotation by default, so the data survives with it, and viewModelScope is cancelled automatically when the ViewModel is cleared.
Reactive/streaming data: Room queries return Flow, so the UI automatically recomposes when the database changes — the direct successor to CursorLoader‘s auto-update behaviour. In a Jetpack Compose UI the flow is collected with collectAsStateWithLifecycle(), which stops collection when the lifecycle drops below STARTED.
Network calls: Retrofit/OkHttp suspending functions called from viewModelScope on Dispatchers.IO; never call them from the main thread.
Persistent background work: WorkManager for tasks that must survive process death (uploads, syncs), with foreground service types declared on API 34+.
Migration Guidance
Move the loading logic out of the activity or fragment into a ViewModel; the data then survives rotation by default, which was the main reason loaders existed.
Replace CursorLoader over a ContentProvider with Room or a Flow-based query; keep ContentResolver calls on Dispatchers.IO when you must read another app’s provider.
Replace getLoaderManager()/initLoader() calls with a ViewModel function called from init or an explicit user action.
Treat any remaining Loader code as legacy: it still compiles and runs, but it receives no improvements and should be scheduled for migration.
29
Explain what a ContentResolver is for and how an app uses it to read and modify data exposed by another app's ContentProvider.
KnownRevise
A ContentResolver is the Android framework object that acts as the client-side bridge to a ContentProvider owned by another app (or by the system). Instead of opening files or databases directly, your app passes a Uri to the resolver, which routes the request to the provider’s process and returns a Cursor for reads or a Uri/row count for writes.
Read full answerHide full answer
Applies toAndroid 16 (API 36), Kotlin 2.x; the APIs described have been stable since API 1, with runtime permission behaviour applying from API 23.
How It Works
Every provider publishes one or more authorities, exposed through android:authorities in its manifest, and addressed with a content://Uri (for example content://com.android.contacts/contacts).
You never call ContentProvider methods directly. You call the matching method on ContentResolver, and Android’s ActivityManagerService finds the provider hosting that authority, performs an IPC call, and marshals the result back.
Well-known system providers include ContactsContract, MediaStore, CalendarContract, Telephony and Settings, plus any provider your own app exports.
Access is governed by the provider’s android:readPermission and android:writePermission; for system providers these are dangerous permissions that must be requested at runtime on API 23+.
Key Operations
The four data operations mirror a database, because providers usually wrap SQLite:
Query: query(uri, projection, selection, selectionArgs, sortOrder) returns a Cursor. Always pass user input through selectionArgs so the provider binds it as a parameter rather than concatenating it into selection.
Insert: insert(uri, contentValues) returns the Uri of the new row.
Update: update(uri, values, selection, selectionArgs) returns the number of rows changed.
Delete: delete(uri, selection, selectionArgs) returns the number of rows removed.
Providers additionally call notifyChange() so clients observing a Uri with ContentObserver or Flow-based wrappers can react to changes.
Important Version-Specific Behaviour
API 23+: dangerous permissions such as READ_CONTACTS require a runtime request; declaring them in the manifest is not enough.
API 29/30 (scoped storage): a MediaStore query returns only the media your app owns unless it holds a broad storage permission, so query MediaStore.Images.Media.EXTERNAL_CONTENT_URI (or getContentUri(MediaStore.VOLUME_EXTERNAL)) and read the returned columns rather than following file paths.
API 34+: apps must declare a foreground service type when accessing contacts or media from a foreground service.
For user-picked documents, prefer the Storage Access Framework (ACTION_OPEN_DOCUMENT) over hard-coded URIs, because it grants explicit, revocable access without broad permissions.
Code Example
Reading contacts on a background dispatcher, with parameter binding and safe cursor handling:
import android.content.ContentResolver
import android.provider.ContactsContract
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
data class Contact(val id: String, val name: String)
// Called from a ViewModel coroutine; never call this from the main thread.
suspend fun loadContacts(resolver: ContentResolver): List<Contact> =
withContext(Dispatchers.IO) {
val projection = arrayOf(
ContactsContract.Contacts._ID,
ContactsContract.Contacts.DISPLAY_NAME
)
// selectionArgs binds the filter as a parameter, preventing injection.
val selection = "${ContactsContract.Contacts.DISPLAY_NAME} LIKE ?"
val selectionArgs = arrayOf("%Ada%")
val contacts = mutableListOf<Contact>()
resolver.query(
ContactsContract.Contacts.CONTENT_URI,
projection,
selection,
selectionArgs,
"${ContactsContract.Contacts.DISPLAY_NAME} ASC"
)?.use { cursor -> // use {} closes the Cursor automatically.
val idIndex = cursor.getColumnIndexOrThrow(ContactsContract.Contacts._ID)
val nameIndex = cursor.getColumnIndexOrThrow(ContactsContract.Contacts.DISPLAY_NAME)
while (cursor.moveToNext()) {
contacts.add(
Contact(
cursor.getLong(idIndex).toString(),
cursor.getString(nameIndex)
)
)
}
}
contacts
}
Required manifest permission, plus a runtime request on API 23+:
Never do resolver work on the main thread: a provider lives in another process, so each call is a binder transaction that can block. Use Dispatchers.IO or WorkManager for larger transfers.
Always close the Cursor: wrap it in use {} or a try/finally, otherwise you leak native resources and may trigger CursorWindow exhaustion.
Bind parameters, never concatenate: selection is raw SQL text, so any user-supplied value must go into selectionArgs.
Prefer ApplicationProvider (in tests) or a Hilt-provided Context to obtain contentResolver in a testable way, and inject the resolver rather than fetching it inside composables.
Handle null results and empty cursors: query() can return null when the provider errors, and count == 0 is a normal state, not a failure.
Don’t assume a Uri is stable across reboots or app reinstalls; persist only the document URI granted by the SAF, and re-check persisted permissions with takePersistableUriPermission.
30
Describe the layers and libraries that make up Android's HTTP networking stack and how they fit together.
KnownRevise
Android’s HTTP networking stack is a set of layered components: your app talks to a high-level API such as Retrofit, which delegates to an HTTP client such as OkHttp, which in turn uses the platform’s socket and TLS (Conscrypt) stack, DNS resolution, and kernel networking. Google’s recommended path for most apps is Retrofit over OkHttp, called from Kotlin coroutines on Dispatchers.IO.
App / business logic layer: a ViewModel exposes state through StateFlow and calls a repository. The repository never touches the UI and never runs requests on the main thread.
API description layer: Retrofit interfaces annotate Kotlin functions with @GET, @POST and so on, and declare them suspend so they integrate directly with coroutines.
HTTP client layer: OkHttp (or the engine behind another client) handles connection pooling, redirects, retries, caching, interceptors, HTTP/2 with HTTP/1.1 fallback, and TLS handshake management. It talks to sockets directly rather than going through HttpURLConnection.
Platform layer: the Socket/SSLSocket APIs and the Conscrypt TLS provider, the system trust store, and the OS-level network security configuration.
OS / kernel layer: sockets, DNS resolution, routing, and the radio hardware — invisible to app code but the reason latency, keep-alives and connection reuse matter.
Core Libraries
Library
Role
Notes
Retrofit
Declarative REST API
Turns interface annotations into typed calls; the recommended default
OkHttp
HTTP client
Default Retrofit engine; interceptors, pooling, caching; HTTP/2 only, no HTTP/3
HttpURLConnection
Platform client
Still usable, but verbose; effectively the legacy path
Volley
Request queue, image loading
Older, largely superseded by Retrofit + OkHttp + Coil
Cronet
Chromium networking engine
HTTP/3 and QUIC at scale; heavy to ship, used mostly by large apps
Ktor client
Multiplatform coroutines client
Useful when sharing networking with other Kotlin targets
How a Request Travels the Stack
A composable collects a ViewModel‘s StateFlow and triggers a user action.
The ViewModel launches a coroutine on Dispatchers.IO (or a repository does) and calls a Retrofit suspend function.
Retrofit builds the request from the annotation, serializes the body with a converter (Moshi, kotlinx.serialization or Gson), and hands it to OkHttp.
Application interceptors run, then OkHttp selects a pooled connection or opens a new one, performs the TLS handshake, and sends the request.
Network interceptors observe the real wire request; the response is decoded, converted to the Kotlin return type, and resumed to the coroutine.
The result updates the StateFlow; failed calls surface as HttpException or IOException for the error path to handle.
Platform Integration and Security
TLS: HTTPS handshakes use the platform’s TLS implementation, with certificate and hostname verification on by default. Never disable verification in production.
Network security configuration: network_security_config.xml governs trust anchors and cleartext policy.
Cleartext HTTP: blocked by default since API 28; opt in per-domain only when unavoidable, and prefer HTTPS.
Dependencies: Hilt injects the Retrofit instance and repository so the networking graph is testable and singleton-scoped.
Choosing the Right Layer
Use Retrofit + OkHttp for typed REST APIs and most new apps. Use OkHttp directly when you need raw control over a single request or a non-REST endpoint. Choose Cronet only when HTTP/3 performance justifies the binary size. Avoid HttpURLConnection and Volley for new code, and treat AsyncTask-style background threading as deprecated in favour of coroutines.
Best Practices and Common Pitfalls
Never block the main thread: all network calls run on Dispatchers.IO or a dedicated CoroutineScope in the ViewModel.
Add an HTTP logging interceptor in debug builds only, and redact headers that carry tokens.
Configure timeouts and a retry policy explicitly rather than relying on defaults that may not fit a flaky mobile network.
Inject the client with Hilt so tests can swap in a MockWebServer or a fake interceptor.
Handle errors explicitly: map IOException to a retryable UI state and HTTP 4xx/5xx to a distinct error state instead of crashing.
Watch connection reuse: excessive concurrent connections or missing keep-alive behaviour drain battery and increase latency on mobile radios.
31
What are the recommended libraries and tools for handling network communication in contemporary Android applications?
KnownRevise
Modern Android development primarily relies on Retrofit layered on top of OkHttp for type-safe, efficient network communication. This stack integrates natively with Kotlin coroutines, so suspend functions perform requests off the main thread without blocking UI rendering, and it fits cleanly into the recommended architecture (repository, ViewModel, StateFlow). While lower-level alternatives exist, this combination is the industry standard for new projects targeting current Android versions.
Read full answerHide full answer
Applies toKotlin 2.x, Android 16 (API 36)
The Standard Networking Stack
Retrofit: A type-safe HTTP client that turns REST API endpoints into Kotlin interfaces. You declare a method with an annotation such as @GET("users/{id}"), and Retrofit generates the implementation. It supports suspend functions directly, which removes the need for callbacks.
OkHttp: The underlying HTTP engine used by Retrofit. It handles connection pooling, transparent GZIP compression, response caching, and a powerful interceptor chain for auth headers, logging and retries.
Converters: Libraries such as converter-gson or converter-moshi serialize JSON responses into Kotlin data classes automatically, so you work with typed objects instead of raw strings.
Coroutines and Flow: Network calls are suspend functions collected in a ViewModel, with results exposed to the UI as StateFlow, keeping the UI thread free and the state observable.
Alternative Libraries
Library
Strengths
Weaknesses
When to choose it
Volley
Google’s mature library, good for simple small requests and image loading
Not type-safe, weaker coroutine integration, largely superseded by Retrofit + OkHttp
Legacy codebases or very simple request/response needs
Smaller community and fewer ready-made converters than Retrofit
Kotlin Multiplatform projects sharing networking code across platforms
HttpURLConnection
Built into the Android SDK, no extra dependency
Heavy manual boilerplate for threading, parsing and caching
Rare low-level needs where adding a dependency is unacceptable
Architecture Integration
Networking code should never live in UI components. Follow the repository pattern: 1. Define the Retrofit API interface in the data layer. 2. Implement a Repository class that calls that interface and maps results to domain models. 3. Expose data to the UI through StateFlow from a ViewModel. 4. Launch coroutines in viewModelScope so the work is cancelled automatically when the ViewModel is cleared.
For periodic or guaranteed background sync, use WorkManager instead of polling from the UI layer. Inject the repository and API service with Hilt to keep the layers decoupled and testable.
Security and Best Practices
HTTPS only: Enforce TLS. Use network_security_config.xml to disable cleartext traffic globally and to pin certificates for critical domains.
Authentication: Attach auth tokens dynamically with an OkHttp interceptor rather than hardcoding them; never store secrets in source code.
Timeouts: Configure connect, read and call timeouts in OkHttpClient so a slow server cannot hang the app.
Error handling: Treat network calls as fallible — handle IOException and HTTP error codes and expose them as a sealed result type to the UI.
Logging: Avoid logging sensitive response bodies in production builds; use an interceptor only in debug variants.
Common Pitfalls
Main-thread blocking: Never call network methods directly from UI code; use suspend functions or WorkManager.
Memory leaks: Tie coroutines to viewModelScope or lifecycleScope so they are cancelled when the UI is destroyed.
Untyped responses: Parsing JSON by hand or ignoring the converter layer discards compile-time safety and invites crashes.
32
What is the recommended way to execute network requests asynchronously in an Android app without blocking the UI thread?
KnownRevise
You perform asynchronous network operations by calling Retrofit suspend functions from a ViewModel coroutine scope and exposing the result as StateFlow to Jetpack Compose. This moves HTTP work off the main thread automatically, survives configuration changes, and lets the UI observe state reactively without manual lifecycle handling.
Retrofit: Declares the HTTP API as a Kotlin interface. With coroutine support built in since 2.11, suspend functions execute the request on a background dispatcher, so you never hit NetworkOnMainThreadException.
ViewModel + viewModelScope: Owns the network call. viewModelScope is tied to the UI lifecycle and cancels automatically when the screen is closed, preventing leaks.
StateFlow: Holds immutable UI state (Loading, Success, Error). The Compose UI collects it and recomposes when data arrives.
Hilt: Provides the Retrofit instance and API service as singleton dependencies, keeping construction out of the ViewModel.
Threading Model
Retrofit suspend functions do not run on the caller’s dispatcher; they switch to OkHttp’s background executor for the actual I/O. This means you can launch from viewModelScope (which defaults to Dispatchers.Main) and still stay off the main thread during the request. MutableStateFlow.value is thread-safe, so the update does not have to happen on the main thread, but collectAsStateWithLifecycle collects on the main dispatcher so the Compose UI always reads state on the UI thread. For heavy local processing of the response, wrap that part in withContext(Dispatchers.IO).
Setting Up Dependencies
Use a Gradle version catalog (libs.versions.toml) and Kotlin DSL. Add the Internet permission in AndroidManifest.xml (required on all API levels), apply the Hilt Gradle plugin and KSP in the module’s plugins block, and declare the libraries:
The API interface, Hilt module, ViewModel, and Compose collector below show the complete flow (import statements omitted for brevity):
data class User(val id: Long, val name: String)
// API interface
interface ApiService {
@GET("users")
suspend fun getUsers(): List<User>
}
// Hilt module
@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {
@Provides
fun provideRetrofit(): Retrofit = Retrofit.Builder()
.baseUrl("https://api.example.com/") // HTTPS only
.addConverterFactory(GsonConverterFactory.create())
.build()
@Provides
fun provideApi(retrofit: Retrofit): ApiService =
retrofit.create(ApiService::class.java)
}
// UI state
sealed class UiState<T> {
object Loading : UiState<Nothing>()
data class Success<T>(val data: T) : UiState<T>()
data class Error(val message: String?) : UiState<Nothing>()
}
// ViewModel
@HiltViewModel
class UserViewModel @Inject constructor(private val api: ApiService) : ViewModel() {
private val _state = MutableStateFlow<UiState<List<User>>>(UiState.Loading)
val state: StateFlow<UiState<List<User>>> = _state.asStateFlow()
init { loadUsers() }
private fun loadUsers() {
viewModelScope.launch {
_state.value = UiState.Loading
_state.value = try {
UiState.Success(api.getUsers())
} catch (e: Exception) {
UiState.Error(e.message)
}
}
}
}
// Compose UI
@Composable
fun UserScreen(viewModel: UserViewModel = hiltViewModel()) {
val state by viewModel.state.collectAsStateWithLifecycle()
when (val s = state) {
is UiState.Loading -> CircularProgressIndicator()
is UiState.Success -> LazyColumn { items(s.data) { Text(it.name) } }
is UiState.Error -> Text("Failed: ${s.message}")
}
}
Best Practices and Common Pitfalls
Never block: Do not call runBlocking or await() from the main thread; always use launch in a scoped coroutine.
Avoid GlobalScope: It outlives the UI and causes memory leaks; viewModelScope cancels with the screen.
Use HTTPS: Plain HTTP is blocked by default on API 28+ unless explicitly allowed in the network security config.
Handle errors explicitly: Wrap calls in try/catch or map to a Result type so the UI can show retry actions.
Deprecated APIs: Do not use AsyncTask (deprecated in API 30) or manual Thread/Handler loops; coroutines and WorkManager are the replacements.
Streaming data: For continuous updates like websockets or polling, return a Flow from the repository and use viewModelScope.launch { flow.collect {} } instead of a single suspend call.
33
What purpose does the Volley library serve in Android development?
KnownRevise
Volley is an HTTP networking library for Android that handles network requests, image loading, and response caching through a request queue that automatically schedules and manages background work. It simplifies common networking tasks such as GET/POST calls, JSON parsing, and image fetching, though for new projects Google and the Android community generally recommend Retrofit with OkHttp as the modern alternative.
Read full answerHide full answer
Applies toAndroid Volley 1.2.x, Kotlin 2.x, Android 16 (API 36); Retrofit/OkHttp comparison applies to current stable releases
What Volley Does
Volley provides a high-level API for network communication in Android apps. Instead of manually managing threads, connections, and caching, you create request objects and add them to a RequestQueue, which handles threading, caching, and response delivery on the main thread.
Request queuing: Requests are queued, prioritized, and executed on worker threads, with responses delivered back to the UI thread.
Automatic caching: Responses are cached in memory and on disk, reducing redundant network calls.
Built-in request types: StringRequest, JsonObjectRequest, JsonArrayRequest, and ImageRequest cover most common cases.
Image loading: NetworkImageView and ImageLoader handle image fetching, caching, and placeholder display.
Key Features
Request prioritization: Each request can be assigned a priority (LOW, NORMAL, HIGH, IMMEDIATE) so critical calls run first.
Retry policies: You can configure timeout and retry behaviour per request with DefaultRetryPolicy.
Cancellation: Requests can be tagged and cancelled in bulk, useful when an Activity or Fragment is destroyed.
Response delivery: Callbacks arrive on the main thread, so you can update UI directly without a handler.
Volley vs Modern Alternatives
Aspect
Volley
Retrofit + OkHttp
Status
Maintained but legacy
Current recommended approach
API style
Request objects + queue
Type-safe interface definitions
JSON handling
Built-in JsonObjectRequest
Moshi/Gson converters
Coroutines/Flow
Callback-based
Native suspend functions and Flow
Image loading
Built-in ImageLoader
Use Coil or Glide instead
Learning curve
Low for simple calls
Slightly higher, more powerful
Best Practices and Caveats
Prefer Retrofit/OkHttp for new code: Volley is not deprecated, but Retrofit offers better type safety, coroutine support, and testability. Use Volley mainly for maintaining existing apps or quick image-loading needs.
Do not use Volley for large file downloads: It loads entire responses into memory; use DownloadManager or OkHttp streaming for big files.
Always handle errors: VolleyError covers timeouts, auth failures, and server errors; show user-friendly messages.
Cancel requests in onStop or onDestroy: Tag requests and call queue.cancelAll(tag) to avoid leaks and callbacks after the UI is gone.
Use HTTPS only: Never send credentials over plain HTTP; Volley does not add security beyond what the underlying connection provides.
For images in new projects, use Coil or Glide: These are more actively maintained than Volley’s ImageLoader.
34
Describe how Retrofit is implemented in Android projects to manage network API interactions securely and efficiently.
KnownRevise
Retrofit is a type-safe HTTP client from Square that converts REST endpoints into Kotlin interfaces, managing serialization, connection pooling, and error handling so that network calls can be declared as suspend functions and integrated with Kotlin coroutines and OkHttp.
Retrofit creates a runtime implementation of a Kotlin interface annotated with @GET, @POST, etc.; you never write the implementation yourself.
OkHttp performs the real HTTP work: connection pooling, response caching, retries, and interceptors.
A converter factory such as converter-gson or converter-moshi is needed to turn JSON response bodies into Kotlin data classes; without one you can still read Response<String>.
Because the API is an interface, it can be mocked in unit tests, separating network logic from business logic.
Coroutine Support and Threading
Declare API methods as suspend fun: Retrofit returns Response<T> or the body type directly.
The call does not block the calling coroutine; OkHttp runs it on its own internal executor, and the coroutine resumes in the dispatcher of the caller (for example Dispatchers.Main inside viewModelScope).
You therefore do not need withContext(Dispatchers.IO) purely for the Retrofit call, but any following Room write should still happen on Dispatchers.IO or use Room’s own suspend functions.
Callback<T> remains a supported, non-deprecated API; prefer suspend functions when you are already using coroutines.
Launch calls from viewModelScope so they are cancelled automatically when the UI is destroyed.
Dependency Configuration
Use Gradle Kotlin DSL (build.gradle.kts) with a version catalog (libs.versions.toml) so versions are centralized and aliases replace hardcoded strings.
Code Example
# libs.versions.toml
[libraries]
retrofit = { group = "com.squareup.retrofit2", name = "retrofit", version = "2.11.0" }
retrofit-converter-gson = { group = "com.squareup.retrofit2", name = "converter-gson", version = "2.11.0" }
okhttp = { group = "com.squareup.okhttp3", name = "okhttp", version = "4.12.0" }
okhttp-logging-interceptor = { group = "com.squareup.okhttp3", name = "logging-interceptor", version = "4.12.0" }
// Secure OkHttp client with a centralized auth interceptor
val client = OkHttpClient.Builder()
.addInterceptor { chain ->
// Obtain token from EncryptedSharedPreferences or Android Keystore, never from hardcoded strings
val token = getTokenFromSecureStorage()
val request = chain.request().newBuilder()
.addHeader("Authorization", "Bearer $token")
.build()
chain.proceed(request)
}
.connectTimeout(15, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.build()
// ApiService.kt
interface ApiService {
@GET("users/{id}")
suspend fun getUser(@Path("id") userId: String): Response<User>
}
// Retrofit instance
val api = Retrofit.Builder()
.baseUrl("https://api.example.com/")
.client(client)
.addConverterFactory(GsonConverterFactory.create())
.build()
.create(ApiService::class.java)
// UserViewModel.kt
class UserViewModel(private val api: ApiService) : ViewModel() {
private val _userState = MutableStateFlow<User?>(null)
val userState: StateFlow<User?> = _userState.asStateFlow()
fun loadUser(id: String) {
viewModelScope.launch {
try {
val response = api.getUser(id)
if (response.isSuccessful) {
_userState.value = response.body()
} else {
// Handle HTTP error (4xx/5xx)
}
} catch (e: IOException) {
// Network or serialization failure
}
}
}
}
Security and Best Practices
Enforce HTTPS and block cleartext traffic with android:usesCleartextTraffic="false" in the manifest.
Never embed secrets in source code or BuildConfig; obtain tokens from a secure backend exchange and store them in EncryptedSharedPreferences or the Android Keystore.
Add auth headers through a single OkHttp interceptor rather than repeating them in every API method.
If you use HttpLoggingInterceptor, set its level to NONE or HEADERS_ONLY in production and redact sensitive headers.
Set read and connect timeouts on OkHttpClient to avoid hanging calls.
Validate and encode path/query parameters; Retrofit’s @Path and @Query encode values, but validate business-level input.
Handle IOException for network failures and check response.isSuccessful for HTTP errors; do not treat every exception as a server error.
Cache idempotent GET responses with OkHttp cache headers when appropriate.
Never perform blocking network or disk work on the main thread; Retrofit suspend calls are non-blocking, but move heavy parsing or Room writes to Dispatchers.IO if they are not already suspend-friendly.
35
What steps would you take to diagnose and fix a network call that is running slowly in an Android app?
KnownRevise
Troubleshooting a slow network request starts with measuring where time is actually spent rather than guessing: you break the request into its phases (DNS, connection, TLS handshake, request submission, server processing, response download), instrument them, and only then decide whether the fix is in the app, the network, or the backend.
Read full answerHide full answer
Applies toAndroid 16 (API 36) with Kotlin 2.x, OkHttp 4.12 and Retrofit 2.11.
How to Break the Problem Down
A request is slow for one of four reasons, and each needs a different fix:
Client-side overhead: work blocked on the main thread, thread-pool starvation, excessive serialization, or the device being in a low-power state.
Connection setup: DNS resolution, TCP connect, and TLS handshake are paid on every new connection and dominate latency on mobile networks.
Server-side processing: the backend genuinely takes long to compute or query the data.
Payload transfer: a large response body over a weak radio link, often the biggest cost on mobile.
Tools and Techniques
Reproduce on a controlled network first. Use Android Studio’s Network Inspector with a Retrofit/OkHttp app to see request timing and bodies live. Then emulate real conditions with the emulator’s network throttling or adb shell svc data disable style tests, and test on a real device — emulators hide radio and DNS behaviour.
Instrument the phases. Add an EventListener to see exactly how long DNS, connect, TLS, and body transfer each took. This is the single most informative step, because it tells you which of the four categories above applies.
Check the dispatcher. I/O should not run on the main thread; use Dispatchers.IO (or let Retrofit handle the dispatch). A request that is “slow” is often just one that is queued behind other work or blocking the UI thread so the user perceives lag.
Inspect the traffic. A proxy such as Charles or Proxyman, or HttpLoggingInterceptor at BODY level in debug builds only, shows headers, redirects, caching headers, and body size. Look for missing Content-Encoding, huge JSON with unused fields, and responses without Cache-Control.
Check device state. App Standby Buckets (API 28+) restrict background network access, Doze defers requests, and battery saver throttles performance. Foreground work needing network needs a foreground service with a declared type (dataSync or similar, required since API 34).
Measure in production. Custom traces via Firebase Performance Monitoring or Perfetto reveal whether slowness is device-, region-, or carrier-specific.
Code Example
A timing listener that prints the phase breakdown, plus a Retrofit call correctly dispatched off the main thread:
class TimingEventListener : EventListener() {
private var start = 0L
override fun callStart(call: Call) { start = System.nanoTime() }
override fun callEnd(call: Call) {
val ms = (System.nanoTime() - start) / 1_000_000
Log.d("Net", "${call.request().url} took ${ms}ms")
}
// Override dnsStart/dnsEnd, connectStart/connectEnd,
// secureConnectStart/secureConnectEnd, responseHeadersEnd to log each phase.
}
val client = OkHttpClient.Builder()
.eventListener(TimingEventListener()) // phase-level timing
.addInterceptor(HttpLoggingInterceptor().apply {
level = HttpLoggingInterceptor.Level.BODY // debug builds only
})
.build()
interface ApiService {
@GET("orders")
suspend fun listOrders(): List<Order>
}
class OrderRepository(private val api: ApiService) {
suspend fun orders(): List<Order> = withContext(Dispatchers.IO) { // Retrofit already offloads I/O; this keeps the context explicit
api.listOrders()
}
}
Best Practices and Common Pitfalls
Fix by category. Slow DNS or connect → enable HTTP/2 and connection reuse (OkHttp does both by default), keep the connection pool warm, and consider DNS-over-HTTPS for flaky resolver behaviour. Slow server → paginate, add server caching, or move work into a WorkManager job with retries. Large body → gzip/Brotli, field selection, and pagination.
Cache aggressively. Add Cache-Control headers and an OkHttp Cache so repeat loads are instant; a stale-while-revalidate strategy hides most perceived slowness.
Set explicit timeouts (connectTimeout, readTimeout) sized to your API’s real budget, and show the user a retry path instead of an indefinite spinner.
Pitfalls: logging interceptor at BODY level in release builds (leaks data and costs CPU); running blocking I/O on the main thread (ANR risk); ignoring retries that multiply latency; and blaming the backend when the real cost is a cold TLS handshake on a flaky carrier network.
36
Can you explain what OkHttp is and the reasons you would choose it as an HTTP client in an Android app?
KnownRevise
OkHttp is a fast, open-source HTTP client for Android and Java applications, developed by Square, that handles every detail of network communication: connection pooling, transparent caching, retries, redirects, and HTTP/2 and WebSocket support. You would use it because writing that plumbing by hand with HttpURLConnection is error-prone and verbose, while OkHttp gives you a small, well-tested API, sensible defaults, and full control over requests, headers, timeouts and interceptors. It is also the default transport engine under popular higher-level libraries such as Retrofit and Coil, so learning it pays off across the modern Android stack.
Read full answerHide full answer
Applies toOkHttp 5.0 (current stable; 4.12.x remains in maintenance), running on Android 16 (API 36) with Kotlin 2.x; the API described here is stable across OkHttp 4.x and 5.x unless noted.
How It Works
OkHttp sits between your app and the operating system’s socket layer, replacing the framework’s legacy HttpURLConnection with its own connection management:
OkHttpClient: the central class that holds configuration (timeouts, cache, interceptors, dispatcher) and reuses a shared connection pool and thread pool. You normally create one instance per app and reuse it for all requests, because each client owns a pool of connections that would otherwise be recreated and wasted.
Request: an immutable description of what to send — URL, HTTP method, headers and an optional RequestBody. You build it with Request.Builder.
Response: the immutable result, exposing the status code, headers and a ResponseBody. The body is a one-shot stream that you must close() (typically with a use block) to return the connection to the pool.
Call: the object returned by client.newCall(request) (implemented internally by RealCall). It can be executed synchronously with execute() (blocking, so never call it on the main thread) or asynchronously with enqueue(), which hands the work to the Dispatcher and returns the result on one of its background threads — never on the main thread.
Interceptors: a chain of functions that observe, rewrite, retry or short-circuit every request and response. Application interceptors see the request once, before redirects and retries; network interceptors see the raw bytes that actually go on the wire, including redirects and If-Modified-Since revalidation.
Key Features
Connection pooling and keep-alive: HTTP/2 and HTTP/1.1 connections are reused across requests to the same host, cutting latency and battery use on mobile networks.
Transparent caching: give the client a Cache directory and OkHttp honours HTTP cache semantics (Cache-Control, ETag, Last-Modified), so repeated calls return instantly and work offline.
Automatic recovery: it silently retries failed requests when the connection is stale, and follows up to 20 redirects by default, with handling that protects against circular redirects.
Modern protocol support: HTTP/2 multiplexing (on Android 5.0 / API 21 and above, where the platform provides ALPN), TLS negotiation with modern cipher suites, and WebSocket clients (WebSocket returned by newWebSocket()) for bidirectional streaming.
Coroutine-friendly API: the separate okhttp-coroutines module provides the Call.await() suspend extension, and ResponseBody is Closeable so it works with use, fitting naturally with Dispatchers.IO and ViewModel + StateFlow architectures.
Pluggability: interceptors make it easy to add authentication tokens, logging (HttpLoggingInterceptor, in the okhttp-logging-interceptor module), crash reporting or request rewriting without touching call sites.
OkHttp vs. Alternatives
Aspect
OkHttp
HttpURLConnection
Retrofit
Ktor client
Layer
Low-level HTTP client
Framework legacy client
Type-safe wrapper over OkHttp
Multiplatform coroutine client
Boilerplate
Moderate, explicit
High, manual
Low, annotations
Low, DSL
Caching/pooling
Built-in, configurable
Limited, manual
Inherits OkHttp’s
Depends on engine
Best use
Custom protocols, WebSockets, fine control
Simple one-off calls
REST APIs with typed endpoints
Kotlin Multiplatform projects
A common Android setup is to let Retrofit define the API surface and let OkHttp act as its okHttpClient, combining Retrofit’s convenience with OkHttp’s interceptors and cache.
Advantages and Limitations
Advantages: battle-tested at huge scale, a small dependency footprint (it brings okio and nothing else heavy), excellent logging with HttpLoggingInterceptor, broad ecosystem compatibility, and active maintenance that tracks new TLS and HTTP standards.
Limitations: it is transport-only — there is no JSON parsing and no lifecycle awareness, so you combine it with kotlinx.serialization or Moshi, coroutines and a ViewModel. Its API is mutable-builder heavy, and misusing it (creating a new client per request, leaking response bodies, or calling execute() on the main thread) causes jank, NetworkOnMainThreadException or connection starvation.
Best Practices
Share oneOkHttpClient for the app’s lifetime; build per-request variation into the Request, not the client.
Never run execute() on the main thread; use enqueue(), a coroutine on Dispatchers.IO, or hand the call to a ViewModel that collects results into StateFlow.
Always close response bodies: wrap the Response in a use block, or call response.body?.string() inside one, so the connection returns to the pool.
Prefer HTTPS with the platform’s default ConnectionSpec rather than relaxing it; use CertificatePinner only after assessing the trade-offs, since pinned certificates expire and can lock users out.
Add a Cache sized to a few megabytes in the app cache directory so repeated GETs are served from disk.
Use HttpLoggingInterceptor at BASIC or HEADERS level in debug builds only — BODY can leak tokens and PII into logcat, and logging should never ship in release.
37
Can you explain what ProGuard does and why it matters when building Android apps?
KnownRevise
ProGuard is a free tool for Java bytecode that shrinks, optimizes and obfuscates compiled code: it removes unused classes, fields and methods, renames the remaining identifiers to meaningless names, and performs low-level optimizations. In Android development it matters because it makes release builds smaller, marginally faster and significantly harder to reverse-engineer — and because its rule files are still the way you tell the build tool which code must not be touched.
Shrinking: builds a call graph from entry points (activities, services, Application subclass, main methods, JNI) and discards everything unreachable.
Obfuscation: renames classes, methods and variables to short names like a and b, which reduces binary size and makes decompiled output hard to read.
Optimization: inlines methods, removes unused parameters, merges classes and simplifies control flow (R8 applies a subset of these by default).
Preverification: adds StackMapTable information required by the JVM class file format; on Android this step is effectively subsumed by the DX/D8 dexing step.
ProGuard vs R8
Aspect
ProGuard
R8
Status in Android
Deprecated; support removed in AGP 7.0
Default since AGP 3.4, the only shrinker from AGP 7.0
Configuration
proguard-rules.pro files
Same ProGuard rule syntax, plus @Keep and @KeepName
Desugaring
No
Yes — combined with D8 in one step
Full mode
N/A
Default since AGP 8.0 (android.enableR8.fullMode no longer needed); more aggressive optimizations
In practice, Android developers still say “ProGuard rules” and edit proguard-rules.pro, but the tool actually processing those rules is R8.
Why It Matters
Smaller apps: shrinking typically removes a large fraction of library code (especially from large dependencies), which lowers download size and improves install success rates on metered connections.
Harder reverse engineering: obfuscation strips meaningful names from decompiled classes.dex, protecting proprietary logic, API endpoints and licensing checks — though it is not encryption and determined attackers can still analyze behavior.
Better performance: fewer classes and methods mean faster class loading, smaller dex files and lower memory pressure; R8’s optimizations can also reduce method-call overhead.
Build hygiene: shrinking failures surface dead code and missing keep rules early, forcing explicit documentation of reflection-based APIs.
Configuring and Keeping Code
Enable it in the release build type with minifyEnabled true (usually together with shrinkResources true), then add rules to proguard-rules.pro:
-keep: keeps the specified class and its members from being shrunk or renamed.
-keepclassmembers: keeps only members of a class that is otherwise kept.
@Keep (Jetpack Annotations): the Kotlin/Java-friendly equivalent for classes and members; @KeepName keeps names without preventing removal.
Mapping file: mapping.txt in the build output folder translates obfuscated stack traces back to original names — required for readable crash reports.
Best Practices and Common Pitfalls
Always test the release variant, not just debug: obfuscation bugs (missing keep rules, reflection failures) only appear when minification is on.
Keep everything reached only by reflection: serialization libraries (Gson, Moshi via codegen are safer), Retrofit service interfaces, Room DAOs accessed reflectively, JNI methods, and enum values used by name.
Upload mapping.txt to your crash-reporting provider (Firebase Crashlytics, Sentry) for symbolicated release stacks; keep copies for every published version.
Do not obfuscate debug builds — it slows iteration and hides stack traces for no benefit.
Prefer compile-time safe alternatives first: Kotlin @JvmStatic companions, Moshi/Kotlinx Serialization code generators, and Room’s generated implementations reduce the need for broad -keep rules.
Watch for ClassNotFoundException or NoSuchMethodException in release logs: the classic symptom of an over-aggressive shrink, fixed with a targeted rule rather than disabling minification.
38
Explain what the Android NDK is and describe the scenarios in which you would typically choose it over the standard Android SDK.
KnownRevise
The Android Native Development Kit (NDK) is a toolset that lets you implement parts of an Android app in C and C++ rather than Kotlin or Java, producing native libraries (.so files) for a specific device ABI (such as arm64-v8a) that run directly on the CPU. It is not a replacement for the Android SDK; it is an escape hatch for the small set of problems where native code is genuinely faster, more portable across platforms, or required by a third-party dependency.
Read full answerHide full answer
Applies toAndroid 16 (API 36) and Android NDK r27+, using Kotlin 2.x.
What the NDK Actually Provides
Toolchains: Clang/LLVM compilers and linkers that target every Android ABI (arm64-v8a, armeabi-v7a, x86_64, x86).
System libraries: a subset of the Bionic libc, plus C++ standard library support (libc++).
Build tooling: ndk-build and, the recommended option, CMake integration through the Android Gradle plugin.
Java Native Interface (JNI): the bridge that lets Kotlin/Java call native functions and lets native code call back into the managed layer.
Native APIs: headers for features such as Vulkan, OpenGL ES, AAudio (API 26+), sync fences, and the native_app_glue framework used by pure-native apps and games. OpenSL ES is legacy; AAudio is the recommended audio path.
Primary Use Cases
CPU-bound, compute-heavy work: signal processing, audio/video codecs, physics simulation, compression, and crypto on large buffers. A tight loop with predictable memory access can be several times faster in C++ than in Kotlin, partly because there is no garbage collection or bounds-checking overhead.
Reusing existing C/C++ code: large, mature libraries (a proprietary codec, a game engine, a database engine, or a cross-platform core shared with iOS) are usually compiled once and wrapped with JNI rather than rewritten in Kotlin.
Games and graphics: engines such as Unity, Unreal, and Godot build on the NDK to access Vulkan and OpenGL ES directly and to minimise per-frame overhead.
Low-latency audio: real-time audio apps use AAudio through the NDK to avoid the latency of the managed AudioRecord/AudioTrack path.
Security through obscurity (limited value): native code is harder to inspect than DEX, so some apps move licence checks or DRM into .so files. This is obfuscation, not real security, and should never be the sole protection.
When to Avoid It
“It will be faster” is not a reason by itself. JNI marshalling has a cost; passing many small objects across the boundary can be slower than staying in Kotlin. Profile first with Android Studio’s CPU profiler and the simpleperf tool before committing to native code.
Business logic and UI belong in Kotlin and Jetpack Compose. Compose has no native equivalent, and JNI calls from composition or recomposition will destroy performance.
You lose access to most platform APIs. Most Jetpack and framework APIs are Java/Kotlin only; calling them from C++ is verbose and error-prone.
You take on extra build complexity: per-ABI binaries, larger APK/AAB sizes (mitigated by App Bundles), harder debugging, and no automatic memory management — native leaks and crashes (SIGSEGV) are your responsibility, tracked with tools such as ASan, HWASan, and tombstone logs.
Best Practices and Common Pitfalls
Keep the JNI boundary thin. Do the minimum work in C++, return plain values or DirectByteBuffers, and batch calls instead of calling into native code inside a hot Kotlin loop.
Never do blocking work on the main thread, native or not — JNI calls run on the calling thread, so use Dispatchers.IO or a CoroutineDispatcher backed by a native thread pool.
Pin the NDK version in build.gradle.kts (ndkVersion = "27.2.12479018") so CI builds are reproducible.
Handle references correctly: local JNI references are freed automatically per call, but long-lived references to Kotlin objects must be managed with NewGlobalRef/DeleteGlobalRef or you will leak or crash.
Test on arm64-v8a and x86_64 (emulator), and enable HWASan in CI to catch native memory errors before release.
39
Explain what the Android Loader framework is and what benefits it was designed to provide.
KnownRevise
A Loader is a legacy Android framework component that loads data asynchronously on a background thread for an Activity or Fragment, automatically surviving configuration changes such as screen rotation and reconnecting to the last loaded result when the UI is recreated. It was useful because it prevented UI jank from disk or network queries, avoided memory leaks from manual background threads, and required no boilerplate to retain data across lifecycle events.
Read full answerHide full answer
Applies toAndroid framework, API 11–36 (LoaderManager and related classes deprecated since API 29 / Android 10)
What Loaders Are
Loaders are part of the android.app and android.content packages, introduced in API 11 (Honeycomb) as the official way to perform background data loading bound to a component’s lifecycle.
LoaderManager: The controller you obtain via getLoaderManager(); it creates, manages and restarts loaders on your behalf.
LoaderManager.LoaderCallbacks<D>: The callback interface you implement to create the loader, receive results and handle resets.
Loader<D>: The abstract base class that performs the load on a background thread and delivers the result on the main thread.
CursorLoader: A concrete loader that queries a ContentProvider and returns a Cursor, automatically registering a ContentObserver to reload when the underlying data changes.
AsyncTaskLoader<D>: A base class for custom loaders that run work using an AsyncTask (and AsyncTask itself is deprecated in API 30).
Why They Were Useful
Asynchronous by default: All load work runs off the main thread, so large database queries or network calls never freeze the UI.
Survive configuration changes: When the device rotates, the Activity is destroyed and recreated, but the loader retains its data and delivers it to the new instance without re-querying.
Lifecycle-aware delivery: Loaders are started and stopped with the component; they do not continue delivering callbacks to a stopped or destroyed Activity, which prevents crashes and leaks.
Automatic data observation: CursorLoader watches the source ContentProvider and re-delivers fresh results whenever the data changes, keeping the UI in sync with no manual polling.
No manual thread management: You did not need Handler, AsyncTask, or retained fragments to coordinate background work and UI updates.
Limitations That Led to Deprecation
Limitation
Impact
Tied to Activity/Fragment
Loaders could not be owned by a ViewModel, making them hard to share and test.
Verbose callback API
Three callback methods plus manual loader ID management added boilerplate.
Cursor-centric design
Built around ContentProvider/Cursor, awkward for modern data sources like Retrofit or Room.
AsyncTask dependency
AsyncTaskLoader relies on AsyncTask, deprecated in API 30.
No structured concurrency
No cancellation scopes, no coroutine-based composition or Flow transformation.
Because of these issues, LoaderManager was deprecated in API 29, and the whole Loader framework is now considered legacy.
Recommended Replacement
For new code on Android 16 (API 36) with Kotlin 2.x and Jetpack Compose:
ViewModel + StateFlow (or LiveData): Hold UI state in a ViewModel; launch loading with viewModelScope.launch using Dispatchers.IO for database work.
Kotlin coroutines and Flow: Replace loader callbacks with suspend functions and cold/hot streams that integrate with Compose via collectAsStateWithLifecycle().
Paging 3 library: Replaces CursorLoader pagination; use Pager with a PagingSource (Room) or RemoteMediator (network + local cache), exposed as Flow<PagingData<T>>.
Room: For local data, Room suspend DAO functions run on Dispatchers.IO automatically when called from a coroutine.
Loaders still function on current Android versions, so they appear in older codebases, but any new feature should use the Architecture Components stack above rather than LoaderManager.
When You Might Still See Loaders
Legacy apps targeting older API levels that query ContentProvider data with CursorLoader.
Code written before Architecture Components (pre-2017) that has not been migrated.
Interview discussions of Android lifecycle and background-loading history, where knowing loaders shows you understand why ViewModel and Paging exist today.
If you encounter loaders in maintenance work, migrate incrementally: replace each CursorLoader with a Room DAO + Flow query, and move the loading logic into a repository called from a ViewModel.
40
What does Android's JobScheduler API do, and when should an app use it instead of running background work directly?
KnownRevise
JobScheduler is an Android system service that schedules deferrable, constraint-driven background work such as syncing data, uploading logs, or prefetching content. Instead of starting work immediately, the app declares when the job may run and what conditions must be met, then the operating system batches and executes jobs in a battery-efficient way.
Read full answerHide full answer
Applies toAndroid API 21+ (Lollipop) through Android 16 (API 36); Kotlin 2.x
What It Schedules
JobScheduler is designed for work that does not need to happen right now:
Deferrable work: tasks that can wait until constraints are satisfied.
Persistent work: scheduled jobs survive device reboots, so they remain valid after a restart.
Batched execution: the system can run jobs from multiple apps together to reduce wakeups and power use.
Retriable work: failed jobs can be retried with a backoff policy.
It is not suitable for work that must run at an exact wall-clock time or for background work that must continue while the user is not actively using the app and the process has been killed; for continuing user-visible work, use a foreground service with an appropriate service type.
Core Components
JobInfo: an immutable description of the job. It contains the job ID, the target JobService, timing constraints, and environmental constraints.
JobService: the entry point where the work runs. It extends android.app.job.JobService and implements onStartJob() and onStopJob(). The service must declare the android.permission.BIND_JOB_SERVICE permission in the manifest so the system can bind to it.
JobScheduler: obtained from Context.getSystemService(Context.JOB_SCHEDULER_SERVICE). Calling schedule() registers the JobInfo.
JobParameters: passed into the service; it is supplied to jobFinished() so the system knows whether the job completed or should be rescheduled.
Common Constraints and Behavior
A JobInfo.Builder can express conditions such as:
Network: setRequiredNetworkType(JobInfo.NETWORK_TYPE_UNMETERED) or NETWORK_TYPE_ANY.
Charging: setRequiresCharging(true).
Idle: setRequiresDeviceIdle(true).
Latency: setMinimumLatency() for the earliest acceptable start.
Deadline: setOverrideDeadline() for the latest acceptable start.
Retry policy: setBackoffCriteria() with linear or exponential backoff.
Recurrence: setPeriodic() for jobs that should run repeatedly.
The system decides the exact start time. Meeting a deadline may cause constraints such as charging or idle to be relaxed, so jobs should not assume that every requested condition was true when they ran.
Relationship to WorkManager and Best Practices
For most modern apps, the recommended API is WorkManager, part of Android Jetpack. It provides a higher-level, lifecycle-aware abstraction over JobScheduler, adds explicit support for chained work, persistent work with backoff, and observable progress, while delegating to JobScheduler where available.
Concern
JobScheduler
WorkManager
Level
Platform system service
Jetpack library
Persistence
Jobs persist across reboot
Optional persistent work
Chaining
Not built in
Supports chained work graphs
Current guidance
Lower-level API still available
Preferred for deferrable background work
Use JobScheduler when writing a thin platform-level component or when a library cannot depend on WorkManager; otherwise prefer WorkManager. Avoid scheduling duplicate jobs with the same ID, because a new JobInfo with an existing ID replaces the previous job, and keep long-running work off the main thread.
41
What impact does Android's Doze mode have on the execution of background services and scheduled tasks?
KnownRevise
Doze mode is a power-saving state introduced in Android 6.0 (API 23) that significantly restricts background activity when the device is idle and unplugged. It defers CPU work, network access, and alarms to preserve battery life, meaning standard background services and scheduled tasks may not run as expected unless designed to handle these constraints.
Read full answerHide full answer
Applies toAndroid 6.0 (API 23) through Android 16 (API 36)
What Is Doze Mode
Doze activates when the device is idle (screen off and not charging; on API 23 the device must also be stationary for a period, while API 24+ adds a lighter doze state that can apply even when the device is moving). Instead of keeping the CPU awake, the system enters periodic maintenance windows. During idle periods, apps cannot access the network or execute background code. This behaviour persists through Android 16, though additional background limits were layered on top in later versions (e.g., API 26 background service limits).
Key Restrictions on Background Work
When Doze is active, the following are deferred or blocked: - Network Access: Apps cannot open sockets or perform HTTP requests outside maintenance windows. - Alarms: AlarmManager triggers via set() and setExact() are deferred to the next maintenance window; only setExactAndAllowWhileIdle() and setAlarmClock() can fire on time. - Sync Adapters: Content provider syncs are postponed until the device exits Doze. - Background Services: Services may be stopped or prevented from starting, especially on API 26+ where implicit background services are restricted. - Wi-Fi Scans: Passive scanning is limited to conserve power.
Exceptions and Allowed Operations
Certain operations bypass Doze restrictions to ensure critical functionality: - High-Priority FCM: Messages with priority: high can wake the device briefly to deliver the payload. - Exact Alarms: setAlarmClock() and setExactAndAllowWhileIdle() fire on time, but SCHEDULE_EXACT_ALARM permission is required for apps targeting API 31+. - Foreground Services: Services showing a persistent notification may run, but Android 14 (API 34) requires declaring specific foreground service types in the manifest. - User Interaction: Activities launched by the user exit Doze temporarily. - Device Owner Apps: Device policy controllers may be exempt from Doze restrictions.
Recommended Solution: WorkManager
Instead of managing alarms or services manually, use WorkManager. It abstracts compatibility across API levels and respects Doze constraints automatically by leveraging JobScheduler (API 23+) or fallback mechanisms. Work ensures deferrable tasks run during maintenance windows without violating battery policies, and it integrates with the recommended architecture (ViewModel, coroutines, and Flow) for observing work status.
Best Practices and Common Pitfalls
Avoid Whitelisting: Do not request ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS unless absolutely critical (e.g., alarm clock apps); Google Play may reject apps that misuse it.
Use WorkManager: Never use AsyncTask (deprecated in API 30) or IntentService (deprecated in API 30) for background work; use coroutines, WorkManager, or foreground services instead.
Test Thoroughly: Use adb shell dumpsys battery unplug and adb shell dumpsys deviceidle force-idle to simulate Doze during development, and adb shell dumpsys battery reset to exit it.
Exact Alarms: Only request exact alarm permissions if your app is a clock or calendar; otherwise use flexible intervals via WorkManager.
Foreground Services: If required, declare the correct foreground service type in the manifest for API 34+ to avoid runtime crashes.
42
Can you explain what a SurfaceView is in Android and describe the situations where it is the appropriate choice?
KnownRevise
A SurfaceView is a special Android View that provides a dedicated drawing surface embedded inside the normal view hierarchy, but backed by a separate surface that the system composites behind the application window. Because it owns its own surface, you can render to it from a separate thread without blocking the UI thread, which makes it the right tool for performance-intensive content such as games, camera previews, and video playback.
Read full answerHide full answer
Applies toAndroid 16 (API 36), Kotlin 2.x
How It Works
A SurfaceView holds a Surface that is managed separately from the rest of the window and composited by the system.
You access the surface through a SurfaceHolder, obtained via getHolder().
You register a SurfaceHolder.Callback to receive surfaceCreated(), surfaceChanged(), and surfaceDestroyed() lifecycle callbacks.
A background render thread locks the canvas with holder.lockCanvas(), draws into it, and then posts the result with holder.unlockCanvasAndPost(canvas).
The surface is destroyed and recreated when the SurfaceView changes size or visibility, so your render loop must respond to the callbacks.
When to Use It
Games and custom render loops: where you need a steady high frame rate and cannot risk UI-thread jank.
Camera preview: the legacy Camera pipeline and many custom Camera2/CameraX implementations render preview frames directly to a SurfaceView.
Video playback: MediaPlayer and Media3 (the current successor to the end-of-life ExoPlayer) can direct decoded video output to a SurfaceView.
OpenGL ES rendering: the underlying surface can be bound to an EGL context for hardware-accelerated 3D graphics.
SurfaceView vs TextureView
Aspect
SurfaceView
TextureView
Surface location
Separate surface behind the window
Texture composited into the window like a normal View
Threading
Can render off the UI thread
Rendering is tied to the UI thread
Performance
More efficient, lower latency
Slightly more overhead
Transformations
Cannot be rotated, scaled, or alpha-animated as part of the window
Supports alpha, rotation, scale, translation
Z-order control
Can be placed behind or on top via setZOrderOnTop()
Follows normal View z-order
Typical use
Games, camera, video
Content that needs View animations
Limitations and Caveats
It is a “hole” in the window: the content is composited separately, so View animations, alpha, and transformations do not affect what is drawn inside.
Scrolling containers: the surface is repositioned asynchronously relative to the view hierarchy, so SurfaceView has historically been prone to flicker in scrolling lists and is still not recommended inside ListView/RecyclerView items.
Lifecycle discipline: you must stop your render thread in surfaceDestroyed() or you will draw to an invalid surface and crash.
Compose integration: you can embed a SurfaceView in Jetpack Compose with AndroidView, but Compose-first apps should prefer Compose-native drawing APIs unless a separate surface is genuinely required.
Best Practices
Always render on a background thread using coroutines or a dedicated thread; never lock the canvas on the UI thread for heavy drawing.
Start the render loop in surfaceCreated() and cleanly stop it in surfaceDestroyed() to avoid leaks and crashes.
Use setZOrderOnTop(true) only when the surface must appear above other window content; keeping it behind is more power-efficient.
For new camera work, prefer CameraX with PreviewView (which manages a TextureView or SurfaceView internally) instead of handling a SurfaceView directly.
Reach for SurfaceView only when you truly need a separate surface and background-thread rendering; for ordinary custom drawing, a Compose Canvas or a regular View is simpler and safer.
43
How would you implement user gesture handling, such as taps, swipes, and long presses, in a modern Android application using Jetpack Compose?
KnownRevise
In modern Android development, gesture detection is best implemented using Jetpack Compose’s pointer input system, which replaces the traditional GestureDetector and onTouchEvent APIs found in the View system. This approach integrates declaratively with the UI layer, allowing you to attach gesture detectors directly to composables via modifiers while keeping business logic in a ViewModel.
Historically, Android Views relied on GestureDetector and overriding onTouchEvent. These are considered legacy in favor of Compose for new development. Compose uses Modifier.pointerInput to run coroutine-based detection within the composition scope. This eliminates boilerplate listener classes and aligns gesture handling with the declarative UI lifecycle. It works on all API levels supported by Compose (API 21+), abstracting away platform-specific touch event differences.
Common Gesture Detectors
The androidx.compose.foundation.gestures package provides suspend functions for common patterns: - detectTapGestures: Handles taps, double taps, and long presses. - detectDragGestures: Tracks horizontal and vertical dragging with start, stop, and cancel events. - detectTransformGestures: Manages multi-touch zooming and rotation. - detectHorizontalDragGestures / detectVerticalDragGestures: Constrain dragging to a single axis. Each function runs inside a coroutine scope, allowing you to call suspend functions or safely update state without blocking the main thread.
Integrating with Architecture
Gesture handlers should not contain business logic. Instead, they invoke functions exposed by a ViewModel. This ensures UI state survives configuration changes and remains testable. Use StateFlow or MutableState to reflect gesture outcomes in the UI. For example, a swipe might trigger a data refresh via a repository, executed on Dispatchers.IO within the ViewModel, keeping the Composable thin. The Composable should observe the ViewModel’s state using collectAsStateWithLifecycle, ensuring the UI updates reactively without manual invalidation calls.
In this snippet, pointerInput uses viewModel as a stable key. The detection logic suspends until a gesture occurs, then calls non-suspend ViewModel methods. Ensure viewModel is stable (e.g., provided via Hilt) to avoid unnecessary recomposition of the gesture detector. The offset parameter gives the tap or long-press position; change.consume() marks the pointer as handled so parents do not also react.
Best Practices and Pitfalls
Avoid Main Thread Blocking: Never perform network or disk operations inside gesture callbacks; delegate these to the ViewModel using coroutines.
Gesture Consumption: Be aware that parent composables may consume gestures (e.g., LazyColumn scrolling). Use pointerInput on specific children or adjust Modifier ordering.
Accessibility: Ensure gestures have alternative triggers, such as clickable semantics for screen readers, using Modifier.semantics.
Testing: Test gesture logic by exposing state flows in the ViewModel rather than simulating touch events in UI tests where possible.
Deprecation Warning: Do not use android.view.GestureDetector in new Compose code; it requires interop layers that reduce performance and type safety.
Context Leaks: Avoid capturing Context inside gesture lambdas; pass required data through the ViewModel instead.
44
What mechanisms does Android use to protect an app’s data and sensitive information?
KnownRevise
Android protects an app’s data through a defense-in-depth model built around the application sandbox, where each app runs in its own process with its own Linux user ID and cannot freely access another app’s files, memory, or components. On top of the sandbox, Android adds permission enforcement, scoped storage, hardware-backed cryptography, file-based encryption (full-disk encryption on legacy devices), and verified boot, so that data is isolated at runtime and protected when the device is locked or compromised.
Read full answerHide full answer
Applies toAndroid 16 (API level 36), with version-specific behavior noted per feature.
The Application Sandbox
Per-app UID and process isolation: Each app is installed with a unique Linux user ID and runs in its own process, so file ownership and process boundaries enforce isolation by default.
SELinux: Security-Enhanced Linux applies mandatory access control to all processes, confining apps and system services even if an app is compromised.
Verified Boot: The platform verifies the integrity of the bootloader, OS, and system partitions at startup and can restrict access to sensitive data after a verified-boot failure.
seccomp and other hardening: System calls are filtered, and exploit mitigations reduce the impact of memory-safety bugs.
Data Storage Protections
Android combines filesystem isolation with encryption and access restrictions.
Storage option
Protection
Notes
Internal storage (filesDir, cacheDir)
App-private, sandbox-isolated, encrypted at rest by file-based encryption
Not shared with other apps by default
External storage / shared media
Scoped storage, media-store access controls
Apps get broad access only to their own media; API 29 introduced scoped storage and API 30 enforced it more broadly
SharedPreferences
App-private, encrypted at rest by file-based encryption
Sensitive values should still not be stored in plaintext; prefer EncryptedSharedPreferences or Encrypted DataStore
DataStore (Preferences/Proto)
App-private; not encrypted by default
Use Encrypted DataStore (AndroidX Security) for sensitive values
Room database
App-private SQLite storage
Encrypt with SQLCipher or store secrets elsewhere when needed
Android Keystore
Keys stay in hardware or the TEE/StrongBox
Key material is not exposed to the app process or OS
File-based encryption: Different files can be encrypted with different keys, tied to device state such as unlocked vs. locked-after-boot.
Credential-encrypted vs. device-encrypted storage: Data protected by the user’s lock-screen credential is only available after the first unlock after reboot.
Permissions and Access Controls
Install-time vs. runtime permissions: Dangerous permissions such as location, camera, microphone, contacts, and calendar require explicit user approval at runtime since API 23.
Scoped storage limits: Broad filesystem access requires special app access, and raw paths to shared storage are restricted.
Foreground service requirements: Background access to location, microphone, or camera is constrained, and API 34 requires declaring a foreground service type.
Notification permission: Apps must request POST_NOTIFICATIONS on API 33+.
Hardware identity attestation: Play Integrity API and hardware key attestation let servers verify that a request comes from a genuine app on a genuine device.
Cryptography and Keystore
Android Keystore keeps private keys in a hardware-backed Keymaster/KeyMint implementation when supported, so keys cannot be extracted even from a rooted device.
Key properties: Keys can be bound to user authentication, requiring biometric or device credential before use.
BiometricPrompt is the recommended API for biometric authentication; older fingerprint APIs are legacy and should be avoided.
EncryptedSharedPreferences (AndroidX Security) is still supported, but for new apps the recommended approach is Encrypted DataStore (Jetpack DataStore with security crypto), which integrates with the DataStore API and uses the Keystore under the hood.
Network and Runtime Protections
TLS by default: Cleartext traffic is blocked for apps targeting API 28+, and network_security_config.xml can pin certificates or restrict trusted CAs.
App signature verification: Updates must be signed with the same key, and signature schemes such as APK Signature Scheme v3 support key rotation.
Isolated processes and sandboxing flags: Highly risky code can run in an isolated process with reduced permissions.
Google Play Protect scans apps for malicious behavior, complementing platform-level controls.
Best Practices and Common Pitfalls
Store only what you need, and prefer storing tokens, keys, and credentials in the Keystore rather than files or preferences.
Never write sensitive data to external storage, because it is not reliably private even under scoped storage.
Request the minimum set of permissions, and check for them at runtime instead of assuming earlier grants persist.
Do not disable TLS or allow cleartext traffic except for local debugging, and never ship that configuration.
Verify server-side with app attestation when protecting high-value data, because client-side checks alone can be bypassed.
Avoid legacy security APIs such as AsyncTask-based credential flows or direct file paths to shared storage; use modern equivalents such as coroutines, Storage Access Framework, or MediaStore.
45
How would you describe the Android Keystore system and what role does it play in protecting cryptographic keys on an Android device?
KnownRevise
The Android Keystore system is a secure container within the Android operating system that allows applications to generate and store cryptographic keys in a way that prevents them from being extracted from the device, even if the device is rooted. It offloads key material to hardware-backed security modules whenever possible, ensuring that sensitive operations like encryption and signing happen inside a trusted environment rather than exposing raw keys to app memory.
Read full answerHide full answer
Applies toAndroid API 23+ (hardware-backed guarantees vary by device), Kotlin 2.x, Android 16 (API 36)
Core Purpose and Security Model
The primary goal of the Keystore is to make key extraction difficult. Instead of generating keys in app memory where they could be dumped via debugging tools or memory scanning, the system generates them within a protected container. When hardware backing is available, the keys never leave the Trusted Execution Environment (TEE) or Secure Element (SE). The app only receives handles or aliases to request cryptographic operations, meaning the private key material is never exposed to the application process. This isolation protects keys from both remote attacks and physical extraction attempts.
Hardware Backing and API Levels
The AndroidKeyStore JCE provider was introduced in API 18 (Android 4.3), but the significant security guarantees arrived with Android 6.0 (API 23), which added hardware-backed keys and the KeyGenParameterSpec used to generate them. The system prefers hardware backing when available, and you can verify where a key actually resides at runtime by checking KeyInfo.isInsideSecureHardware() (API 23+). On API 28 (Android 9) and higher, the system also provides stronger attestation capabilities to verify key properties and confirm that a key is hardware-backed. If hardware backing is unavailable, keys are still protected by the OS sandbox, but physical extraction risks increase significantly.
Key Usage and Restrictions
Keys stored in the Keystore are bound to specific purposes and constraints defined during generation. You can restrict a key to encryption only, signing only, or specific algorithms like AES or RSA. Additionally, you can enforce user authentication requirements, such as requiring a PIN, password, or biometric verification before the key can be used (API 23+). Keys can also have expiration dates or require re-authentication after a set timeout. These restrictions are enforced by the hardware or the OS, preventing misuse even if the application logic is compromised.
Integration with Biometrics and Authentication
The Keystore is commonly paired with the BiometricPrompt API (API 28+, or the androidx.biometric library for backward compatibility) to gate key usage behind user presence. When generating a key, you call setUserAuthenticationRequired(true) on the KeyGenParameterSpec.Builder, which binds the key to the device’s lock screen credentials. During cryptographic operations, you pass a CryptoObject initialized with the key to the biometric prompt. If the user authenticates successfully, the operation proceeds; otherwise, it fails. This pattern is essential for high-security flows like payment authorization or accessing sensitive health data without storing passwords locally.
Best Practices and Common Pitfalls
Always generate keys within the Keystore rather than importing external private keys unless absolutely necessary, as imported keys may not be hardware-backed. Never assume hardware backing exists without checking KeyInfo. Handle UserNotAuthenticatedException gracefully when authentication is required. Avoid performing heavy cryptographic operations on the main thread; dispatch them to Dispatchers.IO using Kotlin coroutines. Finally, remember that Keystore keys are tied to the app’s UID and package name, so they are lost if the app is uninstalled or its data is cleared, requiring a backup strategy for recoverable data.
46
What protection levels can be specified when declaring a permission in the Android manifest, and how do they differ in terms of granting and user approval?
KnownRevise
Every permission declared in the Android manifest carries a protection level that tells the package manager how risky the permission is and whether the system can grant it automatically, the user must approve it at runtime, or the requesting app must share a signing certificate with the declaring app.
Read full answerHide full answer
Applies toAndroid 16 (API 36); the four core levels are consistent across all Android versions
The Four Protection Levels
Protection Level
How It Is Granted
User Prompt
Typical Use Case
normal
Automatically at install time
No
Low-risk, app-isolated features like ACCESS_NETWORK_STATE
dangerous
Only after explicit user approval
Yes, at runtime on API 23+
Access to user data or device capabilities like CAMERA, READ_CONTACTS
signature
Only if the requesting app is signed with the same certificate
No
Sharing data or features between apps from the same developer
signatureOrSystem
System image or same certificate
No
Deprecated; do not use
How Each Level Behaves
normal: These permissions cover features that pose little risk to the user or other apps, such as setting the wallpaper or checking network connectivity. The system grants them automatically when the app is installed, and they are listed in the app’s settings without requiring any runtime request.
dangerous: These permissions cover areas where the app wants data or capabilities that affect user privacy or device operation. On devices running API 23 (Android 6.0) or higher, when the app targets API 23 or higher, the app must request these at runtime using the Activity Result APIs (the older requestPermissions callback pattern and onRequestPermissionsResult are still supported, but registerForActivityResult with ActivityResultContracts.RequestPermission is the recommended approach).
signature: The system grants these only when the app requesting the permission is signed with the same signing key as the app that declared it. This is the standard choice for permissions shared within a suite of your own apps, because it does not expose the feature to arbitrary third-party apps.
signatureOrSystem: This was a less secure extension of signature that also granted the permission to apps bundled in the system image. It is deprecated and its replacement is signature; apps targeting newer API levels should never rely on it.
Runtime Permission Requests (API 23+)
For dangerous permissions on API 23 and above, installation-time granting is not enough. The app must check whether the permission is already granted, request it if not, and handle the case where the user denies it. A denial means the related feature should be disabled or degraded gracefully rather than crashing. If the user selects “don’t ask again,” the app should direct them to the system settings screen to enable the permission manually.
Protection Level Flags
The android:protectionLevel attribute accepts a base level optionally combined (bitwise OR) with flags that further refine granting behavior, mainly for dangerous permissions. Flag availability varies by API level:
system: restricts granting to apps that are part of the system image.
development (API 29+): only granted on development builds; not granted on production devices.
appop (API 23+): ties the permission to an app-ops switch the user can toggle.
pre23 (API 23+): only granted to apps that targeted API 22 or lower.
instant (API 26+): allows an instant app to request the permission.
runtimeOnly (API 28+): the permission can only be granted at runtime, never at install time.
Best Practices and Common Pitfalls
Request minimally: only declare and request dangerous permissions your app genuinely needs; unnecessary requests increase uninstall risk.
Explain before requesting: show a brief rationale so the user understands why the permission is needed.
Never hard-fail on denial: design features to degrade when a permission is refused.
Avoid signatureOrSystem: it is deprecated; use signature instead.
Custom permissions: when defining your own permission for inter-app communication, prefer signature unless there is a clear reason to expose it more broadly.
Check flag support: protection-level flags depend on the API level; verify each flag against the documentation for your target SDK.
47
What are the recommended approaches for safely persisting confidential data on an Android device?
KnownRevise
To securely store sensitive information on Android, you should use the Android Keystore System combined with Jetpack Security libraries like EncryptedSharedPreferences and EncryptedFile, ensuring keys are hardware-backed whenever the device supports it. Never hardcode secrets or store them in plain text within standard SharedPreferences, SQLite, or external storage.
The Android Keystore System lets you store cryptographic keys in a protected container, making it harder to extract them from the device. Introduced in API level 18, it supports hardware-backed storage on most modern devices, meaning keys never leave the Trusted Execution Environment (TEE) or Secure Element when the hardware supports it. When generating keys, specify user authentication requirements (for example biometric or PIN) using KeyGenParameterSpec to enforce access controls before cryptographic operations can proceed.
Jetpack Security Library
The androidx.security:security-crypto library abstracts Keystore complexity into high-level primitives. EncryptedSharedPreferences wraps standard SharedPreferences using a master key from the Keystore, encrypting both keys and values. EncryptedFile provides similar protection for files stored in internal storage. Both handle initialization vectors internally, reducing the risk of cryptographic misuse; key rotation must be planned and triggered at the app level. For new code, Jetpack DataStore is the recommended replacement for SharedPreferences, and sensitive values stored in it should be encrypted before persistence.
Architecture Integration
In a modern Android architecture, access to sensitive data should occur off the main thread within a repository layer. Use Kotlin coroutines with Dispatchers.IO to read or write encrypted data, ensuring UI responsiveness. Combine this with BiometricPrompt (framework API available since API level 28, or the androidx biometric library for broader support) for user authentication before revealing data, and store the authentication result temporarily in memory rather than persisting it. The ViewModel should hold decrypted state via StateFlow only while needed, clearing it when the UI scope is finished.
Common Pitfalls
Plain Text Storage: Avoid unencrypted SharedPreferences, even if the app is private; rooted devices can read the XML files in the app data directory.
Hardcoded Keys: Never embed encryption keys or API secrets in source code or strings.xml; use build-time injection or server-side retrieval.
Logging Sensitive Data: Ensure no sensitive values reach Log.d or crash reporters, as logs are accessible via adb.
Assuming Hardware Backing: Keystore keys are only hardware-backed if the device supports it; check KeyInfo.isInsideSecureHardware (API 23+) when strong guarantees are required.
Backup Compatibility: Encrypted files may not survive standard Android Backup unless configured explicitly; test restore flows carefully.
48
Can you explain what HTTPS is and describe the correct way to enforce secure TLS connections within an Android application?
KnownRevise
HTTPS (HTTP Secure) is the standard protocol for secure communication, layering HTTP over TLS (Transport Layer Security) to encrypt data in transit. On Android, enforcing it means ensuring all network traffic uses TLS 1.2 or higher and blocking insecure cleartext HTTP, which is primarily achieved through manifest configuration and a properly configured HTTP client like OkHttp.
Read full answerHide full answer
Applies toAndroid 16 (API 36) with Kotlin 2.x, OkHttp 4.12+ and Retrofit 2.11+
Core Security Guarantees
Encryption: Prevents eavesdroppers on public Wi-Fi from reading requests or responses.
Integrity: Detects if data is tampered with during transmission.
Authentication: Verifies the server identity via certificate chains signed by trusted certificate authorities (CAs).
Enforcing TLS at the System Level
For apps targeting API 28 or higher, usesCleartextTraffic defaults to false, blocking plaintext HTTP automatically on all devices. You should still declare it explicitly in AndroidManifest.xml to prevent accidental regression if your build targets an older level. For finer control, such as allowing cleartext only to a local debug server or pinning certificates, define a res/xml/network_security_config.xml file and reference it in the manifest. This config is honoured by HttpURLConnection, OkHttp, and other networking APIs that rely on the platform’s trust store.
Configuring the HTTP Client
OkHttp enables TLS by default and validates certificates against the system trust store automatically, so you do not need custom code to turn HTTPS on. Two rules matter most: never disable hostname verification or install a custom TrustManager that accepts all certificates, as this defeats the purpose of HTTPS; and constrain the accepted protocol versions with a ConnectionSpec so that deprecated TLS versions cannot be negotiated. Modern apps typically wrap OkHttp in Retrofit for type-safe API calls.
// Enforce TLS 1.2+ only, then provide this client as a singleton via Hilt
val tlsSpec = ConnectionSpec.Builder(ConnectionSpec.MODERN_TLS)
.tlsVersions(TlsVersion.TLS_1_2, TlsVersion.TLS_1_3)
.build()
val client = OkHttpClient.Builder()
.connectionSpecs(listOf(tlsSpec))
.build()
val retrofit = Retrofit.Builder()
.baseUrl("https://api.example.com/")
.addConverterFactory(GsonConverterFactory.create())
.client(client)
.build()
Best Practices and Pitfalls
Certificate Pinning: For high-security apps, OkHttp’s CertificatePinner can restrict trust to your specific server certificate, protecting against a compromised CA; note that pinning adds operational cost and is being phased out of OkHttp’s own recommendations.
API Levels: The secure usesCleartextTraffic="false" default applies only when your app targets API 28 or higher; if you target an older level, set the flag explicitly to get the same protection on every device.
No Secrets: Never store API keys or tokens in code; use the Android Keystore for key material or a backend-for-frontend pattern for secrets.
Debug Configs: Keep insecure debug overrides (such as trusting user-added CAs or allowing cleartext) in a debug source set config, never in the release build.
49
Describe how the testing framework for Android is organized and which libraries you use to test each layer of an app.
KnownRevise
The Android testing framework is the set of JUnit-based runners, Jetpack test libraries, and Gradle source sets that let you run local JVM unit tests, on-device instrumentation tests, and UI tests from one Android Studio project. It separates tests into local unit tests in the test source set and instrumented tests in the androidTest source set, and it centers on AndroidJUnit4, AndroidX Test rules, and Compose test rules for UI.
Read full answerHide full answer
Applies toAndroid 16 (API level 36) with Kotlin 2.x and Jetpack Compose (managed through the Compose BOM).
Test Source Sets and Runners
test (local unit tests): plain JVM tests, fast, no device required. Run with JUnit; use testImplementation in build.gradle.kts.
androidTest (instrumented tests): run on a real device or emulator through the AndroidJUnit4 instrumentation runner, so they have access to the Context, the Activity lifecycle, and the real UI. Use androidTestImplementation.
Runner: instrumented tests are annotated with @RunWith(AndroidJUnit4::class) and executed by the AndroidJUnit4 runner, which is provided by androidx.test.ext:junit and androidx.test:runner.
AndroidJUnit4, AndroidX Test rules, Hilt testing, Room in-memory DB
Compose UI test
androidTest
Device/emulator
createAndroidComposeRule, ComposeTestRule assertions and actions
View-based UI test
androidTest
Device/emulator
Espresso (legacy XML Views)
Cross-app / black-box
androidTest
Device/emulator
UI Automator
Unit Testing the Kotlin and Flow Layers
Assertions and doubles: JUnit for structure, Google Truth for readable assertions, and MockK (or Mockito) for mocks. Prefer small hand-written fakes of repositories over mocking every class.
Coroutines: use runTest from kotlinx-coroutines-test so virtual time controls delays and background work, and install a TestDispatcher via Dispatchers.setMain so ViewModel scopes are deterministic.
Flow and StateFlow: collect emissions in tests with Turbine (turbine artifact), which lets you assert a sequence of emitted states without real delays.
Android framework code on the JVM: Robolectric provides a simulated Android framework inside local unit tests when you need classes such as Context or Bundle without a device.
Instrumented and UI Testing
Lifecycle-scoped tests: use ActivityScenarioRule to launch and finish an Activity; the older ActivityTestRule is deprecated in favor of ActivityScenario/ActivityScenarioRule.
Compose UI: createAndroidComposeRule (or ComposeTestRule with a Context) gives you the app’s Context plus Compose semantics APIs such as onNodeWithText, performClick, waitForIdle, and onIdle. This is the default way to test Compose UI.
View-based UI: Espresso (onView, perform, check) remains the tool for legacy XML View hierarchies; for new Compose UI, use the Compose test rules instead.
Dependency injection with Hilt: annotate tests with @HiltAndroidTest and add HiltAndroidRule so instrumented tests get the Hilt-generated components; provide test bindings with @TestInstallIn modules.
Room: build an in-memory database with Room.inMemoryDatabaseBuilder in instrumented tests so DAO tests are hermetic and need no file cleanup.
WorkManager: initialize WorkManager for tests with WorkManagerTestInitHelper and build workers with TestListenableWorkerBuilder from androidx.work:work-testing, which lets you run ListenableWorker logic synchronously without scheduling on the real system.
Gradle Configuration
Dependencies come from the version catalog (libs.versions.toml) and are wired in build.gradle.kts with testImplementation for JVM tests and androidTestImplementation for instrumented tests, for example libs.junit, libs.robolectric, libs.kotlinx.coroutines.test, libs.turbine, libs.androidx.junit, libs.androidx.espresso.core, libs.androidx.compose.ui.test.junit4, libs.hilt.android.testing, and libs.androidx.work.testing.
Best Practices and Common Pitfalls
Keep tests hermetic: never depend on the real network or filesystem; inject a fake repository and use in-memory Room databases.
Never block the main thread: production code must use Dispatchers.IO for disk and network, and tests must control the Main dispatcher so coroutines are deterministic.
Match the test to the layer: pure logic in local unit tests, Compose semantics in Compose UI tests, and only use Espresso/UI Automator for View-based or cross-app scenarios.
Respect version-specific behavior: when a test touches runtime permissions (API 23+), scoped storage (API 29/30), exact alarms (API 31+), POST_NOTIFICATIONS (API 33+), or foreground service types (API 34+), grant or configure those conditions explicitly in the test.
Run instrumented tests on a current device or emulator (API 34 or later is a good baseline) so the platform behavior under test matches what the shipping APK targets.
50
How do local unit tests differ from instrumentation tests in Android development?
KnownRevise
Local tests and instrumentation tests are the two test types in Android, distinguished primarily by where they execute: local unit tests run on your machine’s JVM without a device, while instrumentation tests run on an Android device or emulator with full framework access.
Local unit tests: Executed on your development machine’s local Java Virtual Machine. They live in the src/test source set and are built and run by Gradle without needing a connected device or emulator.
Instrumentation tests: Executed on a real Android device or an emulator. They live in the src/androidTest source set and require an instrumentation runner (typically AndroidJUnitRunner) to install and run the test APK alongside your app; the Android Test Orchestrator is optional and used to isolate test execution.
What They Can Access
Local unit tests: Have no access to the Android framework by default. Classes like Context, Activity, or View are unavailable unless you use a simulation layer such as Robolectric. They are ideal for testing pure Kotlin logic, ViewModels (with coroutine test rules), repositories, and use cases.
Instrumentation tests: Have full access to the Android framework, system services, Context, resources, and the UI. They can launch activities, interact with Jetpack Compose trees using the Compose UI Test library (ui-test-manifest), and verify end-to-end flows.
Comparison Table
Aspect
Local Tests
Instrumentation Tests
Execution environment
Local JVM on your machine
Android device or emulator
Source set
src/test
src/androidTest
Android framework access
None (unless using Robolectric)
Full access
Speed
Very fast (seconds)
Slower (device boot, install, launch)
Typical use
Business logic, ViewModels, repositories
UI, integration, end-to-end flows
Dependencies
JUnit, Mockk/Mockito, Turbine
AndroidX Test, Espresso, Compose UI Test
Debugging
Standard IDE debugger
Device/emulator debugging
When to Use Each
Use local tests for anything that does not need Android framework APIs: data transformations, validation rules, state reductions in a ViewModel, and repository logic with fake or in-memory data sources. Because they run instantly, you should aim for the majority of your test coverage here.
Use instrumentation tests when the code under test genuinely depends on Android: UI rendering and interaction, navigation between screens, Room database access with the real driver, WorkManager jobs, or integration with system services like NotificationManager.
Best Practices
Follow the testing pyramid: write many fast local tests, fewer integration-style instrumentation tests, and minimal full end-to-end UI tests.
Keep instrumentation tests deterministic: avoid relying on external network calls, use a fake or mock server with OkHttp’s MockWebServer, and seed the database with known data before each test.
Run local tests on every commit since they are fast; reserve instrumentation tests for CI pipelines or pre-merge checks because they take longer to execute.
For Jetpack Compose UI, use the Compose UI Test library with createComposeRule() rather than older Espresso ViewMatchers where possible, as Compose semantics testing is more robust.
Never put network or disk work in local tests expecting real Android behavior; instead abstract those dependencies behind interfaces you can fake.
51
What are the recommended practices and tools for implementing unit tests in modern Android development?
KnownRevise
Modern Android unit testing relies on local JVM-based tests run on the development machine rather than on a device, using JUnit 5 for test structure, MockK for mocking Kotlin code, and kotlinx-coroutines-test for asynchronous logic. This isolates business logic from Android framework dependencies, gives fast feedback, and reserves framework-dependent code for instrumented tests.
Android distinguishes two test locations by execution environment:
Aspect
Local Unit Tests
Instrumented Tests
Directory
src/test/java
src/androidTest/java
Runs on
Local JVM (development machine)
Emulator or device
Android SDK access
None by default (unless Robolectric is used)
Full framework, Context, resources
Typical use
ViewModels, UseCases, domain logic, mappers
UI, Room DAOs, Android API interactions
Speed
Very fast
Slower (device/emulator startup)
For pure unit testing, focus on src/test to maximize speed and reliability. Local tests cannot access Android resources or a Context by default, so classes under test should not require them.
Recommended Libraries
JUnit 5 (org.junit.jupiter:junit-jupiter:5.10.2): test structure, lifecycle annotations such as @BeforeEach, and assertions. JUnit 4 is still the default in Android Studio templates, but JUnit 5 is the recommended choice for new projects; it requires testOptions { useJUnitPlatform() } in app/build.gradle.kts or the Gradle test task will not discover the tests.
MockK (io.mockk:mockk:1.13.10): idiomatic mocking for Kotlin — supports suspend functions (coEvery/coVerify), final classes, and extension functions, which Java-centric mockers handle poorly.
kotlinx-coroutines-test (org.jetbrains.kotlinx:kotlinx-coroutines-test:1.8.0): the runTest builder and virtual-time controllers for deterministic coroutine tests.
Robolectric (org.robolectric:robolectric): runs real Android framework code on the local JVM when a test genuinely needs Context, SharedPreferences, or intents, keeping it out of src/androidTest.
Truth (com.google.truth:truth) or AssertJ: more readable assertions than plain JUnit assertions, for example assertThat(result).isEqualTo(expected).
Hilt testing artifacts are only needed for instrumented tests; in local unit tests, bypass Hilt and construct classes directly with mocked dependencies.
All of these are declared with the testImplementation configuration (Robolectric also uses testImplementation) in app/build.gradle.kts, preferably through the version catalog (libs.versions.toml) with aliases that include both group and name.
Testing Architecture
ViewModels: expose immutable StateFlow (or LiveData) state and test by triggering events and asserting on the collected state, rather than relying on observation lifecycles.
Coroutines: test with runTest so delays are skipped via virtual time; never use Thread.sleep, which causes flakiness and can block the test dispatcher.
Dependency injection: construct the class under test manually in @BeforeEach with mocked dependencies, keeping the test independent of the Hilt container and of Android instrumentation.
Dispatchers: inject a DispatcherProvider or accept a CoroutineDispatcher so tests can supply the test dispatcher; code that hardcodes Dispatchers.IO is harder to control, though runTest still works when the production dispatcher is UnconfinedTestDispatcher-injected.
Repository/UseCase layers: mock the data sources (Retrofit services, Room DAOs) so tests never touch the network or disk — network or disk work also does not belong on the main thread in production.
Best Practices and Common Pitfalls
Isolation: mock external dependencies (network, database, sensors) so each test verifies one unit of logic; never hit real APIs in unit tests.
Naming: use descriptive names such as fetchUser_returnsData or loadUser_updatesState rather than test1, so failures are self-documenting.
Avoid Context: do not pass Context into classes tested in src/test; if Android dependencies are unavoidable, use Robolectric or move the test to src/androidTest.
Prefer fakes over mocks where a simple in-memory fake (for example, a fake repository) is available — it is more robust than stubbing many methods.
Test state, not implementation: assert on observable outputs (StateFlow values, returned results) rather than verifying every internal collaborator call, so refactors do not break tests.
CI integration: run local tests on every commit; they should complete in minutes because they need no device.
Do not mix runners: AndroidJUnitRunner is the instrumented-test runner for src/androidTest, not a local-test runner; local JUnit 4 tests use the plain JUnit 4 runner, and JUnit 5 tests require useJUnitPlatform().
52
Can you explain what Robolectric does and describe the situations where you would choose it for testing Android code?
KnownRevise
Robolectric is an open-source unit testing framework for Android that runs real Android framework code inside the JVM, so you can test components like Activity, Fragment, ViewModel, Service and View logic without a device or emulator. It is mainly used for fast, local tests that need framework behaviour which plain JUnit/Kotlin tests cannot provide.
Read full answerHide full answer
Applies toRobolectric 4.15 with Kotlin 2.x and Android 16 (API 36) projects using Jetpack Compose and AndroidX.
How It Works
Sandbox in the JVM: Robolectric creates a simulated Android environment by loading the actual Android framework classes from the SDK jar rather than stubbing them out. When your test calls Context.getString(...), Lifecycle methods, or inflates a layout, real framework code executes.
Test runner: You annotate the test class with @RunWith(RobolectricTestRunner::class) (or use @RunWith(AndroidJUnit4::class) with @Config for mixed suites). The runner sets up the Application, Looper, resources, and a Shadow layer before each test.
Shadows: Shadows are Robolectric’s implementations of Android classes that would otherwise fail in a JVM (for example, hardware, sensor, or system-service classes). They let you inspect behaviour, such as which intent was broadcast or which notification was posted.
Lifecycle simulation: Helpers such as ActivityScenario (AndroidX) or Robolectric.buildActivity() start activities and drive them through onCreate, onStart, onResume and so on, so you can assert on state changes.
Key Features
No device required: Tests run in seconds in the standard JVM test source set (src/test), typically through Gradle’s testDebugUnitTest task.
Real framework behaviour: Unlike Mockito-only tests, @Config(sdk = [36]) runs against the actual API 36 framework code, so resource resolution, ViewModel creation, WorkManager scheduling, Room DAOs (with an in-memory database), and Context calls behave like they do on a real device.
Compose support: Robolectric works with createAndroidComposeRule from androidx.compose.ui.test, letting you test Compose UI on the JVM instead of an emulator.
Hermetic environment: Tests are isolated by default; each gets a fresh Application instance and can be configured with @Config(application = …) for a custom test Application.
When to Use It
Presenter/ViewModel-level tests where the class touches Context, resources, preferences, or lifecycle.
Testing Fragment or Activity logic without paying the cost of an instrumented (connected) test.
Repository and database tests combining Room with runTest from kotlinx-coroutines-test.
Verifying framework interactions such as Intent broadcasts, NotificationManager calls, SharedPreferences writes, or WorkManager requests.
Fast feedback in CI where emulator startup time is unacceptable.
When to Prefer Instrumented Tests
Aspect
Robolectric
Instrumented (androidTest)
Execution speed
Very fast (JVM)
Slower (device/emulator)
Hardware/GPU
Simulated via shadows
Real
Compose UI with animations
Limited fidelity
Recommended
Custom ROM/OEM behaviour
Not possible
Possible
CI cost
Low
Higher
Use instrumented tests for anything needing real hardware (camera, sensors, Bluetooth), exact rendering, or integration with other apps.
Best Practices and Common Pitfalls
Prefer real objects over mocks: Let Robolectric provide Context, Resources, and lifecycle; inject fakes only at your own module boundaries.
Set the SDK explicitly with @Config(sdk = [36]) when behaviour differs between API levels; otherwise tests run against a default target.
Use ApplicationProvider.getApplicationContext() to obtain a Context in tests.
Avoid UI assertions that depend on drawing — Robolectric does not render pixels the way an emulator does.
Don’t use it for network calls: keep Retrofit/OkHttp interactions behind an interface and fake it, or use MockWebServer; never hit real endpoints in unit tests.
Watch for Looper timing: use ShadowLooper.idleMainLooper() or advanceToNextPosted() when testing posted tasks, since the main looper does not pump automatically.
53
Explain what UI Automator is and how it is used for testing Android applications.
KnownRevise
UI Automator is Android’s black-box UI testing framework: it runs instrumentation tests on a real device or emulator and can drive any UI element exposed to the accessibility tree, including your own app, other apps, and the system UI itself. It is part of AndroidX Test and is the standard tool for end-to-end flows that cross application boundaries, where Espresso’s in-process, single-app scope is not enough.
Read full answerHide full answer
Applies toAndroid 16 (API 36) with Kotlin 2.x, UI Automator 2.3.x from AndroidX Test; tests run on devices and emulators, with current AndroidX Test artifacts requiring `minSdkVersion` 21.
How It Works
UI Automator tests are instrumentation tests executed by androidx.test.runner.AndroidJUnitRunner on the device under test. Unlike Espresso, which runs inside the app process and can only see that app’s view hierarchy, UI Automator talks to the platform’s accessibility layer, so it observes and manipulates windows belonging to any process:
Cross-app testing: you can launch your app, press Home, open another app, come back, and assert on the result.
System UI coverage: permission dialogs, notifications (device.openNotification()), quick settings, the recents screen and the app launcher are all addressable, which matters for tests that must accept a runtime permission (API 23+) or verify notification behaviour (POST_NOTIFICATIONS, API 33+).
No source code needed: because it works on the rendered UI, UI Automator can test apps whose internals you cannot modify, which also makes it useful for testing interactions with third-party apps.
The trade-off is that this scope comes at a cost: UI Automator knows nothing about your app’s lifecycle, coroutines or Compose recompositions, so synchronisation must be handled with explicit waits instead of Espresso’s automatic idle-resource handling.
Key APIs
UiDevice: the entry point, obtained with UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()). It exposes device-level actions such as click, pressBack, pressHome, openNotification, swipe, waitForIdle and takeScreenshot.
BySelector / By: the modern, fluent way to describe a locator, e.g. By.res(packageName, resourceId), By.text(...), By.desc(...), By.clazz(...). Matching elements are returned as UiObject2, which supports click(), text = ..., scrollUntil, isCheckable and gesture methods.
UiSelector / UiObject / UiScrollable: the older, still-supported API surface. UiScrollable remains a convenient way to scroll a list until an item is visible.
Until + UiDevice.wait: conditions such as Until.hasObject(...) or Until.findObject(...) combined with a timeout replace fixed Thread.sleep calls.
Configurator: global tuning for flakiness, e.g. Configurator.getInstance().setWaitForSelectorTimeout(...) or setWaitForIdleTimeout(...).
UI Automator vs Espresso
Aspect
UI Automator
Espresso
Scope
Any app and system UI (black box)
Single app, in-process (white box)
Synchronisation
Manual waits (wait, waitForIdle)
Automatic idle-resource sync
Compose support
Indirect, via semantics exposed to accessibility
Not Espresso — use Compose Testing (androidx.compose.ui:ui-test-junit4)
Speed
Slower, cross-process
Faster
Typical use
E2E flows, permissions, notifications, other apps
Detailed in-app UI behaviour
Code Example
@RunWith(AndroidJUnit4::class)
class SignInFlowTest {
private lateinit var device: UiDevice
@Before
fun setUp() {
device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
val context = ApplicationProvider.getApplicationContext<Context>()
val intent = context.packageManager
.getLaunchIntentForPackage(APP_PACKAGE)
?: error("No launch intent for $APP_PACKAGE")
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK) // start from a clean state
context.startActivity(intent)
}
@Test
fun entersCredentialsAndSignsIn() {
// Wait for the sign-in screen, then type into the fields
val emailField = device.wait(
Until.findObject(By.res(APP_PACKAGE, "email_field")), 5_000
)
emailField.text = "user@example.com"
device.findObject(By.res(APP_PACKAGE, "password_field")).text = "secret"
device.findObject(By.res(APP_PACKAGE, "sign_in_button")).click()
// Assert the destination screen appeared within the timeout
val signedIn = device.wait(
Until.hasObject(By.res(APP_PACKAGE, "welcome_text")), 5_000
)
assertTrue(signedIn)
}
private companion object {
const val APP_PACKAGE = "com.example.app"
}
}
Add the dependency through the version catalog (libs.versions.toml):
[versions]
uiautomator = "2.3.0"
[libraries]
androidx-uiautomator = { group = "androidx.test.uiautomator", name = "uiautomator", version.ref = "uiautomator" }
Never sleep blindly: replace Thread.sleep with device.wait(Until.hasObject(...), timeout) and tune timeouts once through Configurator instead of scattering magic numbers across tests.
Prefer By.res over text matching: resource IDs are stable across translations and less brittle than By.text, which breaks with locale or copy changes.
Scope selectors with the package name: By.res(APP_PACKAGE, "id") avoids accidentally matching a system UI element with a colliding identifier.
Use Compose testing or Espresso inside your app: for Compose UIs, createAndroidComposeRule with ui-test-junit4 is the recommended approach, since UI Automator only sees what Compose exposes through merged semantics. Keep UI Automator for the parts in-app testing genuinely cannot reach: system dialogs, notifications, and other apps.
Expect and reduce flakiness: animations, full-screen windows and slow devices can make selectors time out; disable animations on test devices and give each test a deterministic starting state by clearing the task (FLAG_ACTIVITY_CLEAR_TASK) as shown above.
Mind the threading rules: UI Automator calls run on the instrumentation thread, not the main thread, so never use them to touch app internals directly — observe the rendered UI only.
54
What are the key strategies and technical practices for optimizing the performance and responsiveness of a modern Android application?
KnownRevise
Enhancing Android app performance centers on main thread safety and minimizing resource contention through modern architecture components. By offloading heavy work from the UI thread and optimizing rendering pipelines, developers ensure smooth interactions and reduced battery consumption.
The UI thread must remain free to process input events and draw frames. Blocking it with network calls, database queries, or complex computations causes ANR (Application Not Responding) errors and jank. Use Kotlin coroutines with viewModelScope to manage background tasks:
Dispatchers.IO: disk and network operations, such as Room queries or Retrofit calls.
Dispatchers.Default: CPU-intensive work, such as parsing large JSON payloads or image transformation.
Dispatchers.Main: only for touching the UI or collecting state that must update the UI.
Results should be exposed from the ViewModel as StateFlow (or LiveData in View-based code) so the Compose UI collects them in a lifecycle-aware way with collectAsStateWithLifecycle, without manual thread switching.
Jetpack Compose Optimization
Compose performance relies on managing recomposition efficiently:
Lazy lists: use LazyColumn or LazyRow for scrollable content instead of a plain Column so only visible items are composed and measured.
State hoisting and remember: hoist state to the lowest common ancestor of the composables that read it, and wrap expensive calculations in remember so they are not recomputed on every recomposition.
Stable and immutable types: data classes whose properties are val and stable (annotated @Stable where needed) let the Compose compiler skip recomposition when the inputs have not changed.
derivedStateOf: use it for values computed from frequently changing state so downstream composables only recompose when the derived result actually changes.
Keyed items: supply stable keys to lazy list items so Compose can reuse nodes across reordering and deletion.
Background Work and Data Access
For deferrable tasks that must survive app restarts, such as syncing data or uploading logs, use WorkManager; AsyncTask and IntentService were deprecated in API 30 and coroutines or WorkManager are the replacements. For local data, Room suspend functions and Flow return types integrate with coroutines so disk access never blocks the main thread. Networking should use Retrofit with OkHttp, calling suspend functions or async enqueue methods. Always add caching and pagination (for example, the Paging library) to reduce payload size and network frequency, which directly improves perceived responsiveness.
Profiling and Measurement
Optimize based on measurement, not guesswork:
Android Studio Profiler: CPU, memory, energy and network timelines for a running app.
Macrobenchmark and Baseline Profiles: measure startup and frame timing in CI and ship pre-compiled code paths to speed cold start.
Perfetto and systrace-style traces: inspect frame drops, main-thread contention and system-level scheduling.
Common Pitfalls
Leaked scopes: launching coroutines on GlobalScope or a scope not tied to a lifecycle leaks work and memory; use viewModelScope or lifecycleScope.
Logic in composables: performing heavy computation or data fetching directly in a @Composable function causes recomposition-driven work; move it into a ViewModel.
Storing Context in a ViewModel: this leaks the Activity or Application context beyond its expected lifetime; inject a repository or use AndroidViewModel only when justified.
Ignoring version-specific behaviour: features such as runtime permissions (API 23), scoped storage (API 29/30), exact alarms (API 31+), POST_NOTIFICATIONS (API 33) and foreground service types (API 34) each change how background and UI work behaves, so target the documented API level rather than assuming older behaviour.
55
Can you explain the purpose and functionality of the Android Profiler within the Android development ecosystem?
KnownRevise
The Android Profiler is an integrated diagnostic tool within Android Studio that provides real-time visualization of an application’s resource usage, enabling developers to detect performance bottlenecks related to CPU, memory, and network consumption. It allows for deep inspection through method tracing and heap analysis without requiring external command-line utilities.
Read full answerHide full answer
Applies toAndroid Studio all versions (Profiler introduced in Android Studio 3.0), Android 16 (API 36)
Core Components
The profiler consists of three main timeline-based monitors: - CPU Profiler: Tracks method execution time and system calls. It uses the Perfetto tracing engine (which replaced the deprecated Systrace) to record native and Java/Kotlin methods. - Memory Profiler: Monitors Java heap and native memory allocation. It helps identify memory leaks by capturing heap dumps and tracking object allocations over time. - Network Profiler: Displays socket activity and data transfer volumes for apps using HttpURLConnection or OkHttp, helping spot excessive or redundant requests.
Note that the Energy Profiler was removed in Android Studio Hedgehog (2023.1); battery analysis should now be done with the separate Battery Historian tool.
How It Works
To use the profiler, connect a physical device or emulator running a debuggable build. Select the target process from the device dropdown menu in the Profiler window. You can then: 1. Monitor: View live graphs of resource usage while interacting with the app. 2. Record: Capture a trace segment for detailed offline analysis. 3. Inspect: Click on timeline events to see stack traces or allocation details.
For CPU tracing, choose between two modes:
Mode
Overhead
Accuracy
Sampled
Lower
Approximate, based on periodic sampling
Instrumented
Higher
Exact per-method timing
Memory profiling allows forcing garbage collection to observe how the app recovers memory.
Best Practices and Common Pitfalls
Profile on Physical Hardware: Emulators may not accurately reflect CPU throttling or thermal behavior found on real devices.
Avoid Main Thread Work: Use the profiler to verify that heavy computations or database queries are not blocking the UI thread, which causes ANRs.
Overhead Impact: Instrumented CPU tracing can significantly slow down app execution, potentially masking timing-sensitive bugs.
Obfuscated Traces: If profiling a release build with R8 enabled, method names will appear meaningless unless a mapping file is provided.
Combine with Metrics: Use Profiler data alongside Firebase Performance Monitoring for production insights, as the profiler is strictly for development environments.
56
What is the StrictMode tool in Android, and what kinds of problems does it help developers detect?
KnownRevise
StrictMode is a developer tool in the Android SDK that detects accidental disk reads, disk writes, and network operations on the main thread, along with VM-level problems such as leaked database cursors or unclosed resources. It runs only when you explicitly enable it, logs violations to Logcat, and can be configured to crash the app so issues are caught during development rather than reaching production.
Read full answerHide full answer
Applies toAndroid SDK, available since API level 9 (all current versions including Android 16 / API 36)
What StrictMode Does
StrictMode instruments your app at runtime to flag operations that should not happen on the UI thread or that indicate resource leaks. Because these mistakes often do not crash the app immediately, they surface later as ANRs (Application Not Responding), janky frame rendering, or memory growth. StrictMode makes them visible and loud during development so they can be fixed before release.
Disk access on main thread: reading or writing files, SharedPreferences, or Room databases from the UI thread.
Network on main thread: any socket or HTTP call on the main thread (Android already throws NetworkOnMainThreadException for this, but StrictMode can catch it earlier or more broadly).
Resource leaks: unclosed Cursor, InputStream, OutputStream, or registered receivers that outlive their owner.
Other VM issues: cleartext network traffic (detectCleartextNetwork(), API 27+), exposed file:// URIs (detectFileUriExposure(), API 24+), or non-SDK interface usage (detectNonSdkApiUsage(), API 28+).
Thread Policy vs VM Policy
StrictMode is configured through two separate policy classes, set via StrictMode.setThreadPolicy() and StrictMode.setVmPolicy().
Policy
Scope
Typical use
StrictMode.ThreadPolicy
Applies to the thread it is set on, usually the main thread
Detect disk reads/writes, network access, custom violations
StrictMode.VmPolicy
Applies to the whole process (VM)
Detect leaked cursors, closables, activity instances, file URI exposure
A ThreadPolicy is built with StrictMode.ThreadPolicy.Builder() and a VmPolicy with StrictMode.VmPolicy.Builder(). You call detectAll() to enable every check in that category, or pick individual detectors such as detectDiskReads(), detectDiskWrites(), or detectNetwork().
Penalties and Logging
Each policy has its own set of penalties that determine what happens when a violation occurs.
penaltyLog(): writes the violation stack trace to Logcat. This is the minimum you should enable.
penaltyDialog(): shows an alert dialog to the developer. Useful for interactive debugging.
penaltyDeath(): crashes the app immediately on violation. Best for CI or strict debug builds.
penaltyDropBox(): logs the violation to the system DropBox service for system-level aggregation.
For VM policies, common penalties are penaltyLog() and penaltyDeath(). A dialog penalty on a VM policy is less common since leaks are not tied to a visible thread.
Best Practices
Enable only in debug builds: wrap StrictMode setup in a build-config check so it never runs in release APKs. Violations that crash in debug should never reach users.
Set it early: configure policies in your custom Application class onCreate() so all threads and components are covered from startup.
Fix, don’t silence: use StrictMode.allowThreadDiskReads() and StrictMode.allowThreadDiskWrites() only as a narrow, temporary permit around legacy code that must run on the main thread, and restore the original policy immediately after.
Combine with coroutines: the proper fix for main-thread disk or network work is to move it off the main thread using Kotlin coroutines with Dispatchers.IO, or WorkManager for deferred background tasks.
Do not rely on it in production: StrictMode adds runtime overhead and is not a substitute for profiling tools like Android Studio Profiler or Perfetto.
Common Pitfalls
Forgetting to enable it: StrictMode does nothing unless you call the set methods, so an app with no StrictMode setup will not report violations.
Leaving it enabled in release: a penaltyDeath() left in a production build causes real crashes for users.
Treating warnings as optional: a disk read on the main thread may look harmless in testing but causes jank or ANR on slower devices.
57
What are the typical scenarios that lead to memory retention issues in Android application development?
KnownRevise
Memory leaks in Android occur when objects that should be garbage collected remain referenced unintentionally, typically through long-lived contexts or active coroutines. The most frequent sources of memory leaks involve holding Activity or Fragment contexts in static fields, non-static inner classes, and unmanaged background tasks. Understanding these patterns is essential for maintaining app stability and performance.
Static Contexts: Storing a Context (especially an Activity) in a static variable prevents the garbage collector from reclaiming the activity instance after rotation or finish().
Non-Static Inner Classes: Anonymous inner classes implicitly hold a reference to the outer class. If a background thread holds an inner class of an Activity, the Activity leaks.
Unregistered Listeners: Failing to unregister broadcast receivers, location listeners, or callbacks in onDestroy (or the appropriate lifecycle callback) keeps references alive.
Coroutine Scope: Launching coroutines in GlobalScope or without tying them to a lifecycle (like viewModelScope or lifecycleScope) can outlive the UI component.
Jetpack Compose Considerations
Compose reduces view-related leaks but introduces state retention risks. - remember: Values stored in remember survive recomposition but must not capture large objects or contexts unnecessarily. - LaunchedEffect: Must be tied to the composition lifecycle. Cancelling effects manually is rarely needed if keys are managed correctly. - DisposableEffect: Use this to clean up resources (like listeners) when the composable leaves the composition, preventing leaks from external systems.
Detection and Diagnosis
LeakCanary: The industry standard library for detecting memory leaks in debug builds. It automatically analyzes heap dumps and reports leak traces.
Memory Profiler: Built into Android Studio, it allows manual heap dumps and memory allocation tracking to identify retained objects.
StrictMode: Can detect leaked SQLiteCursor/SQLiteDatabase objects or leaked Closeable resources (enable with detectLeakedSqlLiteObjects() and detectLeakedClosableObjects()), though it is less focused on general object leaks.
Best Practices for Prevention
Use the application context instead of the Activity context when the activity lifecycle is not required.
Prefer WeakReference if holding a context reference is unavoidable in a long-lived object.
Cancel any custom coroutine scope in onCleared for ViewModels or onDestroy for Fragments; viewModelScope and lifecycleScope cancel themselves automatically.
In Compose, ensure DisposableEffect disposes of resources properly.
Integrate LeakCanary in debug builds to catch regressions early.
58
What is your approach to detecting and fixing Application Not Responding (ANR) issues in an Android application?
KnownRevise
An Application Not Responding (ANR) occurs when the main thread is blocked beyond the system’s input, broadcast, or service timeouts, so a reliable approach combines capturing main-thread stack traces from ANR logs with moving I/O, database, and heavy computation off the UI thread using coroutines or background workers.
Read full answerHide full answer
Applies toAndroid 16 (API 36), Kotlin 2.x
ANR Triggers and Timeouts
The system declares an ANR when the main thread cannot finish a pending operation within these windows: - Input dispatching timeout: 5 seconds while a touch or key event waits for handling. - BroadcastReceiver timeout: 10 seconds for onReceive to complete. - Service timeout: 20 seconds for a service to become foreground. Typical culprits are synchronous network calls, blocking Room queries, bitmap decoding, JSON parsing, or long loops executed directly on the main thread. In Jetpack Compose, expensive recomposition or layout work also counts as main-thread work.
Diagnosis
Reproduce the issue, then inspect the artifacts the system writes when it flags the process: - Logcat: Filter on ANR and the ActivityManager tag; the line ANR in <package> identifies the timeout type and process. - ANR traces: On a rooted device read /data/anr/traces.txt; otherwise run adb bugreport and extract the traces section. The file contains every thread’s stack trace at the moment of the ANR, so look for the main thread and any WAIT, RUNNABLE, or locked frames. - StrictMode: Enable StrictMode.setVmPolicy in debug builds to crash or log on accidental disk or network access on the main thread. - Android Studio Profiler: Record CPU and thread activity while reproducing the slow path to spot long main-thread tasks. - Production telemetry: Use Play Console Vitals or Firebase Crashlytics ANR tracking to catch regressions that only appear on real hardware.
Tool
What it reveals
Best used when
Logcat
ANR reason, package, timeout type
Immediately after a reproducible ANR
/data/anr/traces.txt
Full stack snapshot of every thread
Rooted device or emulator
adb bugreport
Same traces plus system state
Non-rooted devices
StrictMode
Main-thread disk/network violations
During development
Profiler
CPU and method timing during reproduction
Optimizing a known slow path
Play Console Vitals
ANR rate across the fleet
Post-release monitoring
Resolution
Once the blocking call is identified, relocate it and keep the main thread free: - Coroutines: Call suspend repository functions from viewModelScope; the default dispatcher is Dispatchers.Main, so the UI update stays on the main thread while the repository internally uses Dispatchers.IO. - WorkManager: Use it for deferrable, guaranteed background work that must survive configuration changes or app restarts, instead of starting a long service. - Chunk and cache: Split large computations into smaller pieces, cache results, and lazy-load bitmaps or resources. - Broadcast receivers: Do the minimum in onReceive and enqueue the real work with WorkManager.
Best Practices and Prevention
Never block the main thread: All I/O, database, parsing, and bitmap work belongs on Dispatchers.IO or a worker.
Keep StrictMode on in debug: Configure it in your Application class so main-thread violations fail tests rather than reach users.
Monitor ANR rate in production: Treat ANR percentage like crash rate and gate releases on it.
Test on low-end hardware: ANRs often appear only on devices with slow CPUs, thermal throttling, or limited memory.
Respect main-thread limits in Compose: Avoid heavy logic inside composables; derive UI state in the ViewModel and emit it through StateFlow.
59
Which strategies are effective for minimizing the final APK file size in Android development?
KnownRevise
Reducing APK size primarily involves enabling code and resource shrinking alongside architectural decisions like modularization and careful dependency management. By removing unused code, optimizing assets, and delivering only what each device needs, developers can significantly decrease download size and install time.
The Android build system uses R8 by default for shrinking, obfuscation, and optimization, replacing the legacy ProGuard tool. When enabled, R8 performs tree-shaking to remove unused classes, methods, and fields from the final dex files, and optimizes bytecode by removing unused instructions and merging classes where safe. - minifyEnabled: Must be set to true in the release build type to activate code shrinking. - keep rules: Custom rules in proguard-rules.pro are needed for code accessed via reflection, serialization, or JNI. - Obfuscation: Renames classes and members to short names, reducing dex size and providing basic security.
Resource Optimization
Unused resources are often a major contributor to bloat, especially when including multiple density buckets or unused layouts. - shrinkResources: Enables removal of unused drawables, layouts, and strings; requires minifyEnabled to be true. - Image formats: Prefer WebP over PNG and JPEG for smaller file sizes with comparable quality; AVIF (Android 12 / API 31+) offers even better compression on supported devices. - Density splits: Generate separate APKs or App Bundles for specific screen densities (mdpi, hdpi, xhdpi) rather than bundling all. - Vector drawables: Use VectorDrawable or Compose vector graphics instead of multiple raster images for icons. - Language filtering: Specify supported locales via resConfigs to exclude unused translation strings from the package.
Architecture and Dependencies
Modular architecture allows users to download only the features they need initially, deferring the rest. - Dynamic Feature Modules: Use Play Feature Delivery to install features on demand rather than at initial install. - Dependency management: Audit libraries using the APK Analyzer; avoid pulling in heavy transitive dependencies for simple tasks. - Jetpack Compose: While Compose adds a runtime footprint, it often reduces XML layout inflation overhead and eliminates view binding boilerplate. - Version catalogs: Manage dependencies in libs.versions.toml to prevent duplicate library versions across modules. - Native libraries: Target only device-relevant ABIs (arm64-v8a, armeabi-v7a) via abiFilters in the ndk block; bundling all four ABIs (adding x86 and x86_64) roughly quadruples native library size.
Best Practices and Pitfalls
Always verify the impact of shrinking changes using the APK Analyzer in Android Studio to inspect dex and resource composition. - Testing: Thoroughly test release builds; aggressive shrinking can break reflection-based libraries like Gson or Retrofit if keep rules are missing. - Debuggable flag: Ensure release builds are not debuggable (isDebuggable = false, the default); debuggable release builds carry extra metadata and are a security risk. - Legacy APIs: Avoid deprecated AsyncTask (deprecated in API 30, replaced by coroutines and WorkManager) and legacy support libraries; use AndroidX instead. - App Bundle: Upload an Android App Bundle (.aab) instead of APK to let Google Play generate optimized splits for each user.
60
Can you explain the purpose of a PendingIntent and describe common scenarios where one is required in Android development?
KnownRevise
A PendingIntent is a token object that grants another application or system process permission to perform a predefined action on your app’s behalf at a later time. It wraps an ordinary Intent together with your app’s identity, allowing the recipient to execute that intent even if your app process is no longer running.
Read full answerHide full answer
Applies toAndroid 16 (API 36), Kotlin 2.x
How It Works
When you create a PendingIntent, the Android system stores the wrapped Intent and gives you a handle. You pass this handle to a different component, such as the notification manager or alarm scheduler. When the triggering event occurs, that external component hands the token back to the system, which validates your app’s signature and launches the wrapped intent as if your app had called it directly.
Factory methods: You obtain an instance using PendingIntent.getActivity, getBroadcast, getService, or getForegroundService, depending on the target component.
Identity: The system uses your app’s UID and signature, so the other process cannot impersonate your app beyond the specific action you authorized.
Deferred execution: The action happens later, potentially after your process has been killed to reclaim memory.
Common Use Cases
PendingIntent is the standard bridge for cross-process or delayed interactions where your code is not currently executing.
Notifications: Tapping a notification or one of its action buttons launches an Activity or starts a Service.
AlarmManager: Scheduling exact alarms that deliver a Broadcast or start work at a specific wall-clock time.
App Widgets: Home screen widget buttons send clicks back to a BroadcastReceiver in your app.
Autofill (and deprecated Slices): System UI components need permission to trigger app logic remotely; the Slices API has been deprecated by Google in favor of app-specific and generative UI.
Mutability Requirements
Starting with Android 12 (API 31), you must explicitly declare whether the PendingIntent can be modified by the recipient. This is enforced at runtime and throws a SecurityException if omitted.
FLAG_IMMUTABLE: The wrapped intent cannot be changed. Use this in almost all cases because it is more secure.
FLAG_MUTABLE: Allows the recipient to rewrite the intent. Only use this if the receiving component legitimately needs to modify the intent, such as some inline reply scenarios.
FLAG_UPDATE_CURRENT: If a PendingIntent with the same request code already exists, replace its stored intent with the new one instead of creating a duplicate.
Best Practices and Pitfalls
Always declare mutability: Omitting FLAG_IMMUTABLE or FLAG_MUTABLE crashes on API 31 and above.
Use unique request codes: The system matches PendingIntent objects by request code, intent action, data, type, and categories. Reusing a request code unintentionally causes one intent to overwrite another.
Avoid sensitive data: Do not place passwords or tokens in the wrapped intent’s extras, because the receiving process can read them.
Cancel when finished: Call pendingIntent.cancel() when the action is no longer valid, such as when dismissing a notification, to prevent stale intents from launching later.
Prefer WorkManager for deferrable work: For background tasks that do not need exact timing or system-level triggers, use WorkManager instead of AlarmManager with a PendingIntent.
61
How would you build a Navigation Drawer interface in a modern Android app using Jetpack Compose?
KnownRevise
Implementing a Navigation Drawer in modern Android relies on Jetpack Compose Material 3 drawer components, which manage drawer state and swipe gestures without XML layouts or the legacy DrawerLayout. The drawer integrates with Navigation Compose and follows unidirectional data flow, with state held in composition and navigation triggered from item clicks.
The drawer APIs live in androidx.compose.material3 and belong to the newer drawer API set, annotated @ExperimentalMaterial3Api (the stable, older equivalent is ModalNavigationDrawer): - ModalDrawer: The composable that draws the drawer content as an overlay above the main content, taking a drawerContent slot and a content slot. - DrawerState: Holds the open/closed state and drives the animation; created with rememberDrawerState(initialValue = DrawerValue.Closed). - ModalDrawerSheet: The styled container for the drawer’s content, typically a Column of list items. - DrawerDefaults: Standard dimensions and colors, such as drawerContainerColor.
Unlike androidx.drawerlayout.widget.DrawerLayout (used with XML Views), the Compose implementation handles edge-swipe detection internally based on the DrawerState, so no ActionBarDrawerToggle is needed.
Integration with Navigation
A drawer usually drives app navigation, so you pass a NavController into the drawer content. On item selection, call navController.navigate(route) and close the drawer with drawerState.close() (a suspend function, launched from a rememberCoroutineScope). To highlight the active item, compare navController.currentDestination?.route with each item’s route so the UI reflects the user’s location in the nav graph.
Dependency Configuration
Dependencies are declared in the version catalog (gradle/libs.versions.toml) and referenced from build.gradle.kts using the Kotlin DSL.
[versions]
compose-material = "1.3.1"
compose-navigation = "2.8.4"
compose-icons = "1.7.5"
[libraries]
androidx-compose-material3 = { group = "androidx.compose.material3", name = "material3", version.ref = "compose-material" }
androidx-compose-icons-extended = { group = "androidx.compose.material", name = "material-icons-extended", version.ref = "compose-icons" }
androidx-compose-navigation = { group = "androidx.navigation", name = "navigation-compose", version.ref = "compose-navigation" }
This snippet shows a ModalDrawer wrapping a Scaffold. The opt-in annotation is required because the drawer API and TopAppBar are @ExperimentalMaterial3Api.
State hoisting: Keep DrawerState in composition (it owns animation state); hoist only the open/close intent, for example a MutableStateFlow<Boolean> in a ViewModel, and collect it to drive drawerState.open()/close().
Accessibility: Give every icon a content description and ensure items are reachable by keyboard or D-pad for accessibility services and TV.
Performance: Keep drawer content lightweight; avoid heavy image loading inside ModalDrawerSheet so the open animation stays smooth.
API levels: Compose Material 3 requires minSdk 21+, while predictive back gesture support needs the API 33 opt-in and is fully supported from API 34.
Avoid legacy APIs: Use DrawerLayout and ActionBarDrawerToggle only when maintaining legacy Views code; they do not interoperate with a Compose UI tree without a ComposeView wrapper.
Drawer types: Use ModalDrawer for an overlay drawer and DismissibleDrawer when the main content should shift; pick per the Material guidance for your form factor.
62
What does the android:launchMode attribute do, and why would you set it on an activity?
KnownRevise
The android:launchMode attribute in the Android manifest tells the system how a new instance of an activity should be created — or whether an existing instance should be reused — when an Intent targets that activity. It controls the activity’s task affinity and back-stack behavior, so it matters for navigation, deep links, and how many instances of the same screen can exist at once.
Read full answerHide full answer
Applies toAndroid 16 (API 36); the attribute has existed since API 1 and behaves as described on all supported versions.
The Four Launch Modes
standard (the default): a new instance is created in the task that raised the intent. An activity can be instantiated many times, and each instance can belong to a different task.
singleTop: if an instance already sits at the top of the target task, the system routes the intent to it by calling onNewIntent() instead of creating a new instance. If it is not on top, a new instance is created. Common for search screens, notification handlers, and deep-link targets, where duplicates stacked on top of each other would confuse the user.
singleTask: the activity is the root of its task (identified by taskAffinity). If a task with that affinity already exists, the intent is delivered to it via onNewIntent(); otherwise a new task is created. Activities above it in that task are destroyed when it is reused.
singleInstance: like singleTask, but the task can contain only this one activity. Any activity started from it is launched in a different task. This is rarely appropriate and is mainly useful for a global, always-single window such as a custom in-app dialer or overlay.
How the System Chooses a Task
Launch mode is combined with intent flags and taskAffinity:
Intent.FLAG_ACTIVITY_NEW_TASK in the starting intent behaves much like singleTask for that launch.
taskAffinity (defaulting to the package name) decides which task a singleTask or singleInstance activity belongs to. Changing taskAffinity without also setting a launch mode has no effect on standard activities.
Flags such as FLAG_ACTIVITY_CLEAR_TOP and FLAG_ACTIVITY_SINGLE_TOP can override or supplement the manifest value at runtime.
Comparison
Launch mode
New instance when on top?
New instance when not on top?
Can share task with other activities?
Typical use
standard
Yes
Yes
Yes
Default screens
singleTop
No — onNewIntent()
Yes
Yes
Search, notifications, deep links
singleTask
No — onNewIntent()
No, reuses its own task
Yes, as root
Entry points, external deep links
singleInstance
No — onNewIntent()
No, reuses its own task
No
Rare; globally single window
Best Practices and Common Pitfalls
Prefer singleTop over singleTask for in-app navigation; singleTask reuses a task based on affinity and can destroy activities the user still expects to return to.
Handle onNewIntent() in non-standard activities, otherwise a relaunch silently ignores fresh data. Inside onNewIntent(), call setIntent(intent) so that later calls to getIntent() return the latest intent, then update your UI or ViewModel with the new data.
Avoid singleInstance unless you have a specific reason; it produces confusing back-stack behavior because the activity can never stack with others.
Do not use launch mode as a substitute for proper up navigation (parentActivityName and ActionBar.setDisplayHomeAsUpEnabled()); they solve different problems.
Test back-stack behavior after a force-stop and from cold start via adb, because task restoration can differ from normal configuration changes.
63
Explain how the Android notification system works and which components are involved in creating and displaying notifications to the user.
KnownRevise
Android notifications are messages your app posts to the system status bar and notification shade so users can see them even when the app is not in the foreground. The central component is the NotificationManager system service, which receives Notification objects built with NotificationCompat.Builder and displays them on behalf of your app.
NotificationManager: The system service that publishes, updates and cancels notifications. Obtain it with context.getSystemService(NotificationManager::class.java).
NotificationCompat.Builder: The recommended builder (from AndroidX) for constructing notifications in a backward-compatible way. Every notification needs at least a channel ID and a small icon.
Notification: The final immutable object handed to NotificationManager.notify(); you rarely build it directly, preferring the builder.
NotificationChannel: Required on API 26 (Android 8.0) and above. A channel groups notifications and defines their behaviour: importance level, sound, vibration and whether they appear on the lock screen. Users can override these settings per channel in system settings.
PendingIntent: A token that grants another process (the system) permission to perform an action on your app’s behalf, such as opening an activity when the user taps the notification.
Notification Channels
On API 26+ you must create a channel before posting any notification to it; posting to a non-existent channel silently fails. Channels are created once, typically at app startup, because users manage channel settings themselves and re-creating a channel does not change settings the user has already modified. Importance levels determine how intrusive a notification is:
Importance
Behaviour on API 26+
IMPORTANCE_NONE
Silent, never shown
IMPORTANCE_MIN
Silent, shown in the shade only
IMPORTANCE_LOW
No sound or heads-up, shown in the shade
IMPORTANCE_DEFAULT
Makes sound, shown in the shade
IMPORTANCE_HIGH
Makes sound, appears as a heads-up notification
On API 25 and below, channels are ignored and priority is set on the builder instead.
Permissions and API Levels
Behaviour differs by Android version: - API 33 (Android 13) and above: You must hold the POST_NOTIFICATIONS runtime permission or the notification is not shown. Request it with the Activity Result API (registerForActivityResult) instead of the deprecated onRequestPermissionsResult callback. - API 26 (Android 8.0) and above: Notification channels are mandatory. - API 31 (Android 12) and above: PendingIntent mutability must be declared explicitly; use FLAG_IMMUTABLE unless the pending intent needs to be modified later.
Scheduling and Background Work
Do not post notifications directly from long-running background threads. For work that must run even when the app is not active, use WorkManager, which survives process death and respects Doze mode. IntentService and AsyncTask are deprecated (since API 30) and should be replaced with coroutines or WorkManager.
Best Practices and Common Pitfalls
Always set a small icon: A notification without setSmallIcon() is dropped by the system.
Check the permission on API 33+ before posting, otherwise notify() succeeds but nothing appears.
Reuse notification IDs to update an existing notification instead of stacking duplicates; call cancel() when it is no longer relevant.
Use meaningful channel names because users see them in system settings and cannot change them programmatically after creation.
Keep notifications actionable and concise: provide actions via addAction() and deep-link to the relevant screen through the PendingIntent.
Respect user settings: do not bypass channel importance or spam; excessive notifications lead users to disable them entirely.
64
What mechanisms does Android provide for transferring data between activities, and how does the approach change when navigating between Jetpack Compose destinations?
KnownRevise
Android transfers data between activities primarily through Intent extras carried in a Bundle, whereas navigation between Jetpack Compose destinations shifts toward route arguments in the navigation graph and shared ViewModel state; results from a launched screen should be collected through the Activity Result API rather than the deprecated startActivityForResult flow.
An Activity starts another with Intent.putExtra(...); the target reads values with intent.getStringExtra, getIntExtra, and similar typed methods.
Custom objects should implement Parcelable, preferably with @Parcelize; Serializable still works but uses reflection and is slower.
Data passes through the Binder transaction buffer, so keep payloads small and avoid large bitmaps or huge collections.
Intents are one-shot: data is delivered when the target is created and is not automatically retained across configuration changes.
Navigation Arguments in Compose Destinations
In Navigation Compose, a destination declares a route such as "details/{itemId}" and typed arguments through navArgument.
Arguments are parsed from the route and exposed via NavBackStackEntry.arguments, with built-in types like NavType.StringType, IntType, LongType, and BooleanType.
This keeps navigation state in the back stack and survives configuration changes, but only simple values should be passed; complex objects belong in a ViewModel.
Deep links can reuse the same argument pattern, so argument names and types must remain consistent.
Shared ViewModel for Complex State
When destinations need the same large dataset, transient UI state, or repository access, scope a ViewModel to the Activity or navigation graph and expose it as StateFlow.
Both screens observe the same stream, avoiding serialization and Intent size limits while surviving configuration changes.
With Hilt, hiltViewModel() injects the same instance for destinations within the matching scope.
Returning Data with the Activity Result API
startActivityForResult and onActivityResult are deprecated; replace them with registerForActivityResult.
Register an ActivityResultLauncher using a contract such as ActivityResultContracts.StartActivityForResult, then launch it with an Intent.
The callback receives an ActivityResult containing resultCode and data, is lifecycle-aware, and works from both Activities and Fragments without overriding lifecycle methods.
Best Practices and Common Pitfalls
Pass identifiers, not objects: send an ID through an Intent or route argument and let the destination load the full record.
Avoid sensitive data: never place passwords, tokens, or personal identifiers in Intent extras; use secure storage instead.
Prefer type safety: use typed extras and NavType constants; validate arguments before using them to prevent runtime crashes.
Respect lifecycle: read Intent data in onCreate for Activities; in Compose, collect ViewModel state during composition rather than repeatedly reading arguments.
Keep dependencies current: use stable versions of androidx.activity:activity-ktx, androidx.navigation:navigation-compose, and Hilt.
65
Can you explain the purpose and behavior of the ViewModel and LiveData classes within the Android Architecture Components library?
KnownRevise
ViewModel manages UI-related data and survives configuration changes, while LiveData is a lifecycle-aware observable data holder that ensures UI updates only occur when the component is active. Together they separate business logic from UI controllers and prevent common lifecycle-related crashes in Android applications.
The ViewModel class stores and manages UI-related data in a lifecycle-conscious way. It is scoped to the activity or fragment lifecycle rather than the view hierarchy, meaning it survives configuration changes such as screen rotations. This persistence prevents data loss and avoids redundant network or database queries when the UI is recreated.
LiveData Mechanism
LiveData is an observable data holder class that respects the lifecycle of Android app components. Unlike standard observables, it only updates active observers whose lifecycle state is STARTED or RESUMED. This prevents crashes associated with updating views after an activity has been destroyed and reduces unnecessary CPU usage when the app is in the background.
Lifecycle Awareness
Lifecycle awareness is the key differentiator for LiveData. When registering an observer via observe(), you pass a LifecycleOwner. If the owner’s state drops below STARTED, deliveries pause automatically. When it returns to active, the latest value is delivered immediately, eliminating the need for manual lifecycle checks in activities or fragments.
Modern Alternatives
While LiveData remains fully supported, modern Kotlin development often prefers StateFlow or SharedFlow within the ViewModel. These Kotlin coroutine-based streams offer better testability and seamless integration with Jetpack Compose without requiring Android context dependencies. LiveData is still ideal when interoperability with Java or specific lifecycle safety without coroutines is required.
Code Example
@HiltViewModel
class MyViewModel @Inject constructor(private val repository: MyRepository) : ViewModel() {
private val _data = MutableLiveData<String>()
val data: LiveData<String> = _data
fun loadData() {
viewModelScope.launch {
// Offload blocking work to IO dispatcher
val result = withContext(Dispatchers.IO) { repository.fetchData() }
_data.value = result
}
}
}
@Composable
fun MyScreen(viewModel: MyViewModel = hiltViewModel()) {
val data by viewModel.data.observeAsState(initial = "")
Text(text = data)
}
Best Practices
Expose Immutable LiveData: Always expose LiveData not MutableLiveData to prevent external modification of the state.
Avoid Context Leaks: Never pass Context into a ViewModel; use repository patterns for data access to keep logic testable.
Prefer StateFlow for Compose: Use StateFlow when building purely Compose UIs to reduce Android framework dependencies and simplify testing.
Handle Errors: Wrap data states in a sealed UI state class or use event wrappers to handle one-shot events like navigation or snackbar messages.
66
What does a well-structured Android application architecture look like, and which principles and components would you use to build it?
KnownRevise
A good Android app architecture is built on separation of concerns, unidirectional data flow, and a single source of truth, implemented with Jetpack Compose for UI, MVVM using ViewModel and StateFlow, and clearly separated data and domain layers, with Hilt providing dependencies and coroutines/Flow handling asynchronous work off the main thread.
Separation of concerns: UI code draws and reacts to state; it never fetches data or holds business rules. Each class has one reason to change.
Unidirectional data flow (UDF): state flows down from the ViewModel to Compose, and events flow up. Compose is a pure function of state, which makes UI predictable and testable.
Single source of truth: the repository owns app data. The UI, ViewModel, and other components observe that data rather than each keeping their own copy.
Offline-first and resilience: the app reads and writes to a local Room database, so it works without a network and shows stale data while syncing, rather than showing an error or a blank screen.
Testability: because UI logic lives in a ViewModel and data logic in a repository, you can unit test both without an emulator or a real device.
Recommended Layered Structure
UI layer (Jetpack Compose): stateless or state-hoisting composables that observe StateFlow/Flow via collectAsStateWithLifecycle() and emit intents or callbacks to the ViewModel. Use Lifecycle.STARTED awareness so collection stops when the app goes to the background.
Presentation layer (ViewModel): holds UI state as a single immutable data class exposed through a StateFlow, and exposes suspend functions or a private channel of events. It calls the repository and maps results into UI state, including loading and error states.
Domain layer (optional, for larger apps): use cases (Interactor classes) that encapsulate a single piece of business logic and combine multiple repositories. This keeps ViewModels small and makes rules reusable and testable.
Data layer (repository + Room + Retrofit): the repository defines the public API for data access and decides whether to serve cached data, fetch from the network with OkHttp/Retrofit, or schedule background syncs with WorkManager. Data models are mapped to domain models so the UI never sees network or database annotations.
Key Architectural Components
Coroutines and Flow: use Dispatchers.IO for database and network calls; never block the main thread. Flow streams data, StateFlow holds UI state, SharedFlow handles one-off events such as navigation or snackbar messages.
Hilt: inject repositories, use cases, RoomDatabase, Retrofit services, and ViewModels through constructor injection. This removes manual service locators and makes classes trivially testable with fakes.
WorkManager: the only reliable way to run deferred background work that must survive process death, such as syncing data or uploading logs. For API 34+, declare a foreground service type when the work needs to run in the foreground.
Navigation Compose: type-safe, state-driven navigation with a single NavHost, so back stack and deep links are predictable.
Advantages and Limitations
Aspect
Benefit
Trade-off
MVVM + UDF
Predictable, testable, lifecycle-safe UI
More boilerplate for small screens
Repository pattern
Single source of truth, swappable sources
Extra mapping and abstraction layers
Domain/use-case layer
Reusable, isolated business rules
Can be overkill for CRUD apps
Offline-first Room
Works offline, fast UI
Sync conflicts and staleness to handle
Best Practices and Common Pitfalls
Hoist state into the ViewModel and keep composables stateless; pass callbacks down instead of whole view models.
Never do network or disk work on the main thread — use Dispatchers.IO inside the repository and viewModelScope in the ViewModel.
Expose a single StateFlow per screen rather than many LiveData or Flow fields, so the UI always renders a consistent snapshot.
Map data models to domain models at the repository boundary so Retrofit annotations and Room entities never leak into the UI layer.
Avoid passing Context or framework classes into repositories; inject only what a class needs, and prefer fakes over mocks in tests.
67
Can you explain the Model-View-Presenter architecture pattern and how its responsibilities are divided?
KnownRevise
The Model-View-Presenter (MVP) pattern separates an application into three roles: the Model supplies data and business rules, the View is a passive UI that only renders state and forwards user actions, and the Presenter mediates between them by applying presentation logic and telling the View what to display. Its main goal is to keep UI classes free of business logic so that behavior can be unit tested independently of Android UI components.
Read full answerHide full answer
Applies toAndroid development with Kotlin 2.x and Jetpack Compose on Android 16 (API 36); the MVP pattern itself is framework-agnostic and applies to all Android versions.
Core Roles
Model: the data and domain layer. In an Android app this typically includes repositories, Room databases, Retrofit/OkHttp network calls, and business rules. The Model should not know how the UI is rendered.
View: a thin, passive interface to the UI. It exposes methods such as showLoading(), showError() or render(items), but contains no decision logic. An Activity or Fragment historically implemented this interface; with Compose, the contract is usually implemented by the Activity/Fragment hosting the composable rather than by the composable itself.
Presenter: the bridge that reacts to View events, calls the Model, transforms results into UI state, and updates the View through the interface. The Presenter should depend on the View abstraction, not on concrete Android classes such as Activity, Fragment or Composable.
How It Works
The user triggers an action, for example submitting a form or pulling to refresh.
The View forwards that event to the Presenter.
The Presenter calls the Model, running network or database work on Dispatchers.IO or scheduling it with WorkManager—never on the main thread.
The Model returns data or an error.
The Presenter maps the result into display instructions and calls methods on the View interface.
The View updates the screen without knowing why or how the data was produced.
A common variant is Passive View, where the View is fully dumb and the Presenter sets every piece of state. Another is Supervising Controller, where the View performs simple data binding itself and the Presenter handles only complex logic. Passive View is the form usually meant in Android interviews.
MVP Compared with MVVM
Modern Android guidance favors MVVM with Jetpack ViewModel, StateFlow or Flow, coroutines, Hilt, Room and Compose, especially because Compose subscribes to observable state naturally. MVP remains useful for maintaining legacy codebases or enforcing a strict view contract.
Aspect
MVP
MVVM
UI logic owner
Presenter
ViewModel
View dependency
Presenter calls an explicit View interface
UI observes state such as StateFlow
Configuration changes
Presenter usually must be reattached manually
ViewModel survives configuration changes
Compose fit
Needs a contract implemented outside the composable
Composable collects state directly
Testing
Presenter can be tested with a fake View
ViewModel can be tested with Turbine or runTest
Advantages and Limitations
Testability: presentation logic can be verified with a fake View and fake repository, without an emulator.
Separation of concerns: Activities and Fragments stay small because they only implement rendering methods.
Clear contracts: the View interface makes the expected UI behavior explicit.
Boilerplate: every screen needs a contract interface plus a Presenter implementation.
Lifecycle risk: presenters can leak or update a destroyed View if they are not detached and if long-running coroutines are not cancelled.
Less Compose-friendly: Compose is state-driven, so MVP’s imperative “call the view” style often adds friction compared with collecting StateFlow.
Best Practices
Define the contract as a small, stable interface; do not expose Android UI types from it.
Keep the Presenter independent of android.view and androidx.compose classes.
Use Hilt to inject repositories and presenters rather than creating them inside the View.
Run network, database or file work on Dispatchers.IO, or use WorkManager for deferrable background work.
Cancel presenter coroutines when the View is destroyed, and prefer MVVM with StateFlow for new Compose UIs.
68
How do you define the MVVM architectural pattern and explain its implementation within the Android ecosystem?
KnownRevise
Model-View-ViewModel (MVVM) is an architectural pattern that separates an Android app into three distinct layers to isolate UI logic from business logic and data management. In modern Android development, the View observes state exposed by the ViewModel, while the ViewModel interacts with the Model layer to fetch or persist data, ensuring the UI remains a passive renderer of state.
Read full answerHide full answer
Applies toAndroid 16 (API 36) with Kotlin 2.x, Jetpack Compose, and AndroidX Lifecycle
The Three Components
Model: Represents the data layer, typically implemented as a repository that abstracts data sources such as Room databases, Retrofit networks, or the DataStore preferences API. It handles caching, offline support, and data validation without knowing about the UI.
View: In current Android, this is built with Jetpack Compose @Composable functions and ComponentActivity. It renders UI state and forwards user interactions to the ViewModel but contains no business logic. Legacy XML Views are still supported but considered legacy for new development.
ViewModel: An instance of androidx.lifecycle.ViewModel that survives configuration changes like screen rotations. It holds UI state in Kotlin StateFlow streams and executes business logic using Kotlin coroutines within viewModelScope.
Unidirectional Data Flow
MVVM in Android follows a unidirectional data flow where state moves down and events move up. 1. The ViewModel exposes immutable UI state via StateFlow or SharedFlow. 2. The Compose View collects this flow using collectAsStateWithLifecycle(), which respects the lifecycle state to prevent updates when the app is in the background. 3. User interactions, such as button clicks, are passed to the ViewModel as events through methods or channels. 4. The ViewModel processes the event, updates the state, and the View recomposes automatically.
Advantages and Limitations
Aspect
Details
Testability
ViewModels can be unit tested without an Android instrumented test runner since they hold no View references.
Separation
UI changes rarely affect business logic, allowing parallel work between designers and developers.
Lifecycle Safety
Lifecycle-aware components prevent memory leaks and crashes from background updates.
Boilerplate
Requires more files and interfaces than a simple Activity-driven approach, which can overwhelm small apps.
Learning Curve
Teams must understand coroutines, Flow, and Compose state hoisting to implement it correctly.
Implementation Best Practices
Dependency Injection: Use Hilt to inject repositories into ViewModels via @HiltViewModel, avoiding manual instantiation.
UI State Classes: Wrap state in immutable data classes rather than exposing multiple flows, ensuring consistent UI snapshots.
Event Handling: Use one-shot event channels or SharedFlow for navigation and snackbar events so they are not re-emitted on configuration changes.
Background Work: Offload long-running tasks to WorkManager for guaranteed execution rather than keeping them in the ViewModel scope.
Legacy Awareness: Avoid LiveData for new Kotlin-first features unless interoperating with XML Views, as StateFlow is the preferred stream API.
69
What is the Repository pattern within Android architecture, and why is it regarded as a vital component for scalable app development?
KnownRevise
The Repository pattern acts as a mediator between data sources—such as local Room databases and remote Retrofit APIs—and the business logic layer, providing a unified interface for data access while hiding the underlying implementation details. By centralizing data operations, it ensures that ViewModels and UI components remain decoupled from how data is fetched or persisted.
The repository class manages data operations and offers a clean API to the rest of the app. It decides whether to fetch data from the network, retrieve it from the local cache, or combine both. Typically, a repository exposes data via Kotlin Flow for observable streams or suspend functions for one-shot operations, allowing the ViewModel to observe changes without polling.
Why It Matters
Decoupling: The ViewModel does not know if data comes from SQLite, a REST API, or a mock source, facilitating easier refactoring.
Testability: Repositories can be mocked or faked during unit testing, isolating business logic from infrastructure concerns.
Single Source of Truth: It prevents data duplication and inconsistency by managing caching logic centrally, ensuring the UI always reflects the most accurate state.
Concurrency Safety: It encapsulates threading logic, ensuring database and network calls never block the main thread.
Implementation Strategy
A standard implementation defines an interface for the repository, which is then implemented by a concrete class injected via Hilt. The concrete class receives dependencies like a Data Access Object (DAO) and a Retrofit service. When fetching data, the implementation follows a caching strategy chosen per use case, merging multiple streams with operators like combine or flatMapLatest.
Strategy
Behaviour
When to Use
Cache-first
Emit local data immediately, then refresh from network
Offline-capable apps where fast UI rendering matters
Network-first
Fetch from API, fall back to local cache on failure
Data that must be fresh, such as financial or messaging content
Network-only
Always request remote data, ignore cache
Ephemeral data that should never be stale
Best Practices
Expose Flows, Not LiveData: Prefer Kotlin Flow in repositories for better interoperability with coroutines and testability; convert to StateFlow in the ViewModel if needed.
Handle Errors Gracefully: Catch exceptions within the repository or use sealed classes to represent loading, success, and error states passed to the UI.
Avoid Context Leaks: Ensure the repository does not hold an Android Context; it should remain pure Kotlin logic wherever possible.
Use Dependency Injection: Inject repository implementations via Hilt to manage lifecycle and dependencies cleanly without manual instantiation.
Respect Threading: Although Room and Retrofit suspend functions handle dispatcher switching internally, explicitly using Dispatchers.IO for heavy computations ensures safety.
70
What was the Android Support Library, and why was it significant for ensuring backward compatibility in Android development?
KnownRevise
The Android Support Library was a collection of official libraries from Google that backported newer Android framework features and UI components to older platform versions. Although it was the cornerstone of backward compatibility for many years, it is now deprecated and has been fully replaced by AndroidX, which is the required foundation for all modern Android development.
Read full answerHide full answer
Applies toAndroid Support Library (deprecated, legacy) and AndroidX (current, Jetpack)
Historical Context and Purpose
The Support Library was introduced in 2011 with Android 1.6 (API 4) as the Compatibility Package and later grew into a family of versioned libraries (v4, v7, v13, v17, etc.) under the android.support.* namespace. It shipped through the SDK Manager and later Google Maven, letting developers use platform capabilities years before those APIs reached the majority of devices. Representative components included: - AppCompatActivity (appcompat-v7): brought the action bar, theming and menu inflation to API 7+. - FragmentManager (support-v4): gave a consistent fragment lifecycle and back stack across API levels. - RecyclerView (recyclerview-v7): supplied a flexible, performant list and grid view independent of the OS version. - Design Support Library: enabled Material Design components such as floating action bars and collapsing toolbars on older devices.
Why It Was Important
Android’s fragmentation meant that a large share of users ran outdated OS versions. Without the Support Library, apps would either have to target only the newest devices or embed long chains of version checks and hand-rolled fallbacks. The library solved this by: - Backward-compatible APIs: exposing modern classes (fragments, loaders, notifications, permissions helpers) that worked on old platforms. - Consistent UI/UX: delivering Material Design and standard widgets with the same look and behavior from API 14 upward. - Reduced boilerplate: hiding version-specific workarounds inside the library so app code stayed clean. - Faster feature adoption: letting developers adopt new patterns without forcing users to upgrade their OS.
The Shift to AndroidX
In 2018 Google announced AndroidX as the successor, and the Support Library stopped receiving updates. AndroidX is more than a namespace change: - Namespace: artifacts moved from android.support.* to androidx.*. - Modular structure: each library is versioned and published independently, so fixes are not tied to platform releases. - Modern requirement: Jetpack Compose, Room, WorkManager, Hilt and other Jetpack components are built on AndroidX and will not work with the old Support Library. - Tooling support: Android Studio provides an automatic migration (Refactor > Migrate to AndroidX) that rewrites imports and dependency declarations.
Best Practices for Legacy Code
If you still maintain a project that references the old Support Library: 1. Migrate to AndroidX: run the built-in migration tool and verify that all android.support.* imports are gone. 2. Never mix namespaces: having both android.support.* and androidx.* on the classpath causes ClassNotFoundException or InflateException crashes at runtime. 3. Audit transitive dependencies: third-party libraries that still pull in old Support Library artifacts must be upgraded or excluded, otherwise they reintroduce the deprecated namespace. 4. Target current APIs: after migration, set compileSdk and targetSdk to the latest stable release and use AndroidX equivalents for any remaining legacy calls.
Continuing to use the Support Library is risky: it receives no security patches, is incompatible with modern tooling, and can block updates to newer Android versions.
71
Can you explain the purpose of the Android Data Binding Library and describe how it reduces complexity in UI development?
KnownRevise
The Android Data Binding Library is an AndroidX library that is part of Android Jetpack and lets you bind UI components in XML layouts directly to data sources in your app using a declarative format, cutting the boilerplate needed to connect presentation logic and views. It significantly simplified XML-based development, but new projects should use Jetpack Compose, which handles state-driven UI natively without a separate binding library.
Read full answerHide full answer
Applies toAndroidX Data Binding Library (legacy XML Views, API 14+) and Jetpack Compose (Android 16, API 36)
Core Concept
Declarative Layouts: Layout files use a <layout> root tag with a <data> section that declares variables, which can then be referenced directly inside attribute expressions instead of being wired up in code.
Generated Binding Classes: At compile time the build system generates a binding class such as ActivityMainBinding, giving direct, type-safe references to every view that has an ID.
Observable Data: Objects can implement Observable, expose ObservableField properties, or use LiveData/StateFlow; when the data changes, bound views update automatically.
Two-Way Binding: The @={} expression syntax keeps the UI and the data model synchronised in both directions, which is useful for forms and editable fields.
How It Reduces Complexity
Eliminates findViewById: No manual view lookup, casting, or null checks scattered through activities and fragments.
Compile-Time Safety: Binding classes are generated from the layout, so mismatches between code and XML surface as compile errors instead of runtime crashes.
Less Glue Code: Text, images, visibility and click handling can be declared in XML or delegated to a ViewModel, shrinking activity and fragment classes.
Automatic UI Updates: Observable data pushes changes to the bound views, removing the need to manually refresh each widget after a state change.
Clearer Separation of Concerns: Layouts declare what to display while ViewModels hold the state, making screens easier to test, reason about and maintain.
Data Binding vs. Jetpack Compose
Feature
Data Binding (XML)
Jetpack Compose
UI Definition
XML files
Kotlin functions
State Handling
ObservableField, LiveData
State, StateFlow
Boilerplate
Reduced vs. Views
Minimal
Status
Legacy/Maintenance
Recommended
Learning Curve
Moderate
Steeper initially
Common Pitfalls
Overly Complex Expressions: Business logic in XML is hard to read and test; keep expressions simple and move logic into the ViewModel.
Slower Builds: Data Binding adds a code-generation step that can increase compilation time on large projects.
Threading: UI-bound data must be updated on the main thread; LiveData posts automatically, but plain observable fields do not.
Debugging Noise: Generated binding classes can make stack traces harder to follow; know where the generated sources live in your build.
Best Practices and Migration
New Projects: Use Jetpack Compose exclusively; do not introduce Data Binding into new Compose modules.
Existing Apps: Migrate screens to Compose gradually rather than refactoring Data Binding logic extensively.
Security: Never bind sensitive data into logs or debug overlays enabled through build configuration.
When to Keep It: Data Binding remains reasonable for maintaining existing XML screens where a full Compose migration is not yet justified.
72
Can you explain what Dagger 2 is used for in an Android app and why a team would choose it over wiring dependencies by hand?
KnownRevise
Dagger 2 is a fully static, compile-time dependency injection (DI) framework for Java and Kotlin, widely used on Android to provide objects (dependencies) to the classes that need them without those classes constructing the objects themselves. Instead of using reflection at runtime like older DI tools, Dagger generates plain, type-safe code during the build, so DI errors surface as compiler errors rather than crashes in production. On modern Android, most new apps use Hilt, Google’s officially recommended library built on top of Dagger, which provides the same benefits with far less boilerplate.
Read full answerHide full answer
Applies toDagger 2.55+ and Hilt 2.55+ on Android 16 (API 36) with Kotlin 2.x; the concepts apply to all Dagger 2 versions.
The Problem It Solves
An Android app is a graph of collaborating objects: a ViewModel needs a repository, the repository needs a Retrofit API interface and a Room DAO, and those need an OkHttpClient and a database. Without DI, an activity ends up constructing the whole chain, which means the activity knows construction details it shouldn’t care about, and the same objects get rebuilt or awkwardly passed around. Dagger lets each class simply declare what it needs (through a constructor or a field) and lets generated code supply the instances, so classes depend only on abstractions.
How It Works
Dagger builds and validates the object graph at compile time using a small set of annotations:
@Inject: marks a constructor, field or (less commonly) method as something Dagger should supply or populate.
@Module: a class whose methods, annotated with @Provides (or @Binds for interface-to-implementation mappings), tell Dagger how to construct objects Dagger cannot construct itself, such as Retrofit, OkHttpClient or third-party classes.
@Component: an interface or abstract class that ties modules together and exposes the entry points of the graph. Dagger generates its implementation.
@Scope / @Singleton: controls how long an instance is kept and reused, for example one Retrofit instance per app lifetime versus a new one per activity.
@Qualifier: distinguishes multiple bindings of the same type, such as a “logged-in” User versus an “anonymous” one.
If a binding is missing, duplicated or a dependency has a cycle, the build fails with a precise error message listing the affected class.
Advantages
Compile-time safety: missing or circular dependencies are compiler errors, and the generated code is plain Java/Kotlin you can step through in a debugger.
Minimal runtime cost: no reflection, no slow startup graph construction, and ProGuard/R8 works without extra keep rules because the generated code is ordinary.
Boilerplate removal: Dagger generates the factory and provider classes that hand-written DI would require, which is significant in an app with dozens of services.
Testability: modules can be swapped for test modules, so instrumented or unit tests can inject fakes and in-memory repositories without touching production code.
Clear ownership of object lifetimes: scopes make it explicit whether an object lives for the app process, an activity or a shorter flow, which helps avoid leaks and accidental singletons.
Cross-cutting consistency: because everything flows through the graph, logging, error handling and configuration (base URLs, timeouts) are defined once.
Dagger Versus Hilt and Alternatives
Aspect
Dagger 2
Hilt
Manual DI
Setup effort
High; hand-written components and modules
Low; predefined components and entry points
Lowest at first, grows quickly
Compile-time validation
Yes
Yes (same compiler)
No
Android lifecycle integration
Manual
Built-in for activities, fragments, services, ViewModel and Compose
Google recommends Hilt for Android apps because it removes most component boilerplate while keeping Dagger’s generated code and compile-time guarantees. Using raw Dagger is still reasonable for libraries that must avoid Hilt’s dependency, or for apps with graphs Hilt’s predefined components cannot express.
Limitations and Best Practices
Learning curve: the generated-code model and error messages are unfamiliar at first; expect the first integration to take real time.
Build time: annotation processing adds to build time in large modules, so keep the graph modular and avoid unnecessary @Inject churn.
Prefer constructor injection over field injection; it makes classes testable without the container and keeps them immutable.
Bind interfaces with @Binds rather than @Provides when you already have an implementation Dagger can construct, because @Binds is cheaper for the compiler.
Scope deliberately: overusing @Singleton turns the graph into a hidden global state store; scope objects to the shortest lifetime that is correct.
Never do network or disk work in modules’ provider methods; providers should construct objects only, with heavy work deferred to coroutines on Dispatchers.IO or WorkManager.
73
Can you describe the collection of libraries and tools that make up Android Jetpack and what each category provides?
KnownRevise
Android Jetpack is a suite of Google-supported libraries, tools and architectural guidance that accelerate Android development by providing ready-made, backward-compatible building blocks for common app needs such as UI, navigation, data persistence, background work and dependency injection. The components are versioned and released independently of the OS, so you can adopt new features without waiting for platform updates while still targeting the current stable release (Android 16, API level 36).
Read full answerHide full answer
Applies toAndroid 16 (API 36) with Kotlin 2.x and Jetpack Compose as the default UI toolkit.
Purpose and Design Principles
Jetpack exists to reduce boilerplate and fragmentation across the Android ecosystem. Its libraries follow four principles that interviewers often look for:
Separation of concerns: UI, data and logic live in distinct, testable layers, which is why the recommended architecture pairs Compose with a ViewModel and a single source of truth.
Drive UI from data: state flows down through StateFlow or LiveData, and events bubble up, making the UI a rendering function of state.
Modularity: each library is a separate artifact, so you add only what you need via Gradle dependencies.
Backward compatibility and consistency: many libraries ship as “AndroidX” artifacts that work across a wide range of API levels, abstracting version-specific behaviour such as runtime permissions (API 23+) or scoped storage (API 29/30).
The Four Categories
Jetpack is conventionally divided into four groups; the boundaries are loose, but the grouping is a useful way to structure an answer.
Category
What it covers
Representative components
Foundation
Core capabilities the platform itself lacks or implements inconsistently
AppCompat, Core KTX, Multidex, Test (JUnit and Truth extensions), Benchmark
Architecture
Structuring an app so it is testable, scalable and maintainable
Compose (including Material 3 and Compose for Wear OS and TV), Fragment, Activity, Animation and transitions, Layout
Key Components in the Recommended Stack
Modern apps are usually built on a specific, well-defined set of these libraries:
Jetpack Compose: the declarative UI toolkit that replaces the XML view system for new apps. XML layouts and View binding remain supported for legacy code, but Compose is the default recommendation.
ViewModel and StateFlow: hold and expose UI state that survives configuration changes, with coroutines and Flow for asynchronous work on Dispatchers.IO so the main thread is never blocked.
Navigation Compose: type-safe, argument-based navigation between destinations, replacing manual Intent and fragment-transaction handling.
Room: the SQLite abstraction layer with compile-time query validation and coroutine/Flow support for reactive queries.
WorkManager: the standard API for deferrable, guaranteed background work, including constraints, retries and (on API 34+) mandatory foreground service types. It supersedes the deprecated AsyncTask and IntentService.
Hilt: the recommended dependency injection layer, built on Dagger with reduced boilerplate for Android classes.
DataStore: modern, coroutine-based key-value and typed settings storage that is the recommended replacement for SharedPreferences in most use cases; SharedPreferences remains supported but is no longer the preferred choice for new code.
Retrofit and OkHttp: the mainstream third-party networking stack (not part of Jetpack, but the recommended complement), typically combined with a Result-sealed or Resource wrapper for error handling.
Advantages and Limitations
Advantages: battle-tested, well-documented code; consistent APIs across API levels; first-class Kotlin and coroutine support; reduced boilerplate; strong tooling in Android Studio; large community familiarity.
Limitations: a large dependency surface and build-time cost; frequent major releases that require maintenance; occasional overlap between libraries (for example, SharedPreferences versus DataStore, or LiveData versus StateFlow); and version-coupling between Compose, the Compose compiler and the Kotlin compiler, which must be aligned carefully.
Best Practices
Adopt the architecture guidance rather than cherry-picking libraries in isolation: Compose UI → ViewModel with StateFlow → repository → Room/Retrofit, with Hilt wiring the graph.
Prefer StateFlow over LiveData in new Compose code, since LiveData was designed for the view system and is not coroutine-native.
Use the Activity Result APIs instead of the deprecated startActivityForResult and onRequestPermissionsResult.
Keep every library on a recent stable release through the version catalog (libs.versions.toml) so bug fixes and security patches are picked up.
74
Explain the purpose of Koin in Android development and describe the main distinctions between it and Dagger when managing dependency injection.
KnownRevise
Koin is a lightweight dependency injection framework designed specifically for Kotlin that resolves dependencies at runtime using a functional DSL, whereas Dagger (typically used via Hilt on Android) generates dependency graphs at compile time through annotation processing. While Hilt is the officially recommended DI solution for Android, Koin offers a simpler, code-generation-free alternative favored for rapid prototyping or smaller codebases.
Koin: Utilizes Kotlin language features like reified type parameters and function literals. It requires no annotation processors or generated classes. You define modules using module { ... } blocks and retrieve instances via get() or injection via by inject().
Dagger/Hilt: Relies on static code generation. You annotate classes with @Inject, define bindings in @Module classes, and Hilt generates components that wire everything together during compilation. This ensures type safety at compile time but adds build complexity.
Performance and Build Characteristics
Runtime Overhead: Koin incurs a minor runtime cost due to resolution logic, though benchmarks show this is negligible for typical Android apps. Dagger/Hilt has virtually zero runtime overhead because the graph is pre-built.
Build Speed: Koin generally offers faster build times since it skips annotation processing steps. Dagger/Hilt can significantly increase compilation time, especially in large projects with complex graphs.
App Size: Koin adds a small library footprint. Dagger/Hilt adds generated code, which can increase APK size slightly but optimizes execution.
Android Lifecycle Integration
Hilt: Deeply integrated with Android architecture components. It provides scoped annotations like @ViewModelScoped, @ActivityScoped, and automatically injects into Activity, Fragment, and ViewModel via @HiltViewModel.
Koin: Requires the koin-android and koin-androidx-viewmodel extensions. Scoping is managed manually or via viewModel { } blocks within modules. It is less opinionated about Android lifecycle boundaries compared to Hilt.
Feature Comparison
Feature
Koin
Dagger (Hilt)
Approach
Runtime DSL
Compile-time Code Generation
Learning Curve
Low (Kotlin idiomatic)
High (Annotations, Concepts)
Error Detection
Runtime
Compile-time
Build Time
Faster
Slower
Official Support
Community
Google Recommended
Best Practices and Selection
Use Hilt for large-scale enterprise apps, teams following Google’s architecture guidelines, or projects requiring strict compile-time safety and deep Android lifecycle integration.
Use Koin for smaller projects, libraries, or teams prioritizing simplicity, faster build times, and readable Kotlin DSL over compile-time verification.
Avoid Circular Dependencies: Both frameworks fail here, but Koin detects them at runtime while Dagger fails at compile time.
Do Not Mix: Stick to one DI framework per project to avoid maintenance confusion and conflicting scopes.
Explicit Scoping: Koin requires explicit scope declarations (single {} for singleton, factory {} for new instances). Ensure shared instances such as repositories handle concurrency correctly when used across coroutines.
75
What strategies do you employ to ensure your Android application remains compatible and functional across a wide range of operating system versions?
KnownRevise
Supporting multiple Android versions centers on defining the minimum and target SDK levels in your build configuration and using compatibility libraries to bridge feature gaps. You explicitly declare the oldest supported OS, then guard newer API calls with runtime version checks so functionality degrades gracefully rather than crashing on older devices.
The foundation of version support lies in three properties within your module-level build.gradle.kts file: - minSdk: The lowest API level your app supports. Users on devices below this version cannot install the app. A common baseline is API 24 to cover the vast majority of active devices while avoiding very legacy code paths. - targetSdk: The API level you have tested against. The system may enable compatibility behaviors for apps targeting older versions, so you should always target the latest stable release to ensure correct behavior and security. - compileSdk: The version used to compile your code. This should match the latest available SDK so you can access new APIs, though it does not affect runtime behavior on its own.
Handling API Differences
New platform features often require specific API levels, such as runtime permissions (API 23+) or notification channels (API 26+). To use these safely: - AndroidX and Jetpack: Use libraries like androidx.core and androidx.work which backport newer behaviors to older versions, reducing the need for custom branching logic. - Runtime Checks: Wrap API-specific code in if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) blocks. The compiler allows calls to newer APIs as long as compileSdk is high enough, but the runtime check prevents errors on older devices. For permissions like POST_NOTIFICATIONS (API 33+), call checkSelfPermission inside the version guard before requesting or using the feature. - Manifest Flags: Some behaviors, like foreground service types (API 34+), require declarations in AndroidManifest.xml that are ignored on older versions but mandatory on new ones.
UI and Resource Adaptation
When building UI with Jetpack Compose, most layout scaling is handled automatically across screen densities and sizes. For version-specific styling or assets: - Resource Qualifiers: Use directories like values-v31 to provide alternative strings, dimensions, or themes for specific API levels. - Window Size Classes: For foldables and tablets, use androidx.window to adapt layouts based on available space rather than OS version alone.
Best Practices and Testing
Test on the Minimum: Always run your app on an emulator matching your minSdk to catch accidental usage of unsupported APIs.
Avoid Deprecated APIs: Do not use AsyncTask or IntentService (deprecated in API 30); replace them with Kotlin coroutines and WorkManager for background tasks.
Monitor Adoption: Use the Google Play Console dashboard to track version distribution and raise minSdk only when legacy device usage becomes negligible.
Use Lint: Enable Android Lint checks to warn you when calling APIs above your minSdk without a version check.
76
How does the Android Jetpack suite ensure application compatibility across diverse device form factors and varying operating system versions?
KnownRevise
Android Jetpack simplifies cross-device and cross-version development by providing libraries that abstract platform differences, ensuring backward compatibility and consistent UI behavior. Rather than relying solely on framework APIs that change between releases, developers use AndroidX components which bundle fallback implementations for older API levels and adapt layouts for phones, tablets, and foldables.
Jetpack libraries are distributed via Google Maven and are decoupled from the Android OS release cycle. This means bug fixes and feature updates arrive independently of OS updates, allowing developers to adopt improvements without forcing users to upgrade their OS. Key mechanisms include: - AndroidX Namespace: All modern Jetpack libraries use the androidx.* namespace, replacing the deprecated android.support.* libraries which are no longer maintained. - Wrapper Classes: Utilities like ContextCompat and ActivityCompat check the OS version internally and call the appropriate framework method, preventing crashes on older devices while enabling newer features where available. - Lifecycle Awareness: Components like ViewModel and LiveData survive configuration changes consistently across all supported API levels, reducing boilerplate code for state management. - Feature Backporting: Libraries such as androidx.core backport newer framework APIs (e.g., permission checking, file providers) to older Android versions, unifying the development surface.
Adapting to Device Form Factors
With the proliferation of foldables, tablets, wearables, and large screens, Jetpack provides tools to manage window metrics and layout adaptation without maintaining multiple codebases: - Jetpack Compose: The modern UI toolkit uses declarative syntax that adapts naturally to different screen sizes without requiring separate XML layout qualifiers for every density or orientation. - WindowManager: The androidx.window library helps detect foldable hinges, screen folds, and posture changes, allowing apps to adjust UI layout dynamically to utilize expanded screen real estate. - Responsive Layouts: Using ConstraintLayout in Views or Compose’s BoxWithConstraints, developers can define breakpoints that rearrange UI elements from a single-column list to a master-detail grid based on available width.
Handling API Level Differences
While Jetpack handles much compatibility, some features are strictly bound to specific OS versions and require explicit handling by the developer: - Minimum SDK: Define minSdk in Gradle to set the baseline support level; lowering this increases reach but limits access to modern APIs. - Runtime Checks: Use Build.VERSION.SDK_INT to guard APIs introduced after minSdk, ensuring the app does not invoke methods that do not exist on the running device. - Permissions and Privacy: Handle dangerous permissions (API 23+), background location (API 29/31), and notification permissions (API 33+) using the Activity Result APIs rather than the deprecated onRequestPermissionsResult. - Scoped Storage: Adhere to scoped storage rules introduced in API 29 and enforced in API 30, using MediaStore or Storage Access Framework instead of raw file paths.
Best Practices
Test on Multiple Configurations: Use Firebase Test Lab or local emulators covering a matrix of different API levels, screen densities, and form factors to catch compatibility issues early.
Prefer Jetpack Over Framework: Always choose androidx implementations (e.g., androidx.fragment.app.Fragment) over framework ones (android.app.Fragment) for stability and consistent behavior across versions.
Dynamic Delivery: Use Play Feature Delivery to download resources or code specific to certain device configurations, reducing initial app size for users on older or less capable hardware.
Avoid Hardcoding: Do not hardcode device dimensions or pixel values; use dp units and Compose modifiers to ensure scalability across the diverse Android ecosystem.
77
What strategy should be adopted when encountering deprecated methods or APIs during Android development?
KnownRevise
Handling deprecated APIs requires systematic migration driven by lint warnings, where legacy code is replaced with modern equivalents like coroutines or Activity Result APIs while maintaining backward compatibility.
Android Studio highlights deprecated symbols with strikethrough text and lint warnings, and the @Deprecated annotation often carries a ReplaceWith suggestion that lets you apply an automatic fix. Run ./gradlew lint to generate a comprehensive report of every deprecated usage across the modules. Assess the risk of each finding based on: - Removal Timeline: Some APIs are only deprecated, others (such as Kotlin Android Extensions synthetics) have already been removed from the tooling, and a few platform APIs are removed entirely in future API levels. - Functionality: Does the replacement offer better performance, safety or lifecycle awareness? - Scope: Is the usage isolated to one screen or widespread across the codebase?
Common Deprecations and Modern Replacements
Several legacy APIs have official successors recommended by Google.
Type-safe contracts, lifecycle-aware callbacks, no request-code collisions
onRequestPermissionsResult
Activity Result APIs (RequestPermission contract)
Same benefits, consistent permission flow
getExternalStoragePublicDirectory (API 29+)
MediaStore / Scoped Storage
Privacy and scoped access to shared collections
Kotlin Android Extensions synthetics (removed)
Jetpack Compose or View Binding
Synthetic properties broke null safety and were removed from the Kotlin Gradle plugin
Migration Strategy and Compatibility
Migration depends on your minSdkVersion. When a replacement exists only in the platform, guard it with Build.VERSION.SDK_INT and provide a fallback for older devices; most AndroidX libraries (Activity Result, WorkManager, MediaStore helpers) backport the functionality, so no version check is needed once the dependency is added. 1. Prioritize security and correctness first: Fix storage, permission and background-work deprecations, since these affect user data and app behaviour on current devices. 2. Batch refactoring: Group changes by module or feature to keep reviews small and avoid merge conflicts. 3. Keep behaviour equivalent: Verify that the replacement preserves the old semantics — for example, WorkManager runs deferrable, guaranteed work rather than the immediate background execution IntentService provided. 4. Clean up dead code: Remove the old imports, helper methods and request-code constants once all call sites are migrated.
Best Practices
Treat warnings as errors: Configure lint (for example, the ObsoleteLintCustomCheck and deprecation checks) to fail the build in CI so regressions are caught immediately.
Test the refactored paths: Ensure unit and UI tests cover the new code, especially background-work and permission migrations where timing differs.
Document the new patterns: Update internal wikis and code-review checklists so new developers do not reintroduce deprecated APIs.
Monitor crash reports after release: Behaviour differences (such as WorkManager scheduling versus IntentService execution) can surface as timing- or lifecycle-related crashes.
78
What strategies exist for building applications that target Android alongside iOS, web, or desktop, and how do they compare?
KnownRevise
The Android-recommended strategy is Kotlin Multiplatform (KMP), which lets you share business logic across platforms in Kotlin while keeping native UI, with Compose Multiplatform extending that sharing to the presentation layer where appropriate.
Multi-platform development falls into two broad categories:
Logic sharing: share networking, data, domain and state management, but write UI natively per platform. This is KMP’s default model and gives the most native feel.
Full-stack sharing: share both logic and UI from a single codebase. Flutter, React Native and Compose Multiplatform fit here, trading some platform fidelity for development speed.
Kotlin Multiplatform
KMP compiles shared Kotlin code to native binaries (via Kotlin/Native), JVM bytecode, and JavaScript/WebAssembly. You write common code in a commonMain source set and use expect/actual declarations for platform-specific behaviour such as secure storage, platform paths, or notifications.
Best fit: Android-first teams that also ship iOS, or teams that want one source of truth for API clients, caching, and business rules.
UI: keep Jetpack Compose on Android and SwiftUI on iOS initially; migrate shared UI later if Compose Multiplatform covers your target platforms.
Ecosystem: works with Ktor or Retrofit-style clients, SQLDelight or Room (Room 2.7.0+ ships a multiplatform artifact for Android, iOS, JVM and desktop), and kotlinx.serialization.
Compose Multiplatform and Other Frameworks
Compose Multiplatform, from JetBrains and built on Jetpack Compose, lets you share UI across Android, iOS, desktop (Windows, macOS, Linux) and web. iOS support has been stable since Compose Multiplatform 1.7.0, so it is a production option, though you should still verify the latest release notes for your target platforms before a large commitment.
Approach
Language
UI Shared
Best For
KMP (logic only)
Kotlin
No
Android-first teams, maximum native fidelity
Compose Multiplatform
Kotlin
Yes
Teams already on Compose targeting iOS/desktop/web
Flutter
Dart
Yes
Greenfield apps needing identical UI everywhere
React Native
TypeScript/JavaScript
Yes
Teams with web/React expertise
Flutter renders its own UI with Skia/Impeller, giving consistent pixels across platforms but a less native look. React Native uses native components through a JavaScript bridge and the newer Fabric architecture, feeling closer to native but requiring native modules for advanced features.
Best Practices
Start with the domain layer: share repositories, use cases, and models first; keep UI native until sharing it clearly pays off.
Respect platform conventions: Material on Android, Human Interface Guidelines on iOS. Shared UI can feel uncanny if it ignores platform norms.
Isolate platform APIs: never leak Android Context or iOS UIKit types into shared code; wrap them behind expect/actual interfaces.
Test on every target: shared code can behave differently on JVM, native, and JavaScript/WebAssembly runtimes.
Don’t force 100% sharing: sharing low-value or highly platform-specific code adds indirection without benefit.
Common Pitfalls
Over-sharing UI before checking the stability of the Compose Multiplatform version you actually target on non-Android platforms.
Putting Android-specific dependencies in the shared module, which breaks compilation for iOS or web targets.
Ignoring platform capabilities such as background execution limits, notification permissions (Android API 33+ for POST_NOTIFICATIONS), or iOS background modes.
Treating KMP as a rewrite rather than an incremental adoption; the lowest-risk path is adding a shared module to an existing Android and iOS app.
79
What are the primary technical distinctions between the Android Runtime (ART) and the legacy Dalvik virtual machine used in earlier Android versions?
KnownRevise
The Android Runtime (ART) superseded Dalvik as the managed execution environment, fundamentally changing how bytecode is compiled and executed to improve performance and battery efficiency. While Dalvik relied on Just-in-Time compilation during execution, ART employs a hybrid approach combining Ahead-of-Time compilation at install time with runtime profiling.
Read full answerHide full answer
Applies toAndroid 5.0+ (API 21+) for ART as default; Dalvik is obsolete since Android 5.0 (API 21).
Core Differences
Aspect
Dalvik (pre-ART)
ART (Android 5.0+)
Compilation
Interpretation + JIT (from Android 2.2)
AOT at install (dex2oat) + JIT from Android 7.0
Startup speed
Slower (runtime compilation)
Faster (native code ready)
Storage footprint
Lower (no precompiled native code)
Higher initially, mitigated by compact profiles in later versions
Dalvik: Interpreted DEX bytecode and used Just-in-Time (JIT) compilation (introduced in Android 2.2) to translate frequently called methods into native code. This kept install times short but added CPU overhead during execution.
ART: Introduced Ahead-of-Time (AOT) compilation via dex2oat, converting DEX bytecode to native machine code at install time. Starting with Android 7.0 (API 24), ART added a hybrid JIT/AOT model: runtime profiling identifies hot code paths, which are then compiled on-device, while the rest stays interpreted or precompiled.
Profile-guided optimization: Modern ART collects usage profiles and Google Play delivers them with the app, allowing hot paths to be precompiled before first launch.
Performance and Battery Impact
Startup and responsiveness: Native code is ready to execute, so ART reduces app startup time and improves UI smoothness compared to Dalvik.
CPU overhead: Less runtime compilation means the CPU spends fewer cycles translating bytecode, yielding measurable battery life improvements.
Storage trade-off: The original AOT approach stored optimized native code alongside the APK, increasing storage use. Modern Android mitigates this by storing compact profile files and compiling only hot methods, balancing storage and performance.
Garbage Collection Enhancements
Dalvik: Used a stop-the-world GC that could cause visible pauses and dropped frames, especially in memory-intensive apps.
ART: Implemented concurrent garbage collection, moving much of the work to a separate thread without halting application threads. This reduces UI jank and improves frame rates in games and media apps.
Debugging and Diagnostics
Logging and exceptions: ART provides more detailed GC logging and improved exception reporting, including precise context for null pointer exceptions.
Profiling: The Android Studio Profiler uses ART’s runtime instrumentation to deliver accurate CPU and memory data, making bottlenecks easier to identify than under Dalvik.
Current Status and Compatibility
Dalvik removal: Dalvik was removed from AOSP in Android 5.0; there is no flag or setting to enable it on modern devices.
Bytecode compatibility: Both runtimes execute the same DEX bytecode, so apps compiled for Dalvik run unchanged on ART.
Targeting modern Android: When targeting Android 16 (API 36), developers should assume ART exclusively and optimize for its compilation profiles and concurrent GC rather than legacy runtime concerns.
80
What essential factors must developers address when preparing an Android application for internationalization and localization?
KnownRevise
Internationalization (i18n) and localization (l10n) involve designing an app to adapt to languages, scripts, and cultural conventions without code changes. The essential work is externalizing every user-facing string and asset, then applying locale-aware formatting and layout rules so the same build serves every market. This keeps maintenance manageable while giving users a native experience.
Android selects resources using configuration qualifiers. Base strings belong in res/values/strings.xml, with translations in directories such as res/values-es/, res/values-ja/, or res/values-zh-rCN/. In Jetpack Compose, read them with stringResource(R.string.key); in XML, use @string/key. Never hardcode text in Kotlin or layout files, because translation tools cannot extract it and updates require code changes. Use <plurals> for quantity-sensitive text, and remember that languages need more buckets than English singular/plural. Provide locale-specific drawables, colors, and dimensions when cultural meaning or text size differs.
Locale Management and System Integration
From Android 13 (API 33), the platform supports per-app language preferences. Use LocaleManager to read and write applicationLocales, and declare supported locales with android:localeConfig in the manifest so the system settings UI can offer them. For earlier versions, AppCompatDelegate.setApplicationLocales(LocaleListCompat) from AppCompat supplies the same behavior. The UI must react to locale changes: in Compose, observing LocalConfiguration.current or a locale held in a ViewModel triggers recomposition. Let users pick a language inside the app when your product requires it, but persist the choice and respect the system default otherwise.
Layout Directionality and RTL Support
Right-to-left scripts such as Arabic and Hebrew need mirrored layouts. Declare android:supportsRtl="true" in the <application> element; without it, the system will not mirror Views or Compose layouts. In Compose, read LocalLayoutDirection.current and use start/end margins, padding, and alignment instead of left/right. Icons, navigation arrows, and reading order should also reverse. Test on emulators or devices with RTL locales enabled, because alignment and touch-target problems often appear only then.
Regional Formatting for Data Types
Numbers, dates, times, currencies, and units differ by region. Format dates with java.time.format.DateTimeFormatter.ofLocalizedDate(...).withLocale(locale), and currencies with java.text.NumberFormat.getCurrencyInstance(locale) or android.icu.text.NumberFormat when ICU features are needed. Never assume MM/DD ordering, a period as a decimal separator, or a 24-hour clock. Store and compute instants in UTC, then display them in the user’s time zone using java.time.ZonedDateTime; avoid fixed offsets because daylight-saving rules change.
Common Pitfalls and Best Practices
String concatenation: Do not assemble sentences from multiple string resources; word order changes across languages.
Text expansion: German or Finnish can be 30% longer than English, so use flexible Column, Row, and Box weights rather than fixed sizes.
Text in images: Never bake text into images; use separate drawables or overlays per locale.
Pseudo-locales: Test with en-XA and ar-XB to catch untranslated strings and layout breakage early.
Locale fallback: Android falls back through the qualifier chain, but verify that missing translations do not expose raw keys.
81
What practices and APIs should Android developers use to make their applications accessible to users with disabilities?
KnownRevise
Accessibility in Android means building apps that people with visual, hearing, motor, or cognitive impairments can navigate and understand. The foundation is exposing semantic information—labels, actions, and state—to accessibility services such as TalkBack and Switch Access, rather than relying on visual appearance alone.
Read full answerHide full answer
Applies toAndroid 16 (API 36) with Jetpack Compose and Kotlin 2.x
Core Accessibility Principles
Content labels: Every Button, IconButton, Image, and informative text needs a contentDescription or semantic label so screen readers can announce it.
Focus order: Navigation must follow a logical reading order; use semantics properties like traversalIndex to override defaults when layout order differs from visual order.
Touch target size: Interactive elements should be at least 48x48 dp, e.g. sizeIn(minWidth = 48.dp, minHeight = 48.dp), so users with motor impairments can tap reliably.
Color contrast: Text and icons should meet WCAG contrast ratios (4.5:1 for normal text) so users with low vision can read content.
No color-only cues: Never convey meaning through color alone; pair it with text, icons, or patterns for color-blind users.
Jetpack Compose Semantics
Jetpack Compose uses the semantics modifier tree to expose accessibility information. Compose merges semantics from child composables where possible, but complex custom components often need explicit configuration.
Modifier / Property
Purpose
contentDescription
Text alternative for images and icon buttons
semantics { }
Set label, role, state description, or custom actions
clearAndSetSemantics
Replace merged child semantics with a single description for grouped content
progressBarRangeInfo
Announce progress value for sliders and progress indicators
For dynamic state, update stateDescription so TalkBack announces changes, such as “3 of 5 items selected” or “loading complete”. A clickable Box should also declare role = Role.Button, because a plain clickable composable is not announced as a button.
Testing and Validation
TalkBack: Enable in Settings > Accessibility and navigate by touch and gesture to hear announcements.
Accessibility Scanner: Google’s app (API 21+) scans layouts and suggests improvements for touch targets, contrast, and labels.
Compose semantics tree: Use printToLog in tests or Layout Inspector to verify the semantics hierarchy.
Automated tests: Query nodes by onNodeWithTag or onNodeWithContentDescription to ensure labels exist.
Legacy Views and XML
In XML layouts, use android:contentDescription, android:labelFor, and android:importantForAccessibility. The deprecated kotlin-android-extensions synthetics are removed and offer no accessibility help; use View Binding or Compose instead. For WebView content inside AndroidView, ensure the web page has its own accessible markup because Compose semantics do not reach inside.
Common Pitfalls
Decorative images: Set contentDescription = null so screen readers skip purely decorative graphics; do not leave it empty.
Merged semantics: Use semantics(mergeDescendants = true) when a parent should speak for its children, and clearAndSetSemantics when child descriptions would create noise.
Dynamic content: Updating text without changing stateDescription can leave TalkBack silent; announce state changes explicitly.
Custom composables: Always pair interactive gestures with the correct role and clickable semantics.
Touch targets: Small icons or text links below 48x48 dp are hard to activate for users with motor impairments.
Following these practices ensures your app works with TalkBack, Switch Access, and magnification across Android versions.
82
How should you structure and organize code in an Android project to keep it maintainable?
KnownRevise
Good Android code organization comes from layered architecture with clear module boundaries, where UI, business logic and data are separated, dependencies point in one direction, and each file has a single reason to change. The officially recommended baseline is a UI layer built with Jetpack Compose, a domain/data layer built with coroutines and Flow, and dependency injection wired by Hilt.
Read full answerHide full answer
Applies toAndroid 16 (API 36) with Kotlin 2.x, Jetpack Compose, Hilt, Room, Retrofit/OkHttp and WorkManager.
Layer Your App by Responsibility
UI layer: Compose screens and composables only render state and forward user events upward. They hold no business logic and ideally no Android Context beyond what Compose itself provides.
Domain layer (optional but valuable in larger apps): use cases (GetUserProfile, SyncOrders) that orchestrate one piece of business logic and expose a Flow or suspend result.
Data layer: repositories are the single source of truth. They combine Room (local cache), Retrofit/OkHttp (network) and WorkManager (background sync), and they decide whether to serve cache or network.
Dependencies must point inward: UI depends on domain/use cases, use cases depend on repositories, repositories depend on data sources — never the reverse.
Package by Feature, Not by Layer
Group code by feature (feature/profile, feature/checkout, feature/orders) rather than by type (activities, fragments, viewmodels). Feature packages make it easier to find related code, to delete a whole feature, and to extract a feature into its own Gradle module later. Inside each feature package, keep a small set of files: ProfileScreen.kt, ProfileViewModel.kt, ProfileUiState.kt, and so on. Shared code lives in core/ or common/ (theming, networking, database, utility coroutines).
Use Multi-module Gradle Structure When the App Grows
Start with a single app module, then split into modules as compile times and team boundaries demand. A typical layout uses an :app module plus :core:data, :core:designsystem, :core:database, :core:network, and one module per feature such as :feature:profile. Use the Kotlin DSL (build.gradle.kts) with a version catalog (libs.versions.toml) so dependency versions are declared once. Modules enforce your architecture at compile time — a feature module literally cannot reach into another feature’s internals if the dependency isn’t declared.
Keep State and Events Unidirectional
ViewModel with StateFlow: expose a single uiState property, typically a data class holding everything the screen renders, plus a SharedFlow for one-shot events such as navigation or snackbar messages.
State hoisting in Compose: stateless composables take state in and emit events out, which makes them reusable and previewable.
Coroutines: run network and disk work on Dispatchers.IO, never on the main thread; use viewModelScope for screen-scoped work and WorkManager for work that must survive process death.
Follow Naming and Formatting Conventions
Consistent naming is the cheapest form of documentation. Name composables after what they render (OrderSummaryCard), ViewModels after the screen (CheckoutViewModel), use cases as verbs (RefreshFeed), and repository interfaces after the aggregate (UserRepository). Put constants that several features need in a shared core module or a dedicated config module rather than duplicating them. Keep functions short, prefer immutability (val, data class copies), and annotate nullable platform types from Java APIs.
Common Pitfalls
God objects: a ViewModel or repository that touches every screen. Split per feature or per aggregate.
Business logic in composables: conditionals like if (user.isAdmin) belong in the ViewModel or use case, so the same rule can be unit tested.
Passing Context or Activity into ViewModels: this leaks lifecycle-bound objects and breaks testability. Inject an application-scoped dependency through Hilt instead.
Multiple sources of truth: reading straight from Room in one screen and from a network call in another produces inconsistent UI. Route everything through the repository.
Skipping modularization until it hurts: refactoring a 200-file single module into features is far more expensive than starting with two modules early.
83
Describe the core principles of Clean Architecture and how they are applied when building Android applications.
KnownRevise
Clean Architecture is a software design philosophy that organizes code into concentric layers so that business rules stay independent of the user interface, database, and frameworks. In Android development, this means placing use cases and enterprise rules in a domain layer that has no dependencies on Android SDK classes, while the presentation and data layers depend inward toward that domain.
Read full answerHide full answer
Applies toAndroid 16 (API 36) with Kotlin 2.x and Jetpack Compose (principles are version-agnostic)
Core Principles
Independence of Frameworks: Business logic does not depend on Android, Jetpack, or UI toolkits, so the core can be tested as plain JVM code.
Testability: Use cases can run without an emulator, database, or network connection.
Independence of UI: The interface can switch from Jetpack Compose to XML Views without touching business rules.
Independence of Data: The storage mechanism can change from Room to DataStore without affecting the domain.
Dependency Rule: Source code dependencies must only point inward; outer layers may know about inner layers, but inner layers never know about outer ones.
The domain layer defines repository interfaces, while the data layer provides the concrete implementations. This inversion lets the domain remain unaware of whether data comes from a local database or a remote API.
Dependency Rule in Practice
The dependency rule is the most important constraint. A class in the domain layer cannot import androidx, retrofit, or room packages. If a use case needs data, it calls a repository interface defined in the domain module. The data module implements that interface using Retrofit or Room and is injected into the presentation layer, often with Hilt. This direction ensures that framework changes stay contained in the outer layers.
Benefits
Maintainability: Changes to UI or data sources do not ripple into business logic.
Parallel Development: Teams can work on UI and data simultaneously against domain contracts.
Longevity: The app survives framework deprecations because core logic is not coupled to them.
Clear Boundaries: Each layer has a single responsibility, reducing God classes and tangled code.
Common Pitfalls
Over-Engineering: For a small screen or prototype, a full three-layer setup adds unnecessary boilerplate; start simple and refactor as complexity grows.
Anemic Domain: Putting only data holders in the domain layer defeats the purpose; business rules should live in use cases or entities.
Leaking Data Models: Do not pass Room entities or Retrofit response models into the domain or UI; map them to domain entities and UI state models instead.
God Use Cases: A single use case should do one thing; avoid orchestrating entire flows inside one class.
84
In what ways does adopting the MVVM architecture improve the testability of an Android application?
KnownRevise
MVVM improves testability by isolating presentation logic inside the ViewModel, removing direct dependencies on Android UI components like Activity or View. This separation allows you to verify business rules and state changes with fast local JVM tests instead of relying solely on slow instrumented tests on an emulator.
Read full answerHide full answer
Applies toAndroid 16 (API 36) with Kotlin 2.x, Jetpack Compose, and ViewModel with StateFlow
Separation of UI and Logic
In MVVM, the Jetpack Compose UI observes state and sends events, while the ViewModel holds the state and processes logic. Because the ViewModel does not reference UI widgets, you can test its behavior without launching a screen. Pure domain logic should sit further down in Use Cases or Repositories, which are plain Kotlin classes requiring no Android dependencies at all and can be tested with standard JUnit.
Local Tests vs Instrumented Tests
Instrumented tests: Run on a device or emulator; required for UI interactions and classes needing the Android framework Context.
Local unit tests: Run on your development machine JVM; much faster and suitable for ViewModel logic and pure data layers. A plain ViewModel with no Android dependencies needs only kotlinx-coroutines-test; reach for Robolectric only when the code under test touches Android types such as Context.
MVVM shifts the majority of verification into the local test category, speeding up the feedback loop during development.
Swappable Dependencies
The ViewModel should depend on abstractions (interfaces) rather than concrete implementations. With Hilt, you bind these interfaces to real classes in production and fake or mock implementations in tests. This lets you simulate network failures, loading states, or cached data without hitting real servers or databases.
Best Practices
Keep state immutable: Use a data class for UI state so tests can compare exact values with assertEquals.
Avoid Context in ViewModel: Never store a Context in a ViewModel; use SavedStateHandle or dependency injection for needed resources.
Test StateFlow deterministically: Use the kotlinx-coroutines-test library and runTest to control coroutine execution, and call Dispatchers.setMain with a test dispatcher so viewModelScope runs predictably.
Don’t test the View: Verify the ViewModel emits the right state; trust Compose to render it, and cover rendering separately with Compose UI tests when needed.
Manual injection in tests: For pure unit tests, pass fakes through the ViewModel constructor rather than initializing the full Hilt container.
85
Explain what Android App Bundles are and how they improve app distribution compared to standard APK files.
KnownRevise
Android App Bundles (AAB) are the publishing format for Android apps that enable Google Play to generate and serve optimized APKs for each user’s device configuration. Instead of uploading multiple APKs for different screen densities or ABIs, developers upload a single .aab artifact containing all code and resources, which the Play Store then splits into optimized downloads.
An AAB file is a ZIP archive containing all compiled code (DEX files) and resources organized into modules. When a user requests an app, Google Play uses the bundle to generate signed APKs tailored to that device’s CPU architecture (ABI), screen density, and language. This process is transparent to the user and happens server-side.
Key Advantages
Reduced Download Size: Users download only the resources matching their device, typically reducing size by 15-50% compared to a universal APK.
Simplified Publishing: Developers manage one artifact instead of multiple APK variants.
Dynamic Delivery: Features can be deferred until needed via Dynamic Feature Modules, reducing initial install size further.
Play Asset Delivery: Large assets (textures, levels) are delivered separately via Google Play, bypassing APK size limits.
AAB vs APK
Aspect
APK
AAB
Artifact
One or more APKs per variant
Single .aab bundle
Delivery
Universal or manually split APKs
Play-generated, device-specific APKs
Download size
Includes resources for all devices
Only resources for the target device
Publishing
Multiple files to manage
One upload
Dynamic features
Not supported
Supported via Dynamic Feature Modules
Sideloading
Can be installed directly
Must be converted to APKs with bundletool
Module Structure
Base Module: Contains essential code and resources required for app launch. All other modules depend on it.
Dynamic Feature Modules: Optional components declared with the com.android.dynamic-feature plugin. They can be installed at install-time, on-demand, or conditionally based on device features.
Asset Packs: Used for large media assets managed through Play Asset Delivery rather than the bundle itself.
When to Use AAB
AAB is mandatory for new apps on Google Play and strongly recommended for all Android distributions targeting Play. If distributing outside Play (e.g., enterprise sideloading), you may still need to generate APKs from the bundle using bundletool, as some legacy installers cannot parse .aab files directly.
Best Practices and Pitfalls
Test Locally: Use bundletool to generate local APK sets from your AAB to verify behavior before uploading.
Handle Dynamic Modules: Always check SplitInstallManager availability before launching features housed in dynamic modules to avoid crashes.
Avoid Absolute Paths: Resources may be split across APKs; reference them via resource IDs rather than file paths.
Size Limits: The base module must fit within Play Store’s download size constraints (150MB); deliver larger assets via Play Asset Delivery instead of legacy expansion files.
Signing: Google Play manages app signing by default; upload keys authenticate uploads while Google holds the signing key.
86
Explain the factors behind Kotlin's widespread adoption for Android development and the advantages it offers over previous languages.
KnownRevise
Kotlin is a statically typed programming language that runs on the JVM and is now the preferred language for Android development. Google announced first-class support in 2017 and shifted to a Kotlin-first strategy for Android APIs in 2019, making it the default choice for new projects because it reduces boilerplate, prevents common crashes through null safety, and integrates seamlessly with existing Java libraries.
Read full answerHide full answer
Applies toKotlin 2.x and Android 16 (API 36)
Interoperability and Google Backing
Java Interoperability: Kotlin is 100% interoperable with Java, allowing teams to migrate modules incrementally rather than rewriting entire codebases at once.
Official Endorsement: Android framework APIs are documented and designed with Kotlin in mind, meaning new Jetpack libraries often expose Kotlin-specific extensions first.
Tooling Support: Android Studio includes dedicated Kotlin tooling, including a Java-to-Kotlin converter that helps modernize legacy files automatically.
Safety and Conciseness
Null Safety: The type system distinguishes between nullable and non-nullable references, eliminating the risk of NullPointerException at compile time rather than at runtime.
Reduced Boilerplate: Features like data classes, type inference, and default arguments remove the need for getters, setters, and verbose constructors common in Java.
Extension Functions: Developers can add functionality to existing classes without inheritance, keeping utility logic readable and modular without altering source code.
The Modern Android Stack
Jetpack Compose: The modern declarative UI toolkit is built entirely in Kotlin, replacing XML Views for new development and enabling reactive UI patterns.
Coroutines and Flow: These handle asynchronous work and data streams, replacing deprecated patterns like AsyncTask (deprecated in API 30) and complex callback chains.
Architecture Components: ViewModel with StateFlow, Hilt for dependency injection, and Room for local data all provide Kotlin-optimized APIs that leverage suspension and flows.
Kotlin vs Java
Feature
Kotlin
Java
Null Safety
Built into type system
Manual checks, prone to NullPointerException
Syntax
Concise, expressive
Verbose, boilerplate-heavy
Async
Coroutines and Flow
Threads, AsyncTask (deprecated)
UI
Jetpack Compose (Kotlin-only)
XML Views
Interop
Calls Java seamlessly
Can call Kotlin but with friction
Best Practices for Adoption
Start new modules in Kotlin and migrate legacy Java files incrementally during refactoring cycles.
Use coroutines with ViewModel and StateFlow for UI state instead of threads or LiveData where possible.
Enable the Kotlin DSL (build.gradle.kts) with version catalogs for type-safe dependency management.
Avoid mixing heavy Java logic into new Compose screens to maintain a consistent and modern codebase.
87
Explain what Kotlin coroutines are and how they make asynchronous programming easier on Android.
KnownRevise
Coroutines are Kotlin’s language-level feature for asynchronous, non-blocking programming: lightweight suspendable computations that let you run long-running tasks off the main thread while keeping the code looking like a straightforward, top-to-bottom sequence of statements. On Android, they replace callback-heavy or thread-blocking approaches and are the officially recommended way to do network, database, and other asynchronous work.
Read full answerHide full answer
Applies toKotlin 2.x with Android 16 (API 36), using `kotlinx-coroutines-core`, `kotlinx-coroutines-android`, `androidx.lifecycle:lifecycle-viewmodel-ktx`, and `androidx.lifecycle:lifecycle-runtime-compose`.
What a Coroutine Is
A coroutine is not a thread; it is a job that runs on a thread pool and can be suspended at suspension points without blocking the thread it runs on. Because they are cheap, you can launch thousands of coroutines where you would otherwise manage a handful of threads.
Suspend function: a function declared with the suspend keyword that can pause and resume without blocking the current thread.
Coroutine builder: launch (fire-and-forget) or async (returns a Deferred result).
CoroutineScope: the lifetime boundary that cancels coroutines when their scope is destroyed.
Dispatcher: decides which threads the coroutine uses, e.g. Dispatchers.IO for network and disk, Dispatchers.Main for UI work.
How They Simplify Asynchronous Work
Linear, readable code: instead of nested callbacks, you call suspend functions in order and read results directly, including with try/catch and loops.
Thread control without handoffs: withContext(Dispatchers.IO) moves work off the main thread and returns the result where you need it.
Structured concurrency: coroutines launched in a scope are cancelled when that scope ends, so you avoid leaking work when the user leaves a screen.
Native exception handling: failures propagate through try/catch and through the scope’s CoroutineExceptionHandler, unlike raw callbacks or AsyncTask (deprecated since API 30, replaced by coroutines or WorkManager).
Lightweight: suspending instead of blocking means one real thread can serve many concurrent operations.
Key Dispatchers and When to Use Them
Dispatcher
Use for
Notes
Dispatchers.Main
Touching composables, views, ViewModel updates
Never do network or disk here
Dispatchers.IO
Network calls, Room, file access
Backed by a large thread pool
Dispatchers.Default
CPU-bound work such as sorting or parsing large JSON
Uses a pool sized to CPU cores
Structured Concurrency on Android
The recommended pattern is to start coroutines from a ViewModel using viewModelScope (provided by androidx.lifecycle:lifecycle-viewmodel-ktx) and expose results as StateFlow. The scope is cancelled automatically when the ViewModel is cleared, so in-flight requests stop when the user exits the screen. In a Compose UI, collect that StateFlow with collectAsStateWithLifecycle() (from androidx.lifecycle:lifecycle-runtime-compose) so collection pauses when the app is in the background, avoiding unnecessary work. For truly background work that must survive process death, use WorkManager instead of a coroutine scope.
A typical flow looks like this: a repository exposes a suspend function that internally uses withContext(Dispatchers.IO) to perform a Retrofit network call and a Room database insert; the ViewModel launches a coroutine in viewModelScope, calls that suspend function, and updates a MutableStateFlow with the result; the UI collects the StateFlow and recomposes automatically.
Best Practices and Common Pitfalls
Never block the main thread: wrap network, Room, and file work in withContext(Dispatchers.IO) or call an already-suspending function that does.
Prefer suspend functions over exposing Deferred or Flow for one-shot requests; return Flow only when you want a stream of values.
Do not use GlobalScope: it has no lifecycle bound and leaks work; use viewModelScope, lifecycleScope, or a manually created scope.
Handle cancellation properly: check isActive or use ensureActive() in long loops, and make suspend functions cooperative so cancellation works.
Catch exceptions close to the work: a try/catch around launch does not catch exceptions thrown in a child coroutine the same way a plain function call does; catch inside the coroutine or install a CoroutineExceptionHandler.
Avoid runBlocking in app code: it blocks the thread and is meant for tests or main() functions, not Android UI code.
88
Can you define Jetpack Compose and explain how it transforms the approach to building user interfaces on Android?
KnownRevise
Jetpack Compose is Android’s modern toolkit for building native UI. It simplifies and accelerates UI development on Android with less code, powerful tools, and intuitive Kotlin APIs. The fundamental shift is adopting a declarative UI paradigm instead of the older imperative approach used with XML Views.
Read full answerHide full answer
Applies toAndroid 16 (API 36) with Kotlin 2.x and Jetpack Compose
The Declarative Paradigm
In the traditional XML Views system, development was imperative: you inflated a layout, found views by ID, and manually mutated their properties when data changed. Jetpack Compose flips this model. You describe what the UI should look like for a given state using Kotlin functions annotated with @Composable. When the underlying state changes, the framework automatically updates the UI hierarchy through a process called recomposition. This removes the need for manual view manipulation and reduces the risk of state desynchronization. Recomposition is also smart: Compose tracks which state each composable read and only re-executes the parts of the UI whose inputs actually changed, keeping updates efficient.
Compose vs XML Views
Aspect
XML Views (Legacy)
Jetpack Compose (Modern)
Approach
Imperative (mutate views)
Declarative (state drives UI)
Language
XML layouts + Kotlin/Java
Kotlin only
State
Manual updates (setText)
Automatic recomposition
Boilerplate
High (adapters, holders)
Low (lazy lists, modifiers)
Preview
Limited
Interactive previews
State and Recomposition
State management is central to Compose. State objects, such as those created with mutableStateOf, hold data that the UI reads. When you write to a state object, Compose schedules recomposition for any composable that read that data. State typically flows down from a ViewModel using StateFlow, while events flow up from the UI. This unidirectional data flow makes the UI behavior predictable and easier to test. Compose also supports interoperability, allowing you to embed Views in Compose via AndroidView and Compose in Views via ComposeView during migration.
Best Practices and Pitfalls
State Hoisting: Move state to the caller or a ViewModel to make composables stateless and reusable.
Avoid Side Effects: Do not perform network or disk work directly inside a composable body; use LaunchedEffect or observe Flow from a ViewModel instead.
Stable Types: Ensure state classes are immutable or properly annotated with @Stable to prevent unnecessary recomposition.
Modifiers: Always pass modifiers as parameters to custom composables to allow flexible layout configuration.
Previews: Use @Preview annotations to render composables in Android Studio without deploying to a device.
Migration: You do not need to rewrite everything at once; use the interoperability APIs to migrate screen by screen.
89
Describe the mechanism Android uses to schedule and execute deferrable background tasks through WorkManager.
KnownRevise
Android handles deferrable, guaranteed background work through WorkManager, a Jetpack library that persists work requests in an internal SQLite database and executes them even if the app process dies or the device reboots. It chooses the best execution path based on the device state and API level, making it the recommended solution for tasks that must run reliably but do not need to complete immediately.
WorkManager sits on top of lower-level scheduling APIs and automatically selects the right one for the platform: - On API 23+, it uses JobScheduler as the underlying scheduler. - On older API levels (minimum supported is API 14), it falls back to a combination of AlarmManager and BroadcastReceiver.
When you enqueue a WorkRequest, WorkManager stores it in its internal database. A Worker subclass performs the actual work in its doWork() method, which runs on a background thread (no need for Dispatchers.IO). The worker returns a Result to signal success, failure, or retry.
Types of Work Requests
OneTimeWorkRequest: Runs a single time. Use for sync operations, upload, or one-off data processing.
PeriodicWorkRequest: Runs repeatedly at a fixed interval (minimum 15 minutes). Use for periodic sync or cleanup.
You can also chain requests with WorkContinuation using beginWith() and then(), so a second worker runs only after the first succeeds.
Constraints and Scheduling
Constraints tell WorkManager when a task is eligible to run. Common constraints include:
Constraint
Meaning
setRequiredNetworkType(NetworkType.CONNECTED)
Requires an active network connection
setRequiresBatteryNotLow(true)
Skips execution when battery is low
setRequiresCharging(true)
Runs only while the device charges
setRequiresStorageNotLow(true)
Waits for available storage
WorkManager also supports unique work (enqueueUniqueWork) to avoid duplicate scheduled tasks, and it respects Doze mode and app standby automatically.
Observing Work Status
Each WorkRequest has a unique ID. You can observe its WorkInfo state (ENQUEUED, RUNNING, SUCCEEDED, FAILED, BLOCKED, CANCELLED) via getWorkInfoByIdFlow() or getWorkInfosForUniqueWorkFlow() (Flow APIs available since WorkManager 2.9), or with the equivalent LiveData methods (getWorkInfoByIdLiveData(), getWorkInfosForUniqueWorkLiveData()). In a ViewModel, collect the Flow and expose it as a StateFlow for the UI.
Code Example
// A Worker that uploads logs; doWork() runs off the main thread
class SyncWorker(appContext: Context, params: WorkerParameters)
: Worker(appContext, params) {
override fun doWork(): Result {
// Network/disk work is safe here; no coroutine scope needed
return try {
uploadLogs()
Result.success()
} catch (e: Exception) {
Result.retry() // WorkManager reschedules with backoff
}
}
}
// Enqueue with constraints from a ViewModel or repository
fun scheduleSync(context: Context) {
val constraints = Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.setRequiresBatteryNotLow(true)
.build()
val request = OneTimeWorkRequestBuilder<SyncWorker>()
.setConstraints(constraints)
.setBackoffCriteria(
BackoffPolicy.EXPONENTIAL,
WorkRequest.MIN_BACKOFF_MILLIS,
TimeUnit.MILLISECONDS
)
.build()
WorkManager.getInstance(context).enqueue(request)
}
Best Practices and Common Pitfalls
Do not use WorkManager for immediate or real-time work; use foreground services (with a foreground service type declared on API 34+) or coroutines for tasks the user is waiting on.
Keep workers short and idempotent: WorkManager may run the same worker more than once after a retry, so avoid side effects that corrupt data if repeated.
Pass data via Data, not globals: use workDataOf() in the request and inputData inside doWork().
Avoid deprecated APIs: AsyncTask and IntentService are deprecated (API 30+) — replace them with coroutines and WorkManager.
Long-running work on API 34+ may need a foreground service with an explicit foreground service type; plain workers can be killed under memory pressure.
Test workers with TestWorkerBuilder from androidx.work:work-testing to verify logic without a device.
90
What is the recommended approach for connecting an Android app to external services and third-party SDKs?
KnownRevise
Third-party service integration in Android is done by treating the external service as a boundary of your app: you consume it through a network layer (Retrofit/OkHttp), a vendor SDK, or an intent-based system integration, then expose it to the UI through a repository and a ViewModel with StateFlow. The key rule is that the integration must never leak into composables or activities directly, and all network or disk work must run off the main thread.
REST/HTTP APIs: Use Retrofit with Kotlin suspending functions plus OkHttp for interceptors, timeouts and authenticated headers. This is the default for most services.
Vendor SDKs: Added through Gradle. They usually require manifest entries, an initializer in your Application class or the App Startup library, and often their own ProGuard rules.
System-backed integrations: Sharing, authentication (Credential Manager), billing or file picking use platform contracts and the Activity Result APIs, not raw SDKs.
Background sync: Anything that must run when the app is closed goes through WorkManager, with constraints such as network availability declared on the WorkRequest.
Recommended Architecture
Dependency injection with Hilt: Provide OkHttpClient, Retrofit and each service interface from a @Module installed in SingletonComponent::class. Compose code should never construct a Retrofit instance.
Repository layer: A Repository interface hides the data source, so swapping the provider or adding caching does not touch the ViewModel.
Coroutine threading: Suspend Retrofit functions already run on background threads, but wrap any blocking or disk work in withContext(Dispatchers.IO).
State exposure: The ViewModel publishes a sealed UiState (Loading/Success/Error) through a single StateFlow collected by collectAsStateWithLifecycle() in Compose.
Secrets: API keys belong in buildConfigField or local.properties-driven Gradle config, never in source files or a committed strings.xml.
Dependency management: Use a Gradle version catalog (libs.versions.toml) so Retrofit, OkHttp and SDK versions are declared in one place and shared across modules.
Best Practices and Common Pitfalls
Keep the boundary narrow: Map DTOs to domain models in the repository so UI changes do not ripple into the network layer.
Handle errors explicitly: Catch IOException and HttpException in the ViewModel and surface a user-facing state instead of crashing.
Do not block MainThread: A NetworkOnMainThreadException is a guaranteed crash; long polling or file uploads belong in Dispatchers.IO or a CoroutineWorker.
Use HTTPS and certificate pinning where required: OkHttp supports CertificatePinner for high-security providers; plain HTTP needs a network_security_config exception and should be avoided.
Beware SDK bloat and startup cost: Large SDKs initialized in a custom Application class slow cold start; defer initialization with the App Startup library or lazy injection.
Verify version compatibility: SDKs can break on API 34+ foreground service types or API 33 POST_NOTIFICATIONS requirements; check each provider’s release notes before upgrading.
91
How do you add and apply a custom typeface in an Android application?
KnownRevise
To include a custom font you place the font files in the res/font resource directory, optionally group them into a font family using an XML file, and then reference them from your UI — with FontFamily in Jetpack Compose or the android:fontFamily attribute in XML Views. Android 8.0 (API 26) added native support for this, so no third-party library is required.
Read full answerHide full answer
Applies toAndroid 16 (API 36), Kotlin 2.x, Jetpack Compose with Material 3; the XML approach also applies to XML Views on API 26+ (use the `app:` namespace with the `androidx.core` compat classes on older versions).
How It Works
Add the font files. Copy your .ttf or .otf files into app/src/main/res/font/. File names must be lowercase and contain only letters, numbers and underscores, for example roboto_regular.ttf and roboto_bold.ttf.
Create a font family (optional but recommended). Add an XML file such as res/font/roboto.xml that maps each style and weight to a file. This lets the system pick the correct file when you request bold or italic text instead of synthesising a fake bold.
Reference the font. In Compose, build a FontFamily from the R.font resources and pass it to a Text or to your MaterialTheme typography. In XML Views, use android:fontFamily="@font/roboto" on a TextView (or set it globally in your app theme).
Bundle or download. The steps above bundle the font inside the APK. As an alternative, Android 8.0 (API 26) supports downloadable fonts: the font is fetched at runtime from the Google Fonts provider through the FontsContractCompat API, which keeps the APK smaller but requires network access and adds a runtime dependency.
Key Features
Per-weight mapping: a font family can declare normal, bold, italic and boldItalic entries, so the correct outline file is used for every style.
Compose integration:FontFamily accepts Font(R.font.name, FontWeight.Bold, FontStyle.Italic) entries and integrates with TextStyle and Material 3 Typography.
Programmatic loading: for fonts that are not packaged as resources (for example a file downloaded at runtime), Compose provides FontFamily(Font(File(...))), where the Font constructor accepts the file path.
Global theming: defining the font once in your theme or Typography object keeps text styling consistent across the whole app.
// A FontFamily maps each weight/style to its bundled resource file
val Roboto = FontFamily(
Font(R.font.roboto_regular, FontWeight.Normal),
Font(R.font.roboto_italic, FontWeight.Normal, FontStyle.Italic),
Font(R.font.roboto_bold, FontWeight.Bold),
)
// Apply it directly to a single composable
Text(
text = "Hello, custom font!",
fontFamily = Roboto,
fontWeight = FontWeight.Bold,
)
// Or apply it app-wide through Material 3 typography
val AppTypography = Typography(
headlineLarge = Typography().headlineLarge.copy(fontFamily = Roboto),
bodyLarge = Typography().bodyLarge.copy(fontFamily = Roboto),
)
MaterialTheme(
typography = AppTypography,
content = content,
)
Best Practices
Check the licence of the font before shipping it in your APK; many commercial fonts do not permit embedding in an app.
Declare every weight you actually use. If you omit the bold entry, the system draws a synthetic bold that usually looks poor.
Set the font in one place — your theme or Typography — rather than repeating fontFamily on every Text, so a rebrand only touches one file.
Keep APK size in mind. A single variable font can replace several static files; if size matters and network access is acceptable, evaluate downloadable fonts instead of bundling.
Do not load fonts from disk on the main thread. Reading a downloaded font file is I/O work; do it on Dispatchers.IO or with the async FontsContractCompat request API and cache the result.
92
How can you add content sharing features, such as sharing text or images to other apps, in an Android app?
KnownRevise
The standard approach is to use an implicit intent with Intent.ACTION_SEND, supplying the content (text, images, or other URIs) and an appropriate MIME type, then launch it through a chooser so the user picks the target app. For richer integration you can publish direct-share targets with the ShareTarget API and add your app as a share destination via intent-filter declarations in the manifest.
Read full answerHide full answer
Applies toKotlin 2.x with Jetpack Compose, targeting Android 16 (API 36); direct-share features apply to Android 7.0 (API 24) and later.
How It Works
Text sharing: set the action to Intent.ACTION_SEND, put the string in Intent.EXTRA_TEXT, and set a MIME type such as text/plain.
Media and file sharing: pass a content URI in Intent.EXTRA_STREAM with a MIME type like image/* or application/pdf. The URI must be a content:// URI served by a FileProvider, never a raw file:// URI, which fails with FileUriExposedException on API 24+.
Granting access: add Intent.FLAG_GRANT_READ_URI_PERMISSION so the receiving app can read the shared content.
Chooser: wrap the intent with Intent.createChooser() to let the user select from apps that handle the MIME type, and to expose advanced sharing options.
Key Sharing Options
Single content item: ACTION_SEND with EXTRA_TEXT or EXTRA_STREAM.
Multiple items: ACTION_SEND_MULTIPLE with a list of URIs in EXTRA_STREAM.
Direct share targets: apps can publish dynamic shortcuts that appear at the top of the chooser (API 24+), letting users share directly to a specific contact or conversation. Use the SharingShortcutsManager from the androidx.sharetarget library to publish and update these shortcuts.
Receiving shares: declare an <intent-filter> with action android.intent.action.SEND (or SEND_MULTIPLE), category android.intent.category.DEFAULT, and the android:mimeType values you accept. The legacy ChooserTargetService approach using <meta-data android:name="android.service.chooser.chooser_target_service"> is deprecated in favor of SharingShortcutsManager.
Choosing an Approach
Approach
Best for
Notes
ACTION_SEND + chooser
One-off sharing of text/media
No dependencies, works on all API levels
ShareCompat.IntentBuilder
Same, with a fluent API
Part of AndroidX Core; still supported
ShareTarget / direct share
Messaging/social apps with conversations
Requires API 24+ and shortcut publishing
Third-party SDKs
Platform-specific features (stories, deep content)
Adds size and dependency risk; check current support
Code Example
val context = LocalContext.current
val shareLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.StartActivityForResult()
) { /* optional: handle result if the target app returns one */ }
Button(onClick = {
val shareText = "Check out this article: https://example.com/article"
val imageUri: Uri? = null // e.g. FileProvider.getUriForFile(context, authority, file)
val shareIntent = Intent(Intent.ACTION_SEND).apply {
type = if (imageUri != null) "image/*" else "text/plain"
putExtra(Intent.EXTRA_TEXT, shareText)
imageUri?.let {
putExtra(Intent.EXTRA_STREAM, it)
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
}
}
val chooserIntent = Intent.createChooser(shareIntent, "Share via")
shareLauncher.launch(chooserIntent)
}) {
Text("Share")
}
To share a local file, register a FileProvider in AndroidManifest.xml:
Then obtain a URI with FileProvider.getUriForFile(context, "${applicationId}.fileprovider", file).
Best Practices and Common Pitfalls
Always use a chooser: it avoids hard-coding package names and prevents crashes when the user has no app that handles the intent.
Verify a handler exists: call shareIntent.resolveActivity(packageManager) on an implicit intent before wrapping it in a chooser to avoid ActivityNotFoundException; explicit intents do not need this check.
Prefer content URIs: file:// URIs are rejected from API 24 (Android 7.0) onward.
Keep MIME types specific: image/png is better than image/* when you know the format, since it yields more accurate app suggestions.
Avoid deprecated sharing paths: Intent.EXTRA_HTML_TEXT still works, but sharing via platform-specific deep-link SDKs that rely on deprecated solutions (such as Firebase Dynamic Links, which is shut down) should be replaced with App Links or the SDK vendor’s current recommendation.
Test on real devices: emulator images often lack social apps, so chooser behavior can differ.
93
What critical factors need to be addressed when incorporating payment gateway services into an Android application?
KnownRevise
Integrating payment gateways into Android apps requires prioritizing security and regulatory compliance above all else to protect sensitive financial data and maintain user trust. Developers must ensure cardholder data never touches their own servers directly, relying instead on tokenization provided by the payment provider’s SDK while maintaining a responsive UI architecture.
Payment Card Industry Data Security Standard (PCI DSS) compliance is mandatory. Never collect raw Primary Account Numbers (PAN) or CVV on your own backend. Use the payment gateway’s official SDK (e.g., Stripe, Adyen) to tokenize data client-side. The SDK handles encryption and sends data directly to the gateway, returning a token your app sends to your backend for charge initiation. Ensure all network traffic uses HTTPS with TLS 1.2 or higher; Android blocks cleartext traffic by default since API 28. Never log sensitive transaction details or hardcode API secret keys in the app binary.
Network Architecture and Concurrency
Network operations must never occur on the main thread. Use Retrofit with OkHttp for API calls and Kotlin coroutines with Dispatchers.IO for background processing. A NetworkSecurityConfig should enforce certificate pinning where possible to prevent man-in-the-middle attacks. The repository pattern should abstract data sources, allowing seamless switching between sandbox and production endpoints. Coroutines ensure that payment status checks do not block the UI, allowing animations or progress indicators to remain smooth during transaction processing.
UI State Management
Jetpack Compose should be used for the payment interface, observing state via StateFlow exposed by a ViewModel. This ensures the UI survives configuration changes (like screen rotation) without restarting the payment flow. The ViewModel should hold the transaction state (e.g., Loading, Success, Failed) and expose events via SharedFlow for one-shot actions like showing a toast or navigating to a receipt screen. Avoid storing payment tokens in SharedPreferences; use EncryptedSharedPreferences or the Android Keystore System for any persistent sensitive identifiers.
Error Handling and Lifecycle
Payment flows must handle interruptions gracefully. If the app is backgrounded during a 3D Secure authentication flow, the activity result must be captured using the modern Activity Result APIs rather than the deprecated onActivityResult. Implement retry logic with exponential backoff for transient network failures, but never auto-retry financial transactions without user confirmation to prevent double charging. Handle specific error codes from the gateway (e.g., insufficient funds vs. invalid card) to provide actionable feedback to the user.
Best Practices and Pitfalls
Always test thoroughly in the sandbox environment before switching to production credentials. Use ProGuard or R8 to obfuscate code and protect logic flow. Be mindful of regional regulations like PSD2 in Europe, which requires Strong Customer Authentication (SCA). Do not implement custom cryptographic solutions for payment data; rely on the gateway’s SDK. Finally, ensure your app declares the necessary internet permissions and handles network availability checks gracefully using ConnectivityManager before initiating any transaction.