Core bank

The questions that come up in almost every round

Separate from the company reports: this is the ground every mobile interview covers, answered the way you would say it out loud, with the follow-up question each answer usually invites.

50questions24topics
← Company-wise interview experiences
LifecycleArchitectureKotlinCoroutinesFlowBackground workComponentsPerformanceFundamentalsNavigationComposeDIRoomNetworkingTestingBuildFlutterReact NativeiOSSystem designAISecurityDebuggingBehavioural
01Explain the Activity lifecycle. Which callbacks actually matter in an interview?EasyScreeningAndroid DeveloperLifecycle

An Activity moves through states as the user opens, leaves and returns to your screen. Android tells you about each move through a callback.

  • onCreate() — the screen is being built. Inflate the layout, set up the ViewModel. Runs once per instance.
  • onStart() — the screen is visible but not yet interactive.
  • onResume() — the screen is in front and taking touches. Start camera, location, sensors here.
  • onPause() — something came on top (a dialog, a call). Stop anything that should not run in the background.
  • onStop() — no longer visible. Release heavy things.
  • onDestroy() — the Activity is going away, either because you finished it or because of a configuration change.

What interviewers are really checking: do you know the pair rule. Whatever you start in onStart you stop in onStop; whatever you start in onResume you stop in onPause. Most lifecycle bugs are an unmatched pair.

One line to remember: onResume means "the user can touch this", not "the screen is created".

02What happens on screen rotation, and how do you keep state across it?EasyScreeningAndroid DeveloperLifecycle

On rotation Android destroys the Activity and creates a new one, because a new layout may be needed. Anything held in Activity fields is gone.

Three ways to survive it, from best to worst:

  • ViewModel — survives configuration change by design. Put screen state here. This is the normal answer.
  • SavedStateHandle / onSaveInstanceState — survives both rotation and process death, but only small data (ids, a search text, scroll position). It is a Bundle, not a cache.
  • Re-fetch — fine for cheap data, bad for a paid API call.

Follow-up they almost always ask: "ViewModel survives rotation — does it survive the user killing the app?" No. For that you need SavedStateHandle or disk.

03ViewModel vs onSaveInstanceState — when do you use which?MediumTechnicalAndroid DeveloperArchitecture

They solve two different kinds of "the screen went away".

  • ViewModel: survives rotation and lives as long as the screen is in the back stack. Can hold big objects — lists, flows, jobs. Dies when the process dies.
  • onSaveInstanceState / SavedStateHandle: survives process death (Android killed your app in the background and the user came back). Serialised into a Bundle, so keep it tiny.

Rule of thumb: ViewModel holds the data, SavedStateHandle holds the key to the data. The product list goes in the ViewModel; the productId and the search query go in SavedStateHandle.

04What is the difference between val and var, and between val and const val?EasyScreeningAndroid DeveloperKotlin
  • var — can be reassigned.
  • val — cannot be reassigned. It is read-only, not deeply immutable: val list = mutableListOf(1) still allows list.add(2).
  • const val — a compile-time constant. Only primitives and String, only at top level or inside an object/companion. The value is inlined at the call site, so there is no getter at runtime.
var count = 0            // reassignable
val name = "Anand"       // read-only reference
const val BASE_URL = "https://api.example.com/"  // inlined constant

Trap: val can still be backed by a custom getter that returns something new each time, so "val means constant" is wrong.

05Explain null safety in Kotlin. What do ?., ?: and !! actually do?EasyScreeningAndroid DeveloperKotlin

Kotlin splits every type in two: String can never be null, String? might be. The compiler then forces you to handle the nullable case.

  • ?. safe call — call it if not null, otherwise give back null.
  • ?: elvis — "use this instead when the left side is null".
  • !! not-null assertion — "trust me, it is not null". Throws NPE if you were wrong.
  • let with ?. — run a block only when the value exists.
val length = name?.length ?: 0

user?.email?.let { sendMail(it) }

What they want to hear: !! is a code smell in reviews. It is acceptable only where the platform guarantees the value (a view after onCreateView, say) and even then requireNotNull with a message is better, because the crash tells you which value was null.

06What are coroutines, and how are they different from threads?MediumTechnicalAndroid DeveloperCoroutines

A coroutine is a piece of work that can pause and resume without blocking the thread it was running on.

A thread is an OS resource — roughly 1 MB of stack, and switching between threads costs the OS work. A coroutine is just an object on the heap. One thread can run thousands of coroutines, because whenever a coroutine hits a suspend point (a network call, a delay) it steps off the thread and the thread picks up other work.

// 100,000 coroutines: fine
repeat(100_000) { launch { delay(1000) } }

// 100,000 threads: your app dies

The sentence that lands well: "Threads are how work runs in parallel; coroutines are how we wait without wasting a thread."

07Explain Dispatchers.Main, IO and Default. Which one for what?EasyTechnicalAndroid DeveloperCoroutines
  • Main — the UI thread. Touch the UI and state only here.
  • IO — for waiting: network, disk, database. A big elastic pool (64 threads by default) because these threads mostly sit idle.
  • Default — for CPU work: parsing a large JSON, sorting 50k items, image maths. Sized to the number of CPU cores, so it does not thrash.

The practical rule: a suspend function should be safe to call from anywhere. Put withContext(Dispatchers.IO) inside the repository function, not at the call site in the ViewModel.

suspend fun loadUser(id: String): User = withContext(Dispatchers.IO) {
    api.getUser(id)
}

Retrofit and Room already move off the main thread themselves, so wrapping them again is harmless but unnecessary — a nice detail to mention.

08What is structured concurrency? Why does viewModelScope matter?MediumTechnicalAndroid DeveloperCoroutines

Structured concurrency means every coroutine has a parent scope, and the parent is not finished until its children are. Cancel the parent and every child is cancelled too.

That is why viewModelScope matters: when the ViewModel is cleared, every coroutine launched in it is cancelled automatically. No leaked network call writing into a dead screen, no manual bookkeeping.

class CartViewModel : ViewModel() {
    fun load() = viewModelScope.launch {   // dies with the ViewModel
        _state.value = repo.cart()
    }
}

Red flag they look for: GlobalScope.launch. It has no parent, so nothing cancels it — the classic leak.

09launch vs async — and what does coroutineScope give you?MediumTechnicalAndroid DeveloperCoroutines
  • launch — fire work, get a Job, no result. Use it for "do this".
  • async — get a Deferred<T>, call await() for the result. Use it when you need the value, especially two calls in parallel.
suspend fun loadScreen() = coroutineScope {
    val user = async { api.user() }      // both start now
    val feed = async { api.feed() }
    Screen(user.await(), feed.await())   // ~max(t1, t2), not t1 + t2
}

coroutineScope { } is a suspending boundary: it waits for all children and, if any child fails, it cancels the siblings and rethrows. That is what makes the parallel call above safe.

10Flow vs LiveData vs StateFlow — what do you use today and why?MediumTechnicalAndroid DeveloperFlow
  • LiveData — lifecycle-aware, main-thread only, no operators worth the name. Legacy; still fine in older codebases.
  • Flow — cold stream. Nothing happens until someone collects. Rich operators (map, combine, debounce, flatMapLatest). Good for data sources: Room queries, search-as-you-type.
  • StateFlow — hot stream that always holds exactly one current value. This is what a screen's UI state should be.
  • SharedFlow — hot, no stored value, can replay. For one-shot events: show a toast, navigate.
private val _state = MutableStateFlow(UiState())
val state: StateFlow<UiState> = _state.asStateFlow()

Modern answer: StateFlow for screen state, SharedFlow (or a Channel) for events, Flow for the data layer, and collect in the UI with collectAsStateWithLifecycle() so collection stops when the screen is in the background.

11What is the difference between a Service, a WorkManager job and a coroutine for background work?MediumTechnicalAndroid DeveloperBackground work

Pick by two questions: must it survive the app closing? and must the user see it?

  • Coroutine in viewModelScope — work that only matters while the screen is open. Loading a list.
  • WorkManager — work that must finish eventually, even after a reboot. Upload a photo, sync offline orders. It persists the request and respects constraints like "only on Wi-Fi", "only when charging".
  • Foreground Service — work the user is actively aware of and that must keep running now: music playback, navigation, a live workout. Needs a visible notification.
  • Plain (background) Service — mostly a trap on modern Android. Background execution limits will kill it.

One-liner: "If the user would be annoyed when it silently did not happen, it is WorkManager. If the user is watching it happen, it is a foreground service."

12Explain the four Android components and when an Intent is explicit vs implicit.EasyScreeningAndroid DeveloperComponents
  • Activity — a screen.
  • Service — work without UI.
  • BroadcastReceiver — reacts to system or app events (connectivity change, boot).
  • ContentProvider — exposes data to other apps (Contacts, Media).

An explicit intent names the exact component: Intent(this, DetailActivity::class.java). Used inside your own app.

An implicit intent describes an action and lets the system find a handler: "open this URL", "share this text". The system shows a chooser. Always guard it — on some devices nothing can handle it.

val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url))
if (intent.resolveActivity(packageManager) != null) startActivity(intent)
13How do you avoid memory leaks in Android? Give real examples.MediumTechnicalAndroid DeveloperPerformance

A leak in Android is almost always the same story: something long-lived is holding a reference to something short-lived — usually an Activity, Fragment or View.

The usual suspects:

  • A static field or singleton holding an Activity Context. Pass applicationContext when the object outlives the screen.
  • An inner class or callback (a listener, a Handler) that is never unregistered. Unregister in the matching lifecycle callback.
  • GlobalScope.launch capturing the screen. Use viewModelScope or lifecycleScope.
  • Holding a Fragment's binding after onDestroyView. Null it out there.

How you prove it: LeakCanary in debug builds, and Android Studio's Memory Profiler — force a few rotations, hit "force GC", dump the heap and look for more than one instance of the same Activity.

14What is the difference between Serializable and Parcelable?EasyScreeningAndroid DeveloperFundamentals

Both let an object be put in a Bundle or Intent. The difference is cost.

  • Serializable is a Java marker interface. It uses reflection, creates a lot of temporary objects, and is noticeably slower.
  • Parcelable is Android's own: you (or the compiler) write the read/write code, so there is no reflection.

In Kotlin you never hand-write it — one annotation does the work:

@Parcelize
data class Product(val id: String, val title: String) : Parcelable

Bonus point: do not pass big objects between screens at all. Pass the id and let the next screen read from the repository — a Bundle over about 500 KB will throw TransactionTooLargeException.

15Explain launch modes / the back stack. What is singleTop vs singleTask?MediumTechnicalAndroid DeveloperNavigation

The back stack is just a stack of Activities. Launch mode decides whether a new one is pushed or an existing one is reused.

  • standard — always a new instance. Default.
  • singleTop — if that Activity is already on top, reuse it and deliver onNewIntent() instead of creating a second copy. Good for a search screen or a notification target.
  • singleTask — one instance in the task; everything above it is cleared. Good for a Home/launcher screen.
  • singleInstance — one instance, alone in its own task. Rare, mostly for launchers and special system apps.

Interview framing: "duplicate screens when I tap a notification twice" is the problem singleTop + onNewIntent solves — say that and you have answered it in practice, not theory.

16What is ANR and how do you debug one?MediumTechnicalAndroid DeveloperPerformance

ANR = Application Not Responding. The system shows it when the main thread is blocked too long: roughly 5 seconds for input, 10 for a broadcast, or a foreground service that does not start in time.

Usual causes: a network or database call on the main thread, a huge JSON parse, a synchronous SharedPreferences.commit(), a tight lock held by the UI thread, or too much work in onBindViewHolder/composition.

How to debug:

  • Pull /data/anr/traces.txt (or get it from the Play Console) and read the main thread's stack — that is where it was stuck.
  • Reproduce with StrictMode on; it will shout about disk and network on the main thread during development.
  • In production, Play Console vitals and Firebase Performance point at the screen.
17What are extension functions, and when should you NOT write one?EasyTechnicalAndroid DeveloperKotlin

An extension function lets you call a new function on an existing type without inheriting from it. The compiler turns it into a static function with the receiver as the first parameter.

fun String.isValidEmail(): Boolean =
    matches(Regex("^[^@\\s]+@[^@\\s]+\\.[^@\\s]+$"))

"a@b.com".isValidEmail()   // true

Two things interviewers like you to know:

  • They are resolved statically. No polymorphism — the declared type decides which extension runs, not the runtime type.
  • They cannot see private members and cannot override a real member. If a member with the same signature exists, the member wins.

Do not write one when the behaviour belongs to the class itself, or when it hides a heavy operation behind an innocent-looking call.

18data class, sealed class, object, companion object — explain each in one breath.EasyScreeningAndroid DeveloperKotlin
  • data class — a value holder. You get equals, hashCode, toString, copy and destructuring from the primary-constructor properties.
  • sealed class / sealed interface — a closed set of subtypes known at compile time, so when over it needs no else. This is how you model UI state: Loading, Success, Error.
  • object — a singleton, created lazily on first use and thread-safe.
  • companion object — the singleton that belongs to a class; where factory functions and constants live.
sealed interface UiState {
    data object Loading : UiState
    data class Success(val items: List<Item>) : UiState
    data class Error(val message: String) : UiState
}

Follow-up to be ready for: "why sealed over enum?" Because each state can carry its own different data; an enum constant cannot.

19How does recomposition work in Jetpack Compose?MediumTechnicalAndroid DeveloperCompose

A composable is a function that describes UI from state. Compose records which composables read which state. When a state object changes, Compose re-runs only the composables that read it — that re-run is recomposition.

Three facts that make the answer sound senior:

  • Recomposition is skippable: if a composable's parameters are unchanged and stable, Compose skips it entirely.
  • It can run in any order and can be cancelled or restarted, so a composable must have no side effects. Side effects go in LaunchedEffect, SideEffect, DisposableEffect.
  • It is not "the whole screen redraws". That misconception is what the question is testing.
@Composable
fun Counter() {
    var count by remember { mutableStateOf(0) }
    Button(onClick = { count++ }) { Text("Tapped $count") }  // only Text recomposes
}
20remember vs rememberSaveable vs a ViewModel — where does state belong?MediumTechnicalAndroid DeveloperCompose
  • remember — keeps a value across recompositions. Lost on rotation. Use it for pure UI things: is this dropdown open, animation state.
  • rememberSaveable — same, plus survives rotation and process death via the saved-state Bundle. Use it for a text field's value, a selected tab.
  • ViewModel — screen state that comes from or goes to the data layer, and anything that must outlive the composable: the loaded list, the submit result.

The deciding question: "would the user be upset if this reset?" If yes and it is small, rememberSaveable. If yes and it comes from a repository, the ViewModel. If no, plain remember.

21What is state hoisting, and why is it the core Compose pattern?MediumTechnicalAndroid DeveloperCompose

State hoisting means moving state up out of a composable and passing the value down plus an event up. The composable becomes stateless.

// stateless and reusable
@Composable
fun SearchBar(query: String, onQueryChange: (String) -> Unit) { ... }

// the caller owns the state
var query by rememberSaveable { mutableStateOf("") }
SearchBar(query = query, onQueryChange = { query = it })

Why it matters: the composable is now testable, previewable and reusable, and there is exactly one source of truth. This is "state down, events up" — the same idea as a controlled component on the web.

22Why does a LazyColumn need a key, and what goes wrong without one?MediumTechnicalAndroid DeveloperCompose

Without a key, Compose identifies items by their index. If the list is reordered, filtered or an item is removed from the middle, the state attached to a position stays on that position instead of following the item.

Visible symptoms: the wrong row keeps its expanded state, a checkbox appears to jump to a different item, animations look like items teleported, and scroll position is lost.

LazyColumn {
    items(products, key = { it.id }) { product ->
        ProductRow(product)
    }
}

The key must be stable and unique — a database id, not the index and not the title.

23Explain MVVM vs MVI. Which would you pick for a new screen?MediumTechnicalAndroid DeveloperArchitecture

MVVM: the ViewModel exposes state and the View calls methods on it. State is often several independent fields.

MVI: one immutable state object per screen, and every change arrives as an intent/event that a reducer applies. State is never mutated in place; a new copy is emitted.

data class CartState(
    val items: List<Item> = emptyList(),
    val isLoading: Boolean = false,
    val error: String? = null,
)

fun onEvent(event: CartEvent) { /* produce the next state */ }

How to answer the "which" part: for a simple screen MVVM with a single StateFlow<UiState> is enough, and that is already 80% of MVI. Go full MVI when a screen has many interacting flags and you keep hitting impossible combinations like isLoading = true with an error showing — one state object makes those combinations unrepresentable.

24Why use a Repository layer at all? Is it not just an extra file?EasyTechnicalAndroid DeveloperArchitecture

The repository answers one question for the rest of the app: "give me this data, I do not care from where."

Concretely it earns its place by owning:

  • the cache decision — return the database first, then refresh from network;
  • mapping — API DTOs become domain models, so a backend field rename does not touch your UI;
  • error translation — an HTTP 404 becomes a typed NotFound result;
  • testability — the ViewModel test uses a fake repository, no MockWebServer needed.

If a repository only forwards one call to one API with no mapping, say so honestly — it is ceremony there. Interviewers respect that more than a memorised layer diagram.

25What does dependency injection actually buy you, and how does Hilt work?MediumTechnicalAndroid DeveloperDI

DI means a class declares what it needs and someone else supplies it. The payoff is that production supplies the real Retrofit service and a test supplies a fake — without touching the class.

Hilt is a compile-time wrapper over Dagger. You tell it how to build things and where they live:

@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {
    @Provides @Singleton
    fun api(client: OkHttpClient): ApiService = Retrofit.Builder()...create()
}

@HiltViewModel
class FeedViewModel @Inject constructor(private val repo: FeedRepository) : ViewModel()

Key points to mention: it generates code at compile time, so a missing binding is a build error, not a crash; and the @InstallIn component defines the lifetime — SingletonComponent for app-wide, ViewModelComponent for per-ViewModel.

26How do you implement offline-first? Single source of truth, explained.HardSystem designAndroid DeveloperArchitecture

The rule: the UI reads only from the database. The network's job is to update the database, never to feed the screen directly.

fun products(): Flow<List<Product>> = dao.observeAll()   // UI collects this

suspend fun refresh() {
    val fresh = api.products()      // network
    dao.upsertAll(fresh.map { it.toEntity() })   // DB emits, UI updates itself
}

What that buys you: the app opens instantly with cached data, works on a train, and there is no "loading" flash on every navigation.

Then cover the three hard parts, because this is where the question is actually scored:

  • Writes while offline — write to the DB immediately with a pending flag, and queue a WorkManager job to sync.
  • Conflicts — decide a policy out loud: last-write-wins with a server timestamp, or server-wins, or a merge per field.
  • Staleness — store fetchedAt and refresh when it is older than N minutes, rather than on every screen open.
27Room: what are @Entity, DAO and migrations, and what happens if you change a column?MediumTechnicalAndroid DeveloperRoom
  • @Entity — a table, as a Kotlin class.
  • @Dao — an interface of queries; Room generates the implementation and checks your SQL at compile time.
  • @Database — ties entities and DAOs together and holds the version number.

Change the schema without bumping version and the app crashes at startup with "Room cannot verify the data integrity". Bump the version and you must provide a migration:

val MIGRATION_1_2 = object : Migration(1, 2) {
    override fun migrate(db: SupportSQLiteDatabase) {
        db.execSQL("ALTER TABLE product ADD COLUMN discount REAL NOT NULL DEFAULT 0")
    }
}

Mention fallbackToDestructiveMigration() and then immediately say it wipes user data, so it is a debug-only convenience. Also mention that returning Flow from a DAO gives you automatic UI updates on every write.

28How do you handle API errors properly instead of try/catch everywhere?MediumTechnicalAndroid DeveloperNetworking

Turn failures into values at the boundary, so the rest of the app has nothing to catch.

sealed interface ApiResult<out T> {
    data class Success<T>(val data: T) : ApiResult<T>
    data class Failure(val error: AppError) : ApiResult<Nothing>
}

sealed interface AppError {
    data object NoNetwork : AppError
    data object Unauthorised : AppError
    data class Server(val code: Int) : AppError
    data class Unknown(val cause: Throwable) : AppError
}

The repository maps IOException to NoNetwork, HTTP 401 to Unauthorised and so on. The ViewModel then does a when over a closed set, and the UI shows a sentence a human can read instead of a stack trace.

Two details that score: never swallow CancellationException in a catch (e: Exception) — rethrow it, or you break coroutine cancellation; and handle 401 centrally in an OkHttp Authenticator or interceptor, not screen by screen.

29What do you test on Android, and what do you not bother testing?MediumTechnicalAndroid DeveloperTesting

Spend the effort where bugs are expensive and the test is cheap:

  • Unit tests (most of them) — ViewModels, use cases, mappers, validation. Fast, run on the JVM, no device.
  • Repository tests — with an in-memory Room database and MockWebServer. This is where caching bugs hide.
  • UI tests (a few) — the two or three journeys that earn money: login, add to cart, checkout.

What not to test: that Compose draws a Text, that Retrofit parses JSON, getters and setters. You would be testing the framework.

@Test
fun showsErrorWhenLoginFails() = runTest {
    val vm = LoginViewModel(FakeAuthRepo(fails = true))
    vm.submit("a@b.com", "pw")
    assertEquals("Wrong email or password", vm.state.value.error)
}

Mention runTest and injecting the dispatcher (StandardTestDispatcher) — that is the practical detail most candidates miss.

30How do you make a list scroll smoothly? Name the real causes of jank.MediumTechnicalAndroid DeveloperPerformance

Jank means a frame missed its budget — about 16 ms at 60 Hz, 8 ms at 120 Hz. The fix is always "do less work per frame".

The usual causes, in the order you should check them:

  • Work in the item itself — date formatting, string building, sorting, decoding. Precompute it in the ViewModel, not per row.
  • Missing keys in LazyColumn, so items rebuild instead of being reused.
  • Unstable parameters — passing a List that Compose cannot prove is stable, so every row recomposes. Use an immutable list type or mark the class stable.
  • Reading state too high — reading a scroll offset in the parent recomposes the whole screen; read it in the smallest composable, or pass a lambda.
  • Images — load at the display size with Coil, never a full-resolution bitmap in a 100 dp row.

Then say how you measure: Compose's recomposition counts in Layout Inspector, Macrobenchmark with FrameTimingMetric, and Baseline Profiles for startup and first scroll. Measuring, not guessing, is what the question rewards.

31How would you reduce app startup time?HardTechnicalAndroid DeveloperPerformance

First split the number: cold start (process created), warm, hot. Optimise cold start — that is what reviews and Play vitals see.

Then attack it in order:

  • Application.onCreate — every SDK initialised here is on the critical path. Move analytics, crash reporting extras and image libraries to lazy init or App Startup with a background dispatcher.
  • Content providers — libraries that auto-initialise via a provider run before your first frame. AndroidX App Startup exists to merge these.
  • First screen — do not block it on network. Render a skeleton from cache, then fill.
  • Baseline Profiles — ship them. They pre-compile the hot path and routinely cut 20-30% off cold start and first-scroll jank.
  • Splash — use the SplashScreen API rather than a fake splash Activity, which just adds a screen to create.

Measure with adb shell am start -W, Macrobenchmark's StartupTimingMetric, and Play Console vitals for the field data.

32Explain ProGuard / R8 and what breaks when you enable it.MediumTechnicalAndroid DeveloperBuild

R8 does four things in the release build: shrink (remove unused code), optimise, obfuscate (rename classes to a.a.b) and desugar.

What breaks is anything that finds code by name at runtime, because the name changed:

  • Reflection-based JSON (Gson with field names) — keep your model classes or use a compile-time serializer.
  • Classes referenced only from XML or a manifest string.
  • Class.forName and service loaders.
-keep class com.app.model.** { *; }
-keepattributes Signature, *Annotation*

Two practical notes worth saying: upload the mapping.txt so crash reports de-obfuscate, and always smoke-test the release build — "works in debug" proves nothing here.

33What is the difference between an APK and an AAB?EasyScreeningAndroid DeveloperBuild

An APK is the installable file, and it contains every density, language and ABI, so every user downloads resources they will never use.

An AAB (Android App Bundle) is what you upload to Play. It is not installable: Play generates and signs an optimised APK for each device from it. Typical saving is 15-20% download size.

Consequences to mention: Play App Signing holds the upload key separately from the app signing key, and you can ship feature modules on demand. Outside Play (or for local testing) you still produce APKs with bundletool.

34Compose vs XML Views — how do you answer "should we migrate?"MediumTechnicalAndroid DeveloperCompose

Answer like an engineer with a product to ship, not a fan.

What Compose genuinely gives you: less code for the same screen, no findViewById or adapter boilerplate, state-driven UI so the "view out of sync with data" class of bug disappears, and previews that make design iteration fast.

What it costs: a real learning curve around stability and recomposition, some library gaps, and a bigger method count on very old projects.

The migration answer they want: you do not rewrite. Compose and Views interoperate — ComposeView inside an XML screen, AndroidView for a View inside Compose. So: new screens in Compose, and existing screens migrated only when they are being changed anyway. A big-bang rewrite buys the user nothing.

35Flutter: what is the difference between StatelessWidget and StatefulWidget?EasyScreeningFlutter DeveloperFlutter

StatelessWidget describes UI that depends only on its constructor arguments. To change it, the parent rebuilds it with new arguments.

StatefulWidget carries a State object that survives rebuilds of the widget. Calling setState marks it dirty and Flutter re-runs build.

class Counter extends StatefulWidget {
  @override State<Counter> createState() => _CounterState();
}

class _CounterState extends State<Counter> {
  int count = 0;
  @override Widget build(BuildContext context) =>
      TextButton(onPressed: () => setState(() => count++), child: Text('$count'));
}

The detail that impresses: the widget is immutable and thrown away on every build; the State object is the thing that persists. That is why state goes in State, never in the widget's fields.

36Flutter: explain the three trees — Widget, Element and RenderObject.HardTechnicalFlutter DeveloperFlutter
  • Widget — an immutable description ("a blue box with this text"). Cheap to create and recreated constantly.
  • Element — the instance that lives in the tree and holds identity and state. Flutter diffs the new widget against the element's old widget and reuses the element when the runtimeType and key match.
  • RenderObject — the thing that actually measures, lays out and paints. Expensive, so Flutter works hard to reuse it.

Why it is asked: it explains both why rebuilding widgets is not slow, and why Key matters — give two children the same position but different keys and you force Flutter to create new elements instead of reusing state on the wrong child.

37Flutter: how do you choose a state management approach?MediumTechnicalFlutter DeveloperFlutter

Say the axis first: how far does this state travel, and who else needs it?

  • setState — state used by one widget. A toggle, a form field. Perfectly fine; do not apologise for it.
  • Provider / InheritedWidget — share state down a subtree without passing it through every constructor.
  • Riverpod — the current default for new apps: compile-safe, testable, no BuildContext needed, easy to override providers in tests.
  • Bloc — explicit events in, states out, with a clear audit trail. Teams with many developers and complex flows like it; it is heavier for small screens.

Close with the honest line: there is no single right answer, there is a wrong one — mixing three of them in one codebase.

38React Native: what is the bridge, and what changed with the new architecture?HardTechnicalReact Native DeveloperReact Native

In the old architecture, JavaScript and native code could not call each other directly. Every call was serialised to JSON and passed over the bridge asynchronously. That is why fast gestures and big list updates felt laggy: the work was fine, the messaging was the bottleneck.

The new architecture removes that:

  • JSI — JavaScript holds a reference to a C++ object and calls it directly, synchronously, with no serialisation.
  • TurboModules — native modules loaded lazily and typed, built on JSI.
  • Fabric — the new renderer, so layout and view updates can happen without round trips.
  • Codegen — generates the typed glue from TypeScript specs, so mismatches are build errors.

Practical answer to "what does it mean for me": animations and gestures can run on the UI thread, so Reanimated-style smoothness becomes the default rather than a workaround.

39iOS: explain ARC, and what is a retain cycle?MediumTechnicaliOS DeveloperiOS

ARC (Automatic Reference Counting) frees an object when nothing strongly references it. The compiler inserts the retain/release calls — it is not a garbage collector, there is no background sweep.

A retain cycle is two objects holding each other strongly, so the count never reaches zero and neither is freed. The classic case is a closure capturing self while self owns the closure.

// leaks
viewModel.onDone = { self.dismiss() }

// fixed
viewModel.onDone = { [weak self] in self?.dismiss() }

Know the three keywords: strong (owns), weak (does not own, becomes nil), unowned (does not own, must never be nil — crashes if it is). Delegates are weak for exactly this reason.

40iOS: struct vs class in Swift — which do you reach for?EasyScreeningiOS DeveloperiOS
  • struct — value type, copied on assignment, no inheritance, no reference counting cost. Thread-safe by default because nobody shares the instance.
  • class — reference type, shared, supports inheritance, participates in ARC, can be mutated from several places.

Default to struct: models, view state, anything you pass around. Reach for class when you genuinely need shared mutable identity — a cache, a coordinator, a service that holds a connection — or when you must inherit from a UIKit class.

The follow-up: "copied on assignment, so is it slow for a big array?" No — Swift's collections use copy-on-write, so the copy is only made when you mutate.

41Design a chat app for mobile. How do you start answering?HardSystem designMobile EngineerSystem design

Do not start with technology. Start by narrowing the problem out loud, then go layer by layer.

1. Scope (1 minute). One-to-one or groups? Media? Read receipts? Offline send? Pick two or three and say the rest is out of scope.

2. Transport. WebSocket for the live connection, with push notifications as the fallback when the socket is closed and the app is in the background. Explain why not polling.

3. Local storage is the source of truth. Messages table with a client-generated id, server id, status (pending / sent / delivered / read) and createdAt. The UI reads the database, so a sent message appears instantly with a clock icon and is reconciled when the server acknowledges it.

4. Ordering and sync. Server timestamps for ordering, a per-conversation cursor to pull what you missed, and pagination upwards for history.

5. Reliability. The client id makes retries idempotent. A WorkManager/background task drains the outbox.

6. Then the mobile-specific constraints — this is what separates a mobile design round from a backend one: battery (one socket, not three), payload size on a 2G connection, image compression before upload, and what happens when the OS kills the process mid-send.

42How would you design image loading and caching for a feed?MediumSystem designMobile EngineerSystem design

Three layers, in order of cheapness:

  • Memory cache — LRU sized as a fraction of the heap. Holds decoded bitmaps, so a scroll back up is instant.
  • Disk cache — holds the encoded bytes, survives restarts, keyed by URL plus the requested size.
  • Network — last resort, with the server asked for a size-appropriate variant.

Then the details that make a feed feel good: decode at the view's size (never full resolution), cancel the request when the row is recycled, keep a stable placeholder so the layout does not jump, and prefetch a screen ahead. On a slow connection, drop to a lower-quality variant instead of making the user wait.

Finish with: "in production I would not write this — Coil or Glide already does all of it, and I would spend the time on sizing the caches and picking the right URL variant."

43Your app is slow on 2G and in low-memory conditions. What do you do?HardSystem designMobile EngineerSystem design

Mobile interviews in India love this one, because it is the real user. Attack it on three fronts.

Network: smaller payloads (ask the backend for a feed endpoint with only the fields the screen shows), gzip/Brotli, images as WebP/AVIF at display size, one request instead of five on screen open, and aggressive caching so a repeat visit is free.

Perceived speed: render cached content immediately, show skeletons not spinners, make writes optimistic, and never block the screen on an analytics or config call.

Memory: keep bitmaps small, release them in onTrimMemory, page large lists instead of holding everything, and avoid holding whole API responses in memory after mapping.

Then say how you would verify it: throttle to 2G in the emulator or with Charles, test on a 2 GB RAM device, and watch Play vitals for ANRs and cold start on low-end models rather than trusting a flagship phone.

44How do you integrate an LLM like Gemini into a mobile app without wrecking cost and latency?HardTechnicalMobile + AIAI

Say the architecture decision first: the API key never ships in the app. Calls go through your backend, which holds the key, enforces per-user rate limits and can swap models without an app release.

Then the four things that make it usable on a phone:

  • Stream the response. Tokens as they arrive, so the user reads while the model is still writing. Perceived latency drops even if total time is the same.
  • Keep the prompt small. Send a summary of the conversation, not the entire history — context length is cost and latency.
  • Cache aggressively. Identical prompts (a product summary, a tip of the day) are computed once server-side, not per user.
  • Design for failure. Timeouts, a cancel button tied to coroutine cancellation, and a graceful message when the model is unavailable — an AI feature must never be the reason a screen is unusable.

Bonus, and often the differentiator: mention on-device options (Gemini Nano via ML Kit GenAI, or a small quantised model) for privacy-sensitive or offline features, and be honest about the trade-off — smaller model, lower quality, no inference cost.

45What is RAG, explained for a mobile developer?MediumTechnicalMobile + AIAI

RAG = Retrieval Augmented Generation. Instead of hoping the model memorised your data, you look it up and paste the relevant pieces into the prompt.

The pipeline, in plain steps:

  • Split your documents into chunks and store an embedding (a vector) for each.
  • At question time, embed the question and find the nearest chunks.
  • Put those chunks in the prompt: "answer using only this context".
  • Return the answer with the source, so the user can verify it.

For a mobile app the retrieval almost always lives on your backend, and the app just sends the question. What the mobile developer owns: streaming the answer, showing sources, caching recent answers, and handling "I don't know" as a legitimate response rather than a failure.

Why it beats fine-tuning for most app features: your content changes weekly, and RAG means updating an index instead of retraining a model.

46How do you keep API keys and tokens safe in a mobile app?MediumTechnicalAndroid DeveloperSecurity

Start with the honest premise: anything shipped in the app can be extracted. An APK can be unzipped, strings can be dumped, and native libraries can be read. So the goal is to ship as few secrets as possible, not to hide them better.

  • Third-party secrets (payment, email, LLM keys) belong on your backend. The app calls your endpoint.
  • User tokens go in EncryptedSharedPreferences / the Keystore on Android, the Keychain on iOS — never in plain prefs, never in a log.
  • Short-lived access tokens with a refresh token, so a stolen token expires quickly.
  • Certificate pinning to stop casual man-in-the-middle, with a plan for rotating the pin.
  • No secrets in version control; keep them out of the repo and inject at build time — and remember that only keeps them off GitHub, not out of the binary.

Mention Play Integrity / App Attest for "is this a real app on a real device", and be clear that obfuscation raises the effort, it does not provide security.

47How do you debug a crash that only happens in production?MediumTechnicalAndroid DeveloperDebugging

Work from evidence, in this order:

  • Read the real stack trace from Crashlytics or Play Console, de-obfuscated with the matching mapping file. Group by crash signature, not by count.
  • Find the pattern — one OS version? one OEM? only low-memory devices? only after an update? The metadata usually names the cause faster than the stack does.
  • Use breadcrumbs — custom keys and logs you added for the last screens and actions, so you can reconstruct the path.
  • Reproduce deliberately — same OS image in the emulator, "Don't keep activities" on to force process death, low-memory and slow-network conditions.
  • Ship a safe fix behind a flag if you cannot reproduce it, then watch the crash-free rate on the staged rollout.

The sentence interviewers want: "I do not guess and patch — I reproduce first, because a fix I cannot verify is just a new unknown."

48Tell me about yourself. What is the right structure for a mobile role?EasyHRAnyBehavioural

Keep it under 90 seconds and in three parts:

  • Now — role, platform, what you own. "I am an Android developer at X, I own the checkout and payments flow in an app with about 2 million monthly users."
  • Proof — one concrete outcome with a number. "I cut cold start from 4.2s to 2.1s with Baseline Profiles and lazy SDK init, and crash-free sessions went from 98.3% to 99.6%."
  • Why this role — one honest sentence connecting their product to what you want to do next.

What to leave out: your college marks, a chronological list of every project, and jargon with no result attached. End with the number, not with "that's it about me".

49Tell me about a technical decision you got wrong.MediumHRAnyBehavioural

This question tests whether you can be trusted to report bad news. Do not dodge it with "I am a perfectionist".

Use four beats: decision → what went wrong → how you found out → what you changed.

"We cached the product list for 24 hours to cut API load. Prices changed more often than that, so users saw a stale price at checkout. Support tickets told us, not our monitoring — which was the real failure. We moved to a short cache with a price re-validation at checkout, and I added an alert on price-mismatch events. Now I ask 'how will we know if this is wrong?' before shipping a cache."

Pick a real, medium-sized mistake with a clear fix. Not a trivial one (it reads as evasive), not a catastrophic one you blame on someone else.

50Do you have any questions for us? What should you actually ask?EasyHRAnyBehavioural

Ask things whose answers would change your decision. Three good ones:

  • "How does a change reach a user — what does your release process and cadence look like?" Tells you about CI, QA and whether you will be firefighting.
  • "What does the team's test and code-review culture look like today, honestly?" You learn what you are inheriting.
  • "What would you want me to have accomplished in my first three months?" Reveals whether the role is actually defined.

Avoid asking only about salary and leave in the technical round, and avoid "what does the company do?" — that one says you did not look.