Android Developer Interview Questions
Core Overview
Practice Kotlin syntax, Jetpack Compose UI architecture, Coroutines/Flow async processing, and Android SDK lifecycles.
Ready to test your knowledge?
Launch a focused practice session to review questions without distraction.
How does Kotlin enforce null safety, and what are platform types in Android development?
Direct Answer
Kotlin enforces null safety at compile time using nullable vs non-nullable types. Platform types are types from Java code where nullability is unspecified, requiring careful developer checks.
Detailed Explanation
Code Example
// Java signature: public String getName() { return null; }
// Kotlin caller:
val name: String? = api.name // Explicitly declaring as nullable is safe
val uppercase = name?.uppercase() // Safe call operator prevents NPE
Common Interview Pitfalls
- Using the double-bang operator (`!!`) on platform types without verifying that they cannot be null.
- Assuming that Kotlin null checks protect against null values returned by JSON parsing libraries reflection instantiations.
Explain the role of CoroutineDispatchers and structured concurrency in Kotlin.
Direct Answer
CoroutineDispatchers specify the thread pool for coroutine execution (Main, IO, Default). Structured concurrency ensures coroutines are launched in scoped contexts to prevent memory leaks.
Detailed Explanation
Code Example
class MyViewModel : ViewModel() {
fun fetchData() {
viewModelScope.launch { // Structured: cancelled on ViewModel clear
val data = withContext(Dispatchers.IO) {
api.downloadData() // Offloaded to background thread
}
uiState.value = data // Main thread
}
}
}
Common Interview Pitfalls
- Launching long-running tasks in GlobalScope (bypasses structured concurrency, leading to memory leaks when components destroy).
- Performing blocking I/O calls directly on Dispatchers.Main, freezing the Android UI thread.
Compare Flow, StateFlow, and SharedFlow in Kotlin Coroutines.
Direct Answer
Flow is cold and active only during collection. StateFlow is hot, retains one state, and triggers updates on change. SharedFlow is hot, broadcasts to multiple subscribers, and lacks state memory.
Detailed Explanation
Code Example
// Hot state emission
private val _state = MutableStateFlow<UiState>(UiState.Loading)
val state: StateFlow<UiState> = _state.asStateFlow()
// Hot event emission
private val _event = MutableSharedFlow<String>()
val event: SharedFlow<String> = _event.asSharedFlow()
Common Interview Pitfalls
- Collecting flows in Compose or lifecycle environments using plain `.collect` instead of `.collectAsStateWithLifecycle` or `repeatOnLifecycle` (causes resources leak in background).
- Using StateFlow for one-time events, leading to event re-delivery when the device is rotated.
What are inline classes and reified type parameters in Kotlin, and how do they optimize runtime performance?
Direct Answer
Inline functions substitute bytecode at the call site. Reified type parameters preserve generic types at runtime, avoiding Java type erasure.
Detailed Explanation
Code Example
// Reified generic navigation helper
inline fun <reified T : Activity> Context.startActivity() {
val intent = Intent(this, T::class.java)
startActivity(intent)
}
// Usage: startActivity<SettingsActivity>()
Common Interview Pitfalls
- Inlining massive functions with large blocks of code (increases the generated APK bytecode size unnecessarily).
- Attempting to use reified type parameters on non-inline functions (will not compile because type preservation requires compiler code generation).
How do extension functions and delegates work in Kotlin?
Direct Answer
Extension functions add methods to existing classes without inheritance. Delegates offload property reads/writes to helper objects.
Detailed Explanation
Code Example
// Extension function
fun View.hide() { this.visibility = View.GONE }
// Lazy delegation
val database: Database by lazy { Database.build(context) }
Common Interview Pitfalls
- Assuming extension functions override member methods with identical signatures (member methods always win).
- Declaring lazy properties that reference lifecycle-bound contexts, causing memory leaks if retained.
How does exception propagation work in Kotlin Coroutines, and how do you handle errors?
Direct Answer
Exceptions propagate up the job hierarchy, cancelling parents and siblings. Use SupervisorJob or supervisorScope to isolate child failures.
Detailed Explanation
Code Example
// If api1 fails, api2 continues running
supervisorScope {
val first = launch { api1.fetch() }
val second = launch { api2.fetch() }
}
Common Interview Pitfalls
- Wrapping async blocks with try-catch blocks expecting to catch exceptions thrown inside child launch blocks (exceptions are propagated through the Coroutine context instead).
- Using a plain Job inside a CoroutineScope expecting supervisor-like failure isolation.
Describe the three phases of the Jetpack Compose rendering lifecycle and how Recomposition works.
Direct Answer
Compose renders UI in three phases: Composition (what to show), Layout (where to place), and Drawing (how to render). Recomposition runs when State changes.
Detailed Explanation
Code Example
@Composable
fun ProfileCard(name: String) { // Smart skip if name is unchanged
Text(text = name) // Reads name state, draws during composition
}
Common Interview Pitfalls
- Performing database queries or network operations directly inside a `@Composable` block (runs on every single recomposition, freezing the app).
- Reading frequently changing state (like scroll offsets) in the Composition phase instead of using lambda-modifiers for Layout/Draw optimization.
What is state hoisting, and how does it support unidirectional data flow in Compose?
Direct Answer
State hoisting is the pattern of moving state up to a component's caller to make it stateless, promoting unidirectional data flow (state flows down, events flow up).
Detailed Explanation
Code Example
@Composable
fun SearchField(query: String, onQueryChange: (String) -> void) {
TextField(value = query, onValueChange = onQueryChange) // Stateless child
}
Common Interview Pitfalls
- Hoisting state unnecessarily high up the UI tree, causing unrelated parent components to recompose constantly.
- Modifying hoisted state directly from nested children without triggering the callback event parameter.
What is the difference between `remember` and `rememberSaveable` in Jetpack Compose?
Direct Answer
`remember` preserves state across recompositions but loses it on configuration changes. `rememberSaveable` preserves state across both using Bundle mechanisms.
Detailed Explanation
Code Example
@Composable
fun InputForm() {
// Survives recomposition, lost on screen rotation
var text1 by remember { mutableStateOf("") }
// Survives both recomposition and screen rotation
var text2 by rememberSaveable { mutableStateOf("") }
}
Common Interview Pitfalls
- Using `rememberSaveable` for complex objects that cannot be serialized into a Bundle without writing a custom Saver.
- Expecting `remember` to persist data indefinitely like a local database storage.
Compare LaunchedEffect, DisposableEffect, and SideEffect in Compose.
Direct Answer
LaunchedEffect runs suspend blocks on keys changes. DisposableEffect executes cleanups on leaving composition. SideEffect runs on every successful recomposition.
Detailed Explanation
Code Example
@Composable
fun Timer(timer: CustomTimer) {
DisposableEffect(timer) {
timer.start()
onDispose { timer.stop() } // Cleanup
}
}
Common Interview Pitfalls
- Using a frequently changing value as a key in `LaunchedEffect`, causing the coroutine to constantly cancel and restart.
- Forgetting to call `onDispose` at the end of a `DisposableEffect` block.
Explain measurement rules in Compose and how custom layouts are created.
Direct Answer
Compose enforces a single-pass measurement rule (children can only be measured once). Custom layouts are built by measuring children and defining their coordinates.
Detailed Explanation
Code Example
@Composable
fun CustomColumn(modifier: Modifier = Modifier, content: @Composable () -> Unit) {
Layout(modifier = modifier, content = content) { measurables, constraints ->
val placeables = measurables.map { it.measure(constraints) }
var yPosition = 0
layout(constraints.maxWidth, constraints.maxHeight) {
placeables.forEach { placeable ->
placeable.placeRelative(x = 0, y = yPosition)
yPosition += placeable.height
}
}
}
}
Common Interview Pitfalls
- Measuring a child twice inside a custom layout, causing a runtime crash due to single-pass enforcement.
- Using SubcomposeLayout for standard static structures (SubcomposeLayout has a high performance overhead because it defers composition until layout phase).
How do LazyColumn and LazyRow optimize memory usage, and how do you define item keys?
Direct Answer
Lazy components compose and layout only the currently visible items on the screen. Explicit keys ensure item identities are preserved during updates.
Detailed Explanation
Code Example
@Composable
fun ItemList(items: List<Product>) {
LazyColumn {
items(items, key = { it.id }) { product ->
ProductRow(product) // Skip recomposing if identity is unchanged
}
}
}
Common Interview Pitfalls
- Using changing indices or class hashCode values as keys, leading to duplicate key errors or memory leaks.
- Placing another scrollable component (like a nested scrollable Column) inside a LazyColumn without specifying height bounds.
Describe Activity and Fragment lifecycle state transitions and where to release resources.
Direct Answer
Activity and Fragment cycles transition from created to resumed. Release heavy UI resources in onDestroy, and listeners in onPause or onStop depending on state.
Detailed Explanation
Code Example
class DetailFragment : Fragment() {
private var _binding: FragmentDetailBinding? = null
private val binding get() = _binding!!
override fun onDestroyView() {
super.onDestroyView()
_binding = null // Prevent View memory leaks
}
}
Common Interview Pitfalls
- Failing to clear view binding references in `onDestroyView` for Fragments, holding the entire view hierarchy in memory when the fragment is in the backstack.
- Registering heavy event listeners in `onResume` but failing to remove them in `onPause`.
What is the difference between Foreground Services and WorkManager in Android?
Direct Answer
Foreground Services execute immediate, user-perceptible background tasks with a persistent notification. WorkManager schedules persistent, deferrable background tasks.
Detailed Explanation
Code Example
val uploadWorkRequest = OneTimeWorkRequestBuilder<UploadWorker>()
.setConstraints(Constraints.Builder()
.setRequiredNetworkType(NetworkType.UNMETERED) // Only Wi-Fi
.setRequiresCharging(true)
.build())
.build()
WorkManager.getInstance(context).enqueue(uploadWorkRequest)
Common Interview Pitfalls
- Using Foreground Services for background sync actions that do not interest the user directly (spams notification drawers unnecessarily).
- Using standard threads or coroutines inside an Activity for long-running synchronization (these terminate instantly if the OS kills the process).
How do you secure Intents and PendingIntents in Android to prevent security vulnerabilities?
Direct Answer
Secure Intents by using explicit declarations for internal components, and secure PendingIntents by setting FLAG_IMMUTABLE flag, preventing parameter hijacking.
Detailed Explanation
Code Example
val intent = Intent(context, SecureActivity::class.java)
val pendingIntent = PendingIntent.getActivity(
context, 0, intent,
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
)
Common Interview Pitfalls
- Creating mutable PendingIntents without defining a target package, allowing malicious third-party apps to hijack the intent contents.
- Forgetting to declare the `android:exported` property explicitly for manifest components in apps targetting Android 12+.
What are the different Context types in Android, and how can they cause memory leaks?
Direct Answer
Application Context lives for the app runtime. Activity Context is short-lived. Storing an Activity Context reference in long-lived singletons causes memory leaks.
Detailed Explanation
Code Example
// Memory Leak Example:
object LeakManager {
private var context: Context? = null
fun init(ctx: Context) {
this.context = ctx // Leaks if ctx is an Activity Context!
}
}
// Fix: Use ctx.applicationContext instead
Common Interview Pitfalls
- Passing an Activity Context directly to database initialize helpers or API clients that live for the duration of the application.
- Holding static references to Views (which implicitly hold references to their parent Activity Context).
What is Scoped Storage in Android, and how does it affect file access?
Direct Answer
Scoped Storage isolates app storage. Apps have read/write access to their private folder and MediaStore without requiring storage permissions.
Detailed Explanation
Code Example
val resolver = context.contentResolver
val contentValues = ContentValues().apply {
put(MediaStore.MediaColumns.DISPLAY_NAME, "photo.jpg")
put(MediaStore.MediaColumns.MIME_TYPE, "image/jpeg")
}
val imageUri = resolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues)
// Write bytes to imageUri using resolver.openOutputStream(imageUri)
Common Interview Pitfalls
- Requesting broad `READ_EXTERNAL_STORAGE` and `WRITE_EXTERNAL_STORAGE` permissions on Android 13+ (these are ignored; you must use specific media permissions instead).
- Using raw file paths (e.g. `/sdcard/`) to write to shared folders, which throws a permission crash in scoped storage.
How do you secure dynamic Broadcast Receivers in Android?
Direct Answer
Secure dynamic Broadcast Receivers by registering them with explicit export flags (RECEIVER_EXPORTED or RECEIVER_NOT_EXPORTED) in Android 13+.
Detailed Explanation
Code Example
val receiver = MyBroadcastReceiver()
val filter = IntentFilter("com.example.ACTION_UPDATE")
context.registerReceiver(
receiver, filter,
Context.RECEIVER_NOT_EXPORTED // Secure: internal app broadcasts only
)
Common Interview Pitfalls
- Registering dynamic broadcast receivers without specifying export flags on Android 13+ devices, causing runtime exceptions.
- Forgetting to unregister dynamically registered receivers in matching lifecycle methods (`onStop` / `onDestroy`), causing context leaks.
Explain Clean Architecture and MVVM patterns in Android development.
Direct Answer
Clean Architecture divides code into layers (Presentation, Domain, Data) with strict dependency rules. MVVM separates UI (View) from state/logic (ViewModel).
Detailed Explanation
Code Example
// Domain Use Case (Pure Kotlin, no Android dependencies)
class GetUserUseCase(private val repository: UserRepository) {
suspend operator fun invoke(id: String): UserResult = repository.getUser(id)
}
Common Interview Pitfalls
- Importing Android framework classes (like `android.view.View` or context) into the Domain Layer, breaking clean architecture isolation.
- Writing database transactions or network requests directly inside ViewModels instead of delegating to repositories.
How do ViewModels survive configuration changes, and how does SavedStateHandle help with process death?
Direct Answer
ViewModels are cached in the ViewModelStoreOwner across configuration changes. SavedStateHandle persists data during OS background process termination.
Detailed Explanation
Code Example
class UserViewModel(private val savedState: SavedStateHandle) : ViewModel() {
// SavedStateHandle automatically persists and restores this query value
val searchQuery = savedState.getStateFlow("query", "")
fun setQuery(q: String) {
savedState["query"] = q
}
}
Common Interview Pitfalls
- Passing Activity references, Contexts, or views into ViewModels (causes severe memory leaks on rotation because the ViewModel outlives the Activity).
- Storing massive payloads or images in SavedStateHandle (Bundle size is limited to 1MB; excessive size causes TransactionTooLargeExceptions).
Compare Hilt and Dagger2 for dependency injection in Android applications.
Direct Answer
Dagger2 is a compile-time dependency injection framework requiring custom setups. Hilt builds on top of Dagger2, simplifying integration with predefined scopes and Android components.
Detailed Explanation
Code Example
@HiltAndroidApp // Bootstraps Hilt in Application class
class MyApplication : Application()
@AndroidEntryPoint // Enables injection in Activity
class MainActivity : ComponentActivity() {
@Inject lateinit var analytics: AnalyticsTracker
}
Common Interview Pitfalls
- Declaring dependencies inside `@InstallIn(ActivityComponent::class)` and attempting to inject them into ViewModels (ViewModels outlive Activities; dependencies must be scoped to ViewModelComponent or SingletonComponent instead).
- Forgetting to add `@Inject constructor()` on dependency classes, preventing Hilt from resolving class instantiation.
How do you design an offline-first architecture in Android using Room and Retrofit?
Direct Answer
Design a single source of truth repository. The UI observes database updates via Room (Flows). Retrofit updates the database in the background, updating the UI.
Detailed Explanation
Code Example
class UserRepository(private val userDao: UserDao, private val api: UserApi) {
// Flow emission from SQLite Room database acts as single source
val userProfile: Flow<User> = userDao.observeUser()
suspend fun refreshUser() {
val networkUser = api.fetchUser()
userDao.insert(networkUser) // Triggers automatic Flow updates to UI
}
}
Common Interview Pitfalls
- Updating the local database without checking for write conflicts, leading to data synchronization inconsistencies.
- Performing database writes or reads on the Main Thread (Room checks this and throws an IllegalStateException; always execute queries on dispatcher threads).
How does Jetpack Navigation Component handle backstack management and deep links?
Direct Answer
Jetpack Navigation manages screen transactions using nav graphs. It handles deep links by automatically parsing intent data and rebuilding the backstack.
Detailed Explanation
Code Example
composable(
route = "details/{id}",
deepLinks = listOf(navDeepLink { uriPattern = "https://example.com/details/{id}" })
) { backStackEntry ->
val id = backStackEntry.arguments?.getString("id")
DetailScreen(id)
}
Common Interview Pitfalls
- Failing to specify argument types in deep link configurations, causing route parsing validation crashes.
- Re-creating the NavHostController on every recomposition instead of hoisting it to the top-level parent wrapper.
What are the benefits and patterns of a multi-module project structure in Android?
Direct Answer
Multi-module structures partition applications into Gradle subprojects (feature, core, app). This decreases build times, improves encapsulation, and isolates teams.
Detailed Explanation
Code Example
// Gradle feature module build.gradle.kts
dependencies {
implementation(project(":core:network"))
implementation(project(":core:designsystem"))
}
Common Interview Pitfalls
- Creating circular dependencies between feature modules (e.g. `:feature:login` depending on `:feature:profile` and vice versa; solve by creating shared modules or interfaces).
- Declaring API dependency versions independently across modules, causing version conflict errors during runtime aggregation.
Compare Espresso UI testing with Robolectric tests in Android.
Direct Answer
Espresso tests are instrumented tests running on a real device/emulator. Robolectric tests are local unit tests that simulate the Android sandbox on the JVM.
Detailed Explanation
Code Example
// Robolectric test running on local JVM
@RunWith(RobolectricTestRunner::class)
@Config(sdk = [33])
class MyActivityTest {
@Test
fun testClick() {
val controller = Robolectric.buildActivity(MyActivity::class.java).setup()
val activity = controller.get()
activity.findViewById<Button>(R.id.btn_submit).performClick()
}
}
Common Interview Pitfalls
- Using Espresso tests for simple presenter or ViewModel unit tests, slowing down the build CI/CD pipeline.
- Assuming Robolectric shadow behavior matches physical GPU layout sizing checks (cannot test layout overlaps or pixel measurements).
How do you unit test a ViewModel that exposes StateFlow data streams using JUnit?
Direct Answer
Mock dependencies, set up a custom Main Coroutine Dispatcher in JUnit, and collect/assert emitted StateFlow values.
Detailed Explanation
Code Example
@OptIn(ExperimentalCoroutinesApi::class)
class MainViewModelTest {
private val testDispatcher = StandardTestDispatcher()
@Before
fun setUp() {
Dispatchers.setMain(testDispatcher) // Set Main dispatcher to test runner
}
@Test
fun testLoadingState() = runTest {
val viewModel = MainViewModel(mockRepo)
val state = viewModel.uiState.value
Assert.assertEquals(UiState.Loading, state)
}
}
Common Interview Pitfalls
- Failing to call `Dispatchers.setMain` in the test setup, causing a "Module with the Main dispatcher had failed to initialize" crash.
- Calling `viewModel.uiState.collect { ... }` directly in the test without scoping it inside a coroutine launch, freezing the test runner.
How does LeakCanary detect memory leaks in Android applications?
Direct Answer
LeakCanary observes object lifecycles. It uses weak references to verify objects are garbage collected after destruction, generating a heap dump trace on failure.
Detailed Explanation
Code Example
// Add dependency to build.gradle (no code initialization needed, Hilt automatically loads it on debug builds)
debugImplementation("com.squareup.leakcanary:leakcanary-android:2.12")
Common Interview Pitfalls
- Shipping LeakCanary implementation in release production builds (it consumes high memory and causes performance lag during heap dumps; always use `debugImplementation`).
- Ignoring LeakCanary notifications in debug builds, allowing leaks to propagate to production environments.
How do you profile app startup times and layout performance in Android using Systrace and Android Profiler?
Direct Answer
Use Android Profiler to monitor CPU/Memory allocation in real-time. Use Systrace/Macrobenchmark to capture detailed frame drops and trace system-level layout costs.
Detailed Explanation
Code Example
// Trace custom execution block programmatically
import androidx.tracing.trace
fun loadAssets() {
trace("AssetLoadTrace") {
// Business logic monitored in Systrace
processHeavyAssets()
}
}
Common Interview Pitfalls
- Profiling applications in debug builds (debug builds add runtime log wrappers and disable R8 optimization, yielding inaccurate performance metrics; always profile in release-like builds with proguard enabled).
- Analyzing average frame rates instead of focusing on 99th percentile frame drop spikes (jank is perceived as spikes, not averages).
What techniques reduce APK download sizes in Android applications?
Direct Answer
Use Android App Bundles (AAB), enable resource and code shrinking (R8), convert images to WebP format, and remove unused resources.
Detailed Explanation
Code Example
// build.gradle.kts release build configuration:
buildTypes {
getByName("release") {
isMinifyEnabled = true // Strip dead code
isShrinkResources = true // Strip dead resources
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
}
}
Common Interview Pitfalls
- Manually compressing image files while keeping duplicate assets for different screen density buckets (e.g. hdpi, xhdpi) instead of using vectors.
- Failing to verify R8 reflections rules, causing runtime crashes on dynamic class loading.
How do Proguard and R8 optimize and obfuscate code, and how do you write keep rules?
Direct Answer
R8 performs compile-time code shrinking and optimization. Obfuscation renames classes and methods to single letters. Keep rules preserve reflection targets.
Detailed Explanation
Code Example
# Keep rule: preserve all fields in serialize model package
-keepclassmembers class com.example.models.** {
@com.google.gson.annotations.SerializedName <fields>;
}
Common Interview Pitfalls
- Writing over-broad keep rules (like `-keep class com.example.** { *; }`), which disables optimization for the entire package and increases APK size.
- Forgetting to upload the mapping file (`mapping.txt`) to Google Play Console, preventing the de-obfuscation of production crash stack traces.
Official Documentation & Specifications
Kotlin Language & Coroutines
Jetpack Compose UI
Android SDK & Lifecycles
Architecture & State Management
Want to tailer your resume for Android Developer roles?
Import your resume, scan it for critical Android Developer keywords, and compare it against ATS standards instantly.