Android and Kotlin Interview Questions 2026

Android interviews today are really two interviews stitched together, Kotlin the language, and Android the platform, and a lot of candidates are noticeably stronger at one than the other. You can write clean Kotlin and still fumble a question about why a ViewModel survives rotation but not process death, or you can know the Activity lifecycle cold and still not be able to explain what a coroutine's structured concurrency actually buys you over a plain callback.

This page tries to cover both halves properly, Kotlin fundamentals, null safety, and coroutines first, then the Android-specific stuff, lifecycle, Jetpack Compose, architecture components, and enough about memory and performance to talk through a real production bug, not just a textbook definition.

Why Android Interviews Test Both the Language and the Platform Separately

Kotlin is genuinely a different language from Java, not just Java with nicer syntax, and Android itself is a platform with its own lifecycle rules, memory constraints, and architectural conventions that don't exist in a typical backend or web interview. Interviewers ask Android/Kotlin questions to check:

  • Whether you actually understand Kotlin's null safety and coroutines, or you're writing Kotlin like it's Java with different keywords
  • Whether you understand the Activity/Fragment lifecycle well enough to avoid leaking a context or losing state on rotation
  • Whether you know the difference between the old View system and Jetpack Compose, and why Google pushed the ecosystem that direction
  • Whether you've actually debugged a real memory leak or ANR, not just heard the terms

Who This Page Is For

  • Android developers interviewing for roles that specifically mention Kotlin or Jetpack Compose
  • Java developers transitioning into Android and getting comfortable with Kotlin-specific idioms
  • Freshers targeting mobile development roles who've built apps but never had to explain the lifecycle or memory model out loud

Android and Kotlin Interview Questions and Answers (2026)

Kotlin Fundamentals

1. What's the actual difference between Kotlin and Java, and why did Google make Kotlin the preferred language for Android instead of just sticking with Java?

Kotlin is interoperable with Java, both compile to JVM bytecode and can call into each other, but Kotlin adds a lot Java historically lacked, null safety built into the type system, data classes, coroutines for async code, extension functions, and generally more concise syntax for common patterns that took noticeably more boilerplate in Java. Google made it the preferred language in 2019 mainly because of that null safety story, a huge share of Android crashes historically came from null pointer exceptions, and because coroutines gave a much cleaner answer to Android's async and threading problems than the callback-heavy patterns Java code tended to fall into.

2. What's the difference between val and var, and why does Kotlin push you toward val by default?

val declares a read-only reference, once assigned, it can't be reassigned to point at something else (though if it's a mutable object, the object's own contents can still change). var declares a regular, reassignable variable. Kotlin nudges you toward val by default because immutability makes code easier to reason about, fewer places where a value can unexpectedly change out from under you, which matters even more once coroutines and multiple threads are involved, a lot of style guides and linters will actually flag a var that's never reassigned and suggest converting it to val.

3. What's the difference between a Kotlin data class and a regular class? What does the compiler actually generate for you?

A data class is specifically meant to hold data, and the compiler automatically generates equals(), hashCode(), toString(), and a copy() function based on the properties declared in the primary constructor, all things you'd otherwise have to write by hand in a regular class. This matters in practice for things like comparing two objects for equality (a regular class without overriding equals compares by reference, not content) and for making a modified copy of an immutable object without mutating the original, user.copy(name = "New Name").

4. What's the difference between an object declaration and a companion object in Kotlin?

An object declaration creates a singleton, a class with exactly one instance, created lazily the first time it's accessed, useful for things like a shared utility or a single app-wide configuration holder. A companion object is defined inside a class and is tied to that specific class, it's Kotlin's replacement for Java's static members, letting you have functions and properties associated with the class itself rather than any particular instance, while still technically being a real object that can implement interfaces, unlike Java's static members.

5. What's the difference between an interface with a default implementation and an abstract class in Kotlin?

An interface (which in Kotlin can include default method implementations, not just signatures) can be implemented by any number of classes, and a class can implement multiple interfaces at once. An abstract class can hold actual constructor logic and state (fields with real values, not just method defaults), and a class can only extend one abstract class. The same general OOP guidance applies as most languages, interfaces for defining a contract multiple unrelated classes might fulfill, abstract classes when there's genuinely shared state or implementation to inherit, not just a shared shape.

6. What are Kotlin extension functions, and how do they actually work under the hood since Kotlin can't truly add methods to a class it doesn't own?

An extension function lets you call someString.myCustomFunction() as if myCustomFunction were a real member of the String class, even though you didn't modify String's actual source code, which is especially useful for adding convenience methods to classes you don't control, like Android SDK or Java standard library classes. Under the hood, it's syntactic sugar, the compiler actually generates a regular static function that takes the "receiver" object as its first parameter, myCustomFunction(someString), and rewrites the call site to look like a method call, no real modification of the original class happens at runtime at all.


Kotlin Null Safety, Functions and Collections

1. How does Kotlin's null safety actually work, and what's the difference between String and String??

Kotlin's type system distinguishes nullable and non-nullable types at compile time. String means the variable can never hold null, the compiler simply won't let you assign null to it or pass null where a plain String is expected, and it won't let you call methods on it without that safety being guaranteed some other way. String? explicitly allows null, and the compiler forces you to handle that possibility, through a null check, a safe call, or an explicit assertion, before you can freely call methods on it, this compile-time enforcement is what eliminates a huge share of the null pointer exceptions that plague equivalent Java code, where every reference type is implicitly nullable by default.

2. What's the difference between the safe call operator (?.), the Elvis operator (?:), and the not-null assertion operator (!!)?

The safe call operator (?.) calls a method or accesses a property only if the receiver isn't null, returning null instead of crashing if it is, letting you chain calls safely, user?.address?.city. The Elvis operator (?:) provides a fallback value to use when the expression on its left is null, val name = user?.name ?: "Unknown". The not-null assertion (!!) forcibly tells the compiler "trust me, this isn't null," and if you're wrong, it throws a NullPointerException immediately at that line, it's the one operator that reintroduces the exact crash Kotlin's null safety was designed to prevent.

3. Why is using !! considered risky, if the whole point of Kotlin's type system is to prevent null pointer exceptions?

Because !! is an explicit escape hatch that turns off the compiler's null checking for that one expression, you're personally guaranteeing it's not null, and if that guarantee turns out to be wrong at runtime, you get exactly the same crash Kotlin's entire null safety system exists to prevent, just deferred to a specific point you marked yourself. It's not that !! is never appropriate, sometimes you genuinely know more than the compiler can prove, but reaching for it as a lazy way to silence a compiler warning instead of actually handling the null case properly is exactly the pattern that reintroduces Java-style NPE bugs into otherwise "null-safe" Kotlin code.

4. What's the difference between a lambda and a higher-order function in Kotlin?

A lambda is an anonymous, inline block of code you can pass around as a value, { x -> x * 2 }. A higher-order function is a function that either accepts a lambda (or any function) as a parameter, or returns one, fun applyTwice(x: Int, op: (Int) -> Int): Int. They're closely related concepts but distinct, a lambda is the value itself, a higher-order function is what makes passing that value around as an argument or return type actually possible and useful.

5. What's the difference between map, filter, and flatMap on a Kotlin collection?

map transforms every element in a collection into something else, producing a new collection of the same size, one output per input. filter keeps only the elements matching a given condition, producing a new, possibly smaller collection, no transformation of the kept elements themselves. flatMap transforms each element into its own collection, and then flattens all of those resulting collections into a single, combined list, useful when each item maps to zero, one, or many results rather than exactly one, like turning a list of users into a single flat list of all their individual orders.

6. What's the difference between a List and a MutableList in Kotlin, given both ultimately just wrap a Java ArrayList at runtime?

At runtime, they're often genuinely the same underlying object, Kotlin's distinction is purely a compile-time, interface-level one, List only exposes read operations (no add, remove, etc.), while MutableList extends it with those mutating operations. This means you can have a variable typed as List that happens to point at the exact same mutable list object, but through that read-only reference, the compiler simply won't let you call anything that would mutate it, it's an API-level immutability guarantee, not a deep, runtime-enforced one, someone holding a MutableList reference to the same underlying object can still mutate it, and that change would be visible through the List-typed reference too.


Kotlin Coroutines and Asynchronous Programming

1. What is a coroutine, and how is it actually different from a thread?

A coroutine is a lightweight unit of concurrency that can be suspended and resumed without blocking the underlying thread it's running on, thousands of coroutines can run on just a handful of actual OS threads, since a suspended coroutine doesn't hold a thread hostage while it waits. A thread is a genuine OS-level construct, comparatively expensive to create, and a blocked thread sits idle, unable to do other work, until whatever it's blocked on completes. Coroutines let you write code that looks sequential and synchronous, but under the hood, suspension points let the thread go do other useful work instead of blocking, which is exactly the efficiency gain that makes them so central to modern Android's async story.

2. What's the difference between launch and async when starting a coroutine?

launch starts a coroutine and returns a Job, used when you don't need a result back, "fire and forget" in a structured way, if it throws an exception, that exception propagates immediately. async starts a coroutine and returns a Deferred<T>, used when you do need a result, you call .await() on it later to get that result (suspending until it's ready), and importantly, an exception thrown inside an async block is held until you actually call .await() on it, rather than propagating immediately the way launch's does.

viewModelScope.launch {
    val result = async { fetchUserData() }
    val user = result.await()
    updateUI(user)
}

3. What is a CoroutineScope, and why does launching a coroutine without one lead to leaks?

A CoroutineScope defines a lifecycle boundary for coroutines, when the scope is cancelled, every coroutine launched within it is cancelled too, automatically. Android provides scopes tied to real lifecycle owners, viewModelScope (cancelled when the ViewModel is cleared), lifecycleScope (tied to a Fragment/Activity's lifecycle), specifically so a coroutine doing background work doesn't keep running (and potentially holding references to now-destroyed UI) after the screen it belongs to is gone, launching a coroutine on a global, unscoped context (like GlobalScope) means nothing automatically cancels it when the associated UI goes away, which is exactly how you end up leaking work, and sometimes leaking the Activity/Fragment itself through captured references.

4. What's the difference between Dispatchers.Main, Dispatchers.IO, and Dispatchers.Default, and how do you decide which to use?

Dispatchers.Main runs on the Android UI thread, required for anything that touches views directly. Dispatchers.IO is optimized for blocking I/O work, network calls, disk reads/writes, database queries, backed by a larger pool of threads since I/O work spends most of its time waiting, not actually using CPU. Dispatchers.Default is optimized for CPU-intensive work, sorting a large list, parsing JSON, image processing, backed by a thread pool sized to the number of CPU cores. The rule of thumb: do the actual work on IO or Default depending on whether it's I/O-bound or CPU-bound, and switch back to Main only for the final step of actually updating the UI with the result.

5. What's the difference between suspend functions and regular functions? What does the suspend keyword actually change about how the function compiles?

Marking a function suspend tells the compiler this function can pause its execution at certain points (wherever it calls another suspend function) without blocking the underlying thread, and it can only be called from another coroutine or suspend function, not from regular, ordinary code. Under the hood, the compiler transforms a suspend function using something called Continuation-Passing Style, effectively rewriting it into a state machine that can be paused and resumed, this is genuinely complex machinery, but from the developer's side, you get to write what looks like simple, sequential, blocking-style code that actually behaves asynchronously.

6. What is structured concurrency, and why does cancelling a parent coroutine automatically cancel its children?

Structured concurrency means every coroutine is launched within a specific scope, forming a parent-child relationship, and a parent coroutine won't complete until all of its children have completed too, and if the parent is cancelled, that cancellation automatically propagates down to every child coroutine as well. This exists specifically to prevent orphaned, leaked coroutines, without structured concurrency, it's very easy to fire off async work and lose track of it entirely, with structured concurrency, cancelling one scope (like when a screen is destroyed) reliably cleans up absolutely everything that was launched within it, no manual bookkeeping required.


Android Fundamentals: Activities, Fragments and Lifecycle

1. What is an Activity, and how is it different from a Fragment?

An Activity represents a single, focused screen with a user interface, it's a genuine Android system-level component with its own entry in the system's back stack and its own lifecycle managed directly by the OS. A Fragment represents a reusable, modular portion of UI and behavior that must be hosted inside an Activity (or another Fragment), it has its own lifecycle too, but that lifecycle is driven by, and layered on top of, its host Activity's lifecycle, Fragments were introduced specifically to let you compose and reuse UI pieces flexibly across different screen sizes and configurations, which a single monolithic Activity per screen didn't handle well.

2. Walk through the Activity lifecycle, what actually triggers onPause versus onStop versus onDestroy?

onCreate runs once, when the Activity is first created. onStart/onResume run as it becomes visible and then interactive. onPause triggers when the Activity is still partially visible but losing focus, for example another Activity appearing as a translucent overlay or dialog on top of it, it's meant for quick, lightweight cleanup since the Activity might resume immediately. onStop triggers when the Activity is no longer visible at all, the user navigated fully away to a different screen, it's a good place for heavier cleanup since you know you're not visible anymore, though the Activity might still come back via onRestart. onDestroy triggers when the Activity is being permanently removed, either the user finished it, the system reclaimed its memory, or a configuration change is destroying and recreating it, this is where final cleanup should happen, though onDestroy isn't always guaranteed to run in every possible circumstance (like the process being killed abruptly).

3. What's the difference between onSaveInstanceState and ViewModel for surviving a configuration change like screen rotation?

onSaveInstanceState lets you save small amounts of UI state (a Bundle) that survives the Activity being destroyed and recreated, including in the more severe case of the entire process being killed by the system and later restored, but it's meant for lightweight, serializable data, not large objects or things like in-flight network calls. ViewModel survives a configuration change (like rotation) by design, the same ViewModel instance is retained across the Activity being destroyed and recreated, but critically, it does not survive actual process death, if Android kills your app's process entirely (say, due to memory pressure while it's in the background), the ViewModel is gone too, which is exactly why the two are often used together, ViewModel for surviving rotation cheaply, onSaveInstanceState (or, more commonly now, SavedStateHandle combined with ViewModel) for surviving the more severe process-death case.

4. What is a Fragment's view lifecycle, and why is it actually separate from the Fragment's own lifecycle?

A Fragment's own lifecycle roughly parallels an Activity's, but its view (the actual inflated layout) can be destroyed and recreated independently, for example when a Fragment is placed on the back stack, its view is destroyed while the Fragment instance itself stays alive in memory, only to have its view recreated later when the user navigates back to it. This distinction matters practically because holding a reference to a view (say, in a variable set in onCreateView) without clearing it when the view is destroyed (onDestroyView) is a classic Fragment memory leak, the Fragment object survives, but you're now holding onto a view that's supposed to be gone, keeping the entire old view hierarchy alive in memory unnecessarily.

5. What's the difference between an implicit intent and an explicit intent?

An explicit intent directly names the exact component (a specific Activity or Service class) that should handle it, used when you know precisely what you want to start, typically within your own app. An implicit intent instead declares an action and some data, without naming a specific component, and lets the Android system figure out which app (or apps, prompting the user to choose) can actually handle that request, sharing a piece of text, opening a URL in a browser, taking a photo, this is the mechanism that lets different apps interoperate without knowing about each other's specific class names ahead of time.


Android UI: Views and Jetpack Compose

1. What's the difference between the traditional View/XML system and Jetpack Compose, and why did Google build Compose instead of just improving the View system?

The traditional system describes UI declaratively in XML layout files, but updating that UI imperatively in code, manually finding views by ID and mutating their properties, which gets increasingly hard to keep in sync as an app's UI state grows more complex. Jetpack Compose is a fully declarative UI toolkit written entirely in Kotlin, you describe what the UI should look like for a given state, and Compose itself handles figuring out what actually needs to change on screen when that state changes, no manual view lookups or mutation. Google built Compose from scratch rather than incrementally improving the View system because the View system's imperative, ID-based architecture was fundamentally at odds with the declarative, state-driven UI patterns that had already proven successful elsewhere (React on the web, SwiftUI on iOS).

2. What is recomposition in Jetpack Compose, and how does Compose actually decide which part of the UI needs to redraw?

Recomposition is Compose re-executing a composable function (or just the parts of it that actually need to change) in response to a change in some state it reads. Compose is smart about scope, it tracks exactly which state each composable actually reads during composition, and when that specific state changes, only the composables that genuinely depend on it are scheduled for recomposition, not the entire UI tree, which is why Compose can be efficient even with frequent state updates, it's not blindly redrawing everything on every change, it's precisely targeting just the affected parts.

3. What's the difference between remember and rememberSaveable in Compose?

remember caches a value across recompositions, so it isn't recalculated every single time the composable recomposes, but that cached value is lost entirely if the Activity is destroyed and recreated, like on a configuration change. rememberSaveable does the same recomposition-surviving caching, but additionally saves that value into the Android Bundle savedInstanceState mechanism, so it survives configuration changes (and process death recreation) too, similar in spirit to how onSaveInstanceState worked in the View system, you'd use rememberSaveable for state you genuinely want to survive rotation, like the current text in a search box, and plain remember for more ephemeral state that's fine being reset.

4. What is state hoisting in Compose, and why is it considered a best practice?

State hoisting means moving a composable's state up to its caller instead of keeping it internal to the composable itself, the composable becomes "stateless," receiving its current value and an event callback (like onValueChange) as parameters, rather than managing its own mutable state directly. This is considered a best practice because it makes composables more reusable and testable, a stateless composable doesn't care where its state actually lives or how it's persisted, it just displays whatever value it's given and reports events upward, which also naturally centralizes state management in a single, more predictable place (often a ViewModel) rather than scattering it across many individual composables.

5. What's the difference between a stateless and a stateful composable?

A stateful composable manages its own internal state directly, using remember internally, which makes it simple to use with no setup, but harder to control or observe from outside, and generally harder to reuse in a different context that needs different state-handling logic. A stateless composable holds no internal state of its own at all, it purely receives data and event callbacks as parameters and renders based on them, pushing all the actual state management responsibility up to whoever is calling it, most real Compose codebases prefer building stateless composables and hoisting state up to a smart, stateful wrapper (often backed by a ViewModel) rather than mixing state management into every low-level UI piece.


Android Architecture Components

1. What is a ViewModel, and how does it actually survive a configuration change when the Activity hosting it gets destroyed and recreated?

A ViewModel is designed to hold and manage UI-related data in a way that survives configuration changes, so a screen rotation doesn't force you to re-fetch data or lose in-progress state. It survives because it isn't actually owned by the Activity instance that gets destroyed and recreated, it's retained by the Android framework's ViewModelStore, which is itself tied to a more stable underlying component that persists across the specific Activity instance being torn down and rebuilt, so when the new Activity instance asks for its ViewModel again after recreation, the framework hands back the exact same, already-existing instance instead of creating a fresh one.

2. What's the difference between LiveData and Kotlin's StateFlow for exposing UI state from a ViewModel?

LiveData is lifecycle-aware out of the box, it automatically stops delivering updates to an observer whose lifecycle (Activity/Fragment) isn't in an active state, which historically made it a natural fit for Android UI specifically. StateFlow is part of Kotlin coroutines/Flow, not Android-specific at all, it's not inherently lifecycle-aware on its own, but it integrates cleanly with coroutines and offers more powerful operators (map, combine, and so on) for transforming a stream of state, and it's become the more commonly recommended choice in modern Android architecture guidance, typically paired with repeatOnLifecycle in the UI layer to get equivalent lifecycle-safety to what LiveData gave you automatically.

3. What is the Repository pattern in Android's recommended architecture, and what problem does it solve?

A Repository sits between your ViewModel and your actual data sources (a remote API, a local Room database, a cache), presenting a single, clean interface for "get this data" without the ViewModel needing to know or care whether that data is coming from the network, a local database, or some combination of both. This solves the problem of ViewModels becoming bloated with data-fetching logic and tightly coupled to specific data source implementations, if you later change how data is fetched or cached, you only need to update the Repository, not every ViewModel that consumes that data.

4. What is Room, and what does it actually generate for you compared to writing raw SQLite queries by hand?

Room is Android's official persistence library, a wrapper around SQLite that lets you define your database schema through annotated Kotlin data classes and DAO (Data Access Object) interfaces with declared query methods, rather than writing raw SQL string queries and manually parsing Cursor results by hand. At compile time, Room generates the actual implementation code for those DAOs, including compile-time verification of your SQL syntax against your declared entities, catching a mismatched column name or invalid query as a build error instead of a runtime crash, which raw SQLite usage can't give you.

5. What's the difference between a ViewModel surviving configuration changes and it surviving process death? These are genuinely two different things people conflate.

Surviving a configuration change (like rotation) means the exact same ViewModel instance, with all its current in-memory state, is handed back to the recreated Activity, nothing was actually destroyed at the process level, the OS is just tearing down and rebuilding the UI layer while your app's process keeps running the whole time. Surviving process death is a completely different, more severe scenario, the OS killed your entire app process (commonly while it's in the background and the system needs memory), and when the user returns, Android restarts your app's process from scratch, which means every ViewModel instance is genuinely gone, recreated fresh, this is exactly why SavedStateHandle exists, to let a ViewModel recover a small amount of critical state even after this more severe kind of "death," which plain in-memory ViewModel retention can't help with at all.


Android Networking and Data Persistence

1. What's the difference between Retrofit and using raw HttpURLConnection or OkHttp directly?

OkHttp is a lower-level HTTP client, it handles the actual network request/response mechanics well, but you're still responsible for manually building requests, parsing raw response bodies, and converting JSON to and from your data classes yourself. Retrofit is built on top of OkHttp and adds a much higher-level, declarative layer, you define an interface describing your API endpoints as regular Kotlin function signatures with annotations, and Retrofit generates the actual implementation, handling request building, execution, and automatic JSON (de)serialization (typically via a converter like Moshi or kotlinx.serialization) for you, dramatically less boilerplate for the common case of talking to a REST API.

2. What's the difference between SharedPreferences and DataStore? Why did Google recommend moving away from SharedPreferences?

SharedPreferences provides simple key-value storage, but its API is synchronous (which can block the calling thread, including potentially the UI thread) and it has no built-in support for handling errors or data consistency issues gracefully, plus its apply()/commit() distinction has historically confused a lot of developers about actual write guarantees. DataStore is the modern replacement, built entirely around Kotlin coroutines and Flow, so reads and writes are properly asynchronous and don't risk blocking the main thread, and it comes in two flavors, Preferences DataStore (still simple key-value, but done right) and Proto DataStore (fully typed, schema-defined storage using Protocol Buffers), which gives you real type safety that SharedPreferences never offered.

3. How would you handle an API call that needs to survive the user rotating their screen or navigating away and back?

You'd trigger the network call from within the ViewModel, scoped to viewModelScope rather than directly from the Activity/Fragment, since the ViewModel (and therefore the coroutine's scope) survives a configuration change even though the UI layer observing it gets destroyed and recreated. The in-flight coroutine just keeps running through the rotation entirely unaffected, and once it completes, it updates a StateFlow/LiveData that the newly recreated UI simply re-subscribes to and immediately receives the latest (or eventually-arriving) value from, no need to restart or duplicate the network call at all.

4. What's the difference between a ContentProvider and just using Room directly? When do you actually need a ContentProvider?

Room (and direct database access generally) is scoped to your own app's private storage, other apps can't access it directly. A ContentProvider is specifically Android's mechanism for exposing structured data across app boundaries, in a controlled, permission-gated way, so a different app entirely (or a system component like the system Contacts app) can query, insert, or observe your data through a well-defined, standard interface. You genuinely need a ContentProvider specifically when you're building something meant to share data across process/app boundaries, for most apps that just need their own private local storage, Room alone is the right tool and a ContentProvider would be unnecessary overhead.


Android App Performance and Memory Management

1. What causes a memory leak in Android specifically, and how does holding a reference to an Activity context cause one?

A memory leak in Android happens when something holds a reference to an object (commonly an Activity or its Context) longer than that object's actual lifecycle, preventing the garbage collector from reclaiming it even after it's supposed to be destroyed. A classic example is a long-lived singleton, or a background task/listener that outlives the screen, holding a reference to an Activity's context, if the Activity is destroyed (say, on rotation) but that background object still references it, the entire Activity, and everything it in turn references, its views, its whole object graph, stays pinned in memory, unable to be collected, even though the user has long since left that screen.

2. What is the difference between Application context and Activity context, and why does using the wrong one for the wrong thing cause leaks?

Application context is tied to the lifetime of the entire app process, it exists for as long as the app is running and isn't tied to any specific screen. Activity context is tied specifically to that one Activity instance's lifecycle, meant to be destroyed along with it. Using an Activity context where you actually needed a longer-lived reference, say, passing it into a singleton or a static field that outlives the Activity, is exactly the mistake that causes the leak described above, the fix is generally to use Application context for anything genuinely long-lived that doesn't need to be tied to a specific screen, and to be deliberate about never letting an Activity context escape into something with a longer lifetime than the Activity itself.

3. What is ANR (Application Not Responding), and what actually triggers it?

An ANR is Android's mechanism for detecting when an app's main (UI) thread has been blocked for too long, roughly 5 seconds for most cases, and the system shows the user a dialog offering to close the unresponsive app. It's triggered by doing heavy, blocking work directly on the main thread, a large synchronous network call, a slow database query, heavy computation, anything that prevents the main thread from processing the next UI event or input in a timely manner, which is exactly why offloading I/O and CPU-heavy work to a background dispatcher (via coroutines) instead of doing it directly on the main thread is such a core Android performance principle.

4. How would you diagnose a janky, laggy UI in an Android app? What tools would you reach for?

Android Studio's Profiler is the main tool, it lets you inspect CPU usage, memory allocation, and network activity in real time while the app runs, which can reveal whether jank is coming from heavy work happening on the main thread, from excessive object allocation triggering frequent garbage collection pauses, or from something else entirely. Layout Inspector helps identify unnecessarily deep or complex view hierarchies that take too long to measure and lay out on every frame. For Compose specifically, the Layout Inspector and Compose-specific recomposition counts can reveal composables recomposing far more often than they actually need to, which is a common, less obvious source of jank in Compose-based UIs.

5. What's the difference between a weak reference and a strong reference, and how does that relate to preventing memory leaks in Android?

A strong reference is a normal reference, as long as it exists, the garbage collector will never reclaim the object it points to, even if nothing else in the app actually needs that object anymore. A weak reference (WeakReference in Java/Kotlin) lets you hold onto an object without preventing it from being garbage collected, if nothing else holds a strong reference to it, the GC is free to reclaim it, and the weak reference then simply returns null when you try to access it. This is used in specific patterns where you genuinely need to reference something (like an Activity, from a long-lived background task) without that reference itself being the reason it can never be garbage collected, though in modern Android development, this pattern shows up less often since properly scoped coroutines already solve most of the underlying lifecycle problem more cleanly.


Testing, Build System and Production Practices

1. What's the difference between a unit test and an instrumented test in Android?

A unit test runs entirely on your development machine's JVM, fast, no Android device or emulator needed, but it can't use real Android framework classes (those are stubbed out or need to be mocked), which makes it a good fit for testing pure business logic, ViewModels, repositories, use cases, that don't directly depend on the Android runtime. An instrumented test actually runs on a real device or emulator, giving you access to the genuine Android framework and APIs, which is necessary for testing things that actually depend on Android-specific behavior, UI interactions, database operations through Room's actual SQLite engine, but it's meaningfully slower to run since it needs an actual device/emulator to execute against.

2. What is Gradle, and what's actually happening during an Android build?

Gradle is the build automation system Android projects use, it reads your build configuration (dependencies, build variants, plugins) and orchestrates the entire build process, compiling your Kotlin/Java source into bytecode, processing resources (layouts, strings, images), running annotation processors (like Room's or Hilt's code generation), merging everything together with any library dependencies, and finally packaging it all into an APK or AAB. A large part of why Android builds can feel slow is genuinely how much work is happening in that pipeline, especially annotation processing and resource merging across many dependencies, which is why build performance (incremental builds, caching) is its own whole area of Android tooling concern.

3. What's the difference between a debug build and a release build, and what does ProGuard/R8 actually do to your release APK?

A debug build includes debugging symbols, isn't optimized, and is typically signed with a default debug key, meant purely for local development and testing, faster to build but larger and slower to run than a release build would be. A release build is what you actually ship, optimized and signed with your real production signing key. R8 (which replaced ProGuard as Android's default code shrinker) runs specifically on release builds, and it shrinks (removes unused code and resources), obfuscates (renames classes/methods to short, meaningless names, making reverse engineering harder), and optimizes your compiled code, which meaningfully reduces the final APK's size and can improve runtime performance, at the cost of needing to carefully configure keep rules for anything relying on reflection that R8's static analysis might otherwise incorrectly strip out.

4. What is Hilt (or Dagger), and what problem does dependency injection solve specifically in an Android context?

Dependency injection generally means a class receives its dependencies from the outside rather than constructing them itself, which makes code more testable (you can substitute mocks) and reduces tight coupling. Hilt is Android's officially recommended DI library, built on top of Dagger, specifically designed to handle the awkward reality that Android framework classes like Activities and Fragments are instantiated by the OS itself, not by your own code, so you can't just pass dependencies into their constructors the normal way, Hilt provides Android-specific annotations and generated code that handle injecting dependencies into these framework-managed classes cleanly, without you manually wiring up a dependency graph by hand the way you'd have to with raw Dagger.

5. What's the difference between an APK and an AAB (Android App Bundle), and why does the Play Store prefer AAB now?

An APK is a single, complete package containing everything needed to install and run your app on any device, all resources for every screen density, every supported CPU architecture, every language, bundled together, whether a given device actually needs all of it or not. An AAB is a publishing format, not something installed directly on a device, instead, you upload it to the Play Store, and the Play Store itself generates and serves optimized, device-specific APKs on the fly for each individual user's actual device configuration, only including the resources, languages, and native libraries that specific device genuinely needs, which meaningfully reduces the download and install size compared to one bloated, one-size-fits-all APK trying to cover every possible device.



Struggling to Find a Job? Get Specific Batch Wise job Updates ✅ Check now

Join our WhatsApp Channel for more resources.