Skip to content

Kotlin Developer Interview Questions & Answers

40 questions for Kotlin Developer interviews — 3 easy, 32 medium, 5 hard.

About the Kotlin Developer role

A Kotlin developer specialises in writing idiomatic, safe Kotlin across Android and sometimes shared Kotlin Multiplatform modules. They focus on language features like coroutines, generics, sealed hierarchies and DSLs, and often help teams migrate Java code to Kotlin. This role is common in companies modernising legacy Android apps or sharing business logic across Android and iOS with Kotlin Multiplatform. Strong testing habits are expected.

Idiomatic Kotlin Coroutines Java to Kotlin migration Kotlin Multiplatform Code review
Solved for this roleStart practicing
3 QUESTIONS FREESign up to unlock the full question bank and track your progress.Unlock every question and answer, then continue practising company-wise and package-wise.
3 questions free Unlock the rest in seconds
01

What are sequences in Kotlin and when are they better than collections?

Save to ▾
Show answer
Sequences evaluate lazily, element by element, while collection operations like map and filter create a new intermediate list at every step. For long chains on large data, or when you only need the first few results with take or first, asSequence() avoids extra allocations. For small lists, plain collections are often faster because sequences add per-element overhead. Measure before switching in performance-critical code.
02

How do you store relationships in Room, such as orders with items?

Save to ▾
Show answer
Create separate entities and link them with a foreign key column, like OrderItem.orderId. Then define a data class with @Embedded order and @Relation(parentColumn = "id", entityColumn = "orderId") items, and query it in a DAO method annotated with @Transaction so both reads are consistent. For many-to-many relations, add a cross-reference entity with junction. Add indices on foreign key columns for query speed.
03

How do you build a single-activity app with the Navigation component?

Save to ▾
Show answer
Keep one Activity hosting a NavHost, and represent each screen as a Compose destination or Fragment. Define routes in a navigation graph, preferably with type-safe routes using Kotlin serialization in Navigation 2.8 and later. Use nested graphs for flows like login or checkout, pop the back stack with popUpTo when finishing a flow, and handle deep links on destinations. This simplifies shared transitions and state handling.
04

What file storage options does Android provide and how does scoped storage affect them?

Save to ▾
Show answer
App-specific storage (filesDir, cacheDir, getExternalFilesDir) needs no permission and is deleted on uninstall. Shared media is accessed through MediaStore, and other documents through the Storage Access Framework. Since Android 10, scoped storage blocks broad file system access, and since Android 13 media permissions are split into images, video and audio. The photo picker lets users choose media without any permission, which is preferred.
05

How do you write tests for a Room DAO?

Save to ▾
Show answer
Create an in-memory database with Room.inMemoryDatabaseBuilder in an instrumented test or with Robolectric, get the DAO, insert test data and assert query results. For Flow queries, collect the first value with first() or use Turbine. Close the database in @After. Test edge cases like conflict strategies and ordering. In-memory databases are fast and isolated, so each test starts clean.
06

Tell me about a time you handled a production crash under pressure.

Save to ▾
Show answer
Situation: after a release, crash-free users dropped from 99.6 to 97 percent. Task: stop the impact fast. Action: I checked Crashlytics, found a crash on Android 14 caused by a missing foreground service type, paused the staged rollout in Play Console, shipped a hotfix with a test on an emulator for that API level, and wrote a short postmortem. Result: crash rate recovered within a day and we added API-level checks to our release checklist.
07

Users who reinstall the app lose all their settings. How can you restore them?

Save to ▾
Show answer
Android Auto Backup automatically backs up app data to the user's cloud backup account for apps targeting API 23 and above, unless disabled. Configure dataExtractionRules for Android 12 and above to include settings files and exclude tokens or caches. For important data like notes, sync to your own server tied to the user's account, since backup is not guaranteed. Test with adb shell bmgr commands.
08

Explain backpressure handling in Flow with buffer, conflate and collectLatest.

Save to ▾
Show answer
Flow is sequential by default: the producer suspends until the collector processes each value. buffer() runs producer and collector in separate coroutines with a queue between them. conflate() keeps only the latest value when the collector is slow, dropping intermediate ones, good for sensor or progress updates. collectLatest cancels processing of the previous value when a new one arrives. Choose based on whether every value matters.
09

Write a search flow that debounces user input and cancels stale requests.

Save to ▾
Show answer
private val query = MutableStateFlow("") val results: StateFlow<List<Item>> = query .debounce(300) .map { it.trim() } .filter { it.length >= 2 } .distinctUntilChanged() .flatMapLatest { q -> repo.search(q) } .catch { emit(emptyList()) } .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptyList()) fun onQueryChange(text: String) { query.value = text }
10

After an app update, users report that the app crashes on launch. Logs show a Room migration error. What do you do?

Save to ▾
Show answer
Immediately halt the staged rollout in Play Console. Reproduce by installing the previous version, adding data, and upgrading. The schema version was probably increased without a matching migration, or a migration SQL does not match the new entity. Write the correct Migration, test it with MigrationTestHelper from every supported old version, and release a hotfix. Avoid destructive fallback because it deletes user data.
11

What conflict strategies does Room offer for inserts?

Save to ▾
Show answer
@Insert(onConflict = ...) supports REPLACE, which deletes the old row and inserts the new one; IGNORE, which skips the insert; ABORT, the default, which rolls back the transaction; and others. REPLACE can trigger cascading deletes on foreign keys because it deletes first. @Upsert, added in Room 2.5, inserts or updates without deleting, making it safer for syncing server data into local tables.
12

A teammate uses !! everywhere to silence compiler errors. How do you handle this in code review?

Save to ▾
Show answer
Explain calmly that !! converts a compile-time check into a possible runtime crash. Suggest alternatives per case: safe calls with ?.let, Elvis with a default or early return, requireNotNull with a clear message, or fixing the type at the source so it is non-null. Add a detekt rule to flag !! in new code. Pair with them on one file so the change feels like help, not criticism.
13

Tell me about a time you disagreed with a designer about a UI decision.

Save to ▾
Show answer
Situation: a designer wanted a custom tiny close icon on a full-screen dialog. Task: keep the app accessible without blocking the design. Action: I showed that the touch target was below the 48dp guideline and that TalkBack had no label, and prototyped the same look with a larger invisible touch area. Result: we kept the visual style, passed accessibility checks, and agreed to review touch targets in future design handoffs.
14

How do you migrate from SharedPreferences to DataStore without losing user data?

Save to ▾
Show answer
Use SharedPreferencesMigration when creating the DataStore: preferencesDataStore(name = "settings", produceMigrations = { ctx -> listOf(SharedPreferencesMigration(ctx, "old_prefs")) }). On first access, DataStore copies the values and deletes the old file. Update all reads and writes to go through the new repository at once, because mixing both stores leads to inconsistent values. Test the migration with existing preference files.
15

Network calls keep running after the user leaves a screen and results arrive late. How do you fix this?

Save to ▾
Show answer
The work is likely launched in GlobalScope, a custom scope never cancelled, or an Application-level scope. Move it to viewModelScope so it is cancelled when the ViewModel is cleared, and make sure the calls are cancellable, which Retrofit suspend functions are. If work must outlive the screen, like uploading a file, move it to WorkManager or an application scope deliberately, and do not update UI from it.
16

What is the difference between launch and async?

Save to ▾
Show answer
launch starts a coroutine for work that returns no result and gives back a Job you can cancel or join. async returns a Deferred<T>, and you call await() to get the result. Use async to run independent tasks in parallel, such as fetching profile and orders together, then awaiting both. Exceptions differ: launch propagates them to the parent immediately, while async holds them until await, though in structured concurrency the parent still gets cancelled.
17

Explain object, companion object and object expressions.

Save to ▾
Show answer
An object declaration creates a thread-safe singleton initialised lazily on first access. A companion object is an object tied to a class, used for factory methods and constants, similar to static members in Java; add @JvmStatic for Java callers. An object expression creates an anonymous object on the spot, often to implement an interface such as a callback. Avoid holding Context or Activity references in singletons to prevent memory leaks.
18

How does coroutine cancellation work, and why must code be cooperative?

Save to ▾
Show answer
Cancelling a Job sets it to cancelling state, and the next suspension point such as delay, withContext or a suspending library call throws CancellationException. A tight CPU loop with no suspension keeps running, so check isActive or call ensureActive() or yield() inside it. Do not swallow CancellationException in a generic catch block; rethrow it. Use finally or withContext(NonCancellable) for cleanup that must finish.
19

Write a function that retries a suspend call with exponential backoff.

Save to ▾
Show answer
suspend fun <T> retryWithBackoff( times: Int = 3, initialDelay: Long = 500, factor: Double = 2.0, block: suspend () -> T ): T { var delayMs = initialDelay repeat(times - 1) { try { return block() } catch (e: IOException) { /* retry */ } delay(delayMs) delayMs = (delayMs * factor).toLong() } return block() }
20

What are extension functions and how are they resolved?

Save to ▾
Show answer
Extension functions let you add functions to an existing class without inheriting from it, for example fun String.isValidEmail(): Boolean. They are resolved statically at compile time based on the declared type of the variable, not the runtime type, so they do not support polymorphism. They cannot access private members of the class. They are used widely in Android KTX libraries, such as Context.toast or View.isVisible.
21

The app shows an ANR when the user taps Save. The save function is suspend. What could be wrong?

Save to ▾
Show answer
A suspend function does not automatically run off the main thread. If it calls blocking code, like a synchronous file write, a SharedPreferences commit, a blocking SDK or heavy JSON parsing, it still blocks the main thread. Check with a profiler or StrictMode. Fix by wrapping the blocking part in withContext(Dispatchers.IO) or Default, or by using main-safe APIs such as Room suspend DAOs or DataStore.
22

Write a periodic sync job with WorkManager that runs only on unmetered network.

Save to ▾
Show answer
class SyncWorker(ctx: Context, params: WorkerParameters) : CoroutineWorker(ctx, params) { override suspend fun doWork(): Result = try { repo.sync() Result.success() } catch (e: IOException) { if (runAttemptCount < 3) Result.retry() else Result.failure() } } val request = PeriodicWorkRequestBuilder<SyncWorker>(6, TimeUnit.HOURS) .setConstraints(Constraints(requiredNetworkType = NetworkType.UNMETERED)) .setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 30, TimeUnit.SECONDS) .build() WorkManager.getInstance(context).enqueueUniquePeriodicWork( "sync", ExistingPeriodicWorkPolicy.KEEP, request)
23

What does derivedStateOf do and when should you use it?

Save to ▾
Show answer
derivedStateOf creates state computed from other state, and only triggers recomposition when the computed result changes. A classic example is showing a scroll-to-top button: val showButton by remember { derivedStateOf { listState.firstVisibleItemIndex > 0 } }. Without it, every scroll pixel would recompose. Do not use it when the result changes as often as the input, because it only adds overhead.
24

How do you build an adaptive layout for phones, foldables and tablets in Compose?

Save to ▾
Show answer
Use WindowSizeClass from the Material 3 adaptive library to choose layouts by width class, not device type. On compact width show a bottom navigation bar; on medium use a navigation rail; on expanded use a list-detail layout with ListDetailPaneScaffold or NavigationSuiteScaffold. Avoid locking orientation, since Android 16 ignores orientation restrictions on large screens. Test with resizable emulators and foldable postures.
25

You need to cache API responses so the home screen opens instantly. What approach do you take?

Save to ▾
Show answer
Store the response in Room, or a file for a single blob, and have the screen observe the cached data immediately while a refresh runs in the background, known as stale-while-revalidate. Store a timestamp to decide when cache is too old. HTTP caching with OkHttp Cache can also help for GET requests with proper cache headers. Show cached content first, then update smoothly when fresh data arrives.
26

How do you prioritise when you have several bugs and a feature deadline in the same week?

Save to ▾
Show answer
I rank by user impact and risk: crashes and data loss first, then bugs affecting payments or login, then the feature, then minor UI issues. I share the plan with my lead and product manager so trade-offs are visible, and I ask for help or a scope cut early instead of quietly missing the deadline. Small, safe fixes I batch together to save release overhead.
27

Your team is migrating a Java Android app to Kotlin. How would you plan it?

Save to ▾
Show answer
Kotlin and Java interoperate, so migrate gradually rather than all at once. Start with new features in Kotlin, then convert leaf classes like models and utilities, writing tests before converting. Add nullability annotations to Java code so Kotlin sees correct types. Review auto-converted code because it often overuses !!. Track progress per module and agree on style rules with ktlint or detekt so the codebase stays consistent.
28

Write a delegated property that logs whenever a value changes.

Save to ▾
Show answer
import kotlin.properties.Delegates class Settings { var theme: String by Delegates.observable("light") { prop, old, new -> println("${prop.name} changed from $old to $new") } } // Use Delegates.vetoable if you need to reject a change // by returning false from the lambda.
29

What is the difference between viewModelScope and lifecycleScope?

Save to ▾
Show answer
viewModelScope is cancelled when the ViewModel is cleared, so it survives configuration changes like rotation and suits loading data. lifecycleScope is tied to an Activity or Fragment lifecycle and is cancelled when it is destroyed, so it suits UI-related work. For collecting flows in views, combine lifecycleScope with repeatOnLifecycle(Lifecycle.State.STARTED) so collection stops when the app goes to background. Avoid GlobalScope in app code.
30

How do you set up CI/CD for an Android app?

Save to ▾
Show answer
On every pull request, run lint, ktlint or detekt, unit tests and a debug build in a CI service like GitHub Actions or Bitrise, with Gradle caching. On merge to main, build a signed release AAB using secrets, run instrumented tests on emulators or a device farm, and upload to the Play internal track with Gradle Play Publisher or fastlane. Promote to production manually after QA with a staged rollout.
31

What is a sealed class and when would you use it on Android?

Save to ▾
Show answer
A sealed class restricts its subclasses to a known set defined in the same package and module. The compiler knows every subtype, so a when expression over it can be exhaustive without an else branch. On Android it is commonly used to model UI state such as Loading, Success(data) and Error(message), or one-time events. Adding a new subtype causes compile errors wherever handling is missing, which prevents forgotten cases.
32

How do you store sensitive data such as tokens securely on Android?

Save to ▾
Show answer
Never store secrets in plain SharedPreferences or hardcode them in code. Use the Android Keystore to generate a key that never leaves secure hardware, and encrypt the data before storing it in DataStore or a file. The security-crypto EncryptedSharedPreferences library is deprecated, so many teams use Tink with Keystore. Keep tokens short-lived with refresh tokens, and clear them on logout. Exclude them from backups.
33

How should a Fragment or Activity collect a Flow safely?

Save to ▾
Show answer
Use lifecycleScope.launch with repeatOnLifecycle(Lifecycle.State.STARTED) so collection starts when the UI is visible and stops in the background, avoiding wasted work and crashes from updating views that are not ready. In a Fragment, use viewLifecycleOwner.lifecycleScope because the fragment view can be destroyed while the fragment lives. In Compose, use collectAsStateWithLifecycle(), which does the same lifecycle-aware collection.
34

Describe a time you fixed a difficult concurrency bug.

Save to ▾
Show answer
Situation: our cart total sometimes showed wrong values after quick taps. Task: find the race. Action: I added logs with thread names and found two coroutines updating the same mutable list. I changed state updates to MutableStateFlow.update {}, which is atomic, and added a test with many concurrent updates. Result: the bug disappeared, and I shared a short note with the team on safe state updates so it would not repeat.
35

How do Room migrations work?

Save to ▾
Show answer
When you change the schema, increase the database version and provide a Migration(from, to) with SQL like ALTER TABLE, or use AutoMigration for simple changes with exported schemas. Without a migration, Room throws at startup unless you call fallbackToDestructiveMigration, which wipes user data. Export schemas to version control and test migrations with MigrationTestHelper so upgrades from every old version are verified.
36

A screen shows a snackbar again every time the device rotates. How do you fix it?

Save to ▾
Show answer
The event is probably stored as state in a StateFlow or LiveData, so it is re-delivered when the new Activity collects again. Options: model it as part of UI state and have the UI call a function like onMessageShown() to clear it after display, which is the approach the official guidance recommends; or send it through a Channel exposed with receiveAsFlow so each event is consumed once. Collect with lifecycle awareness to avoid losing events.
37

How do you handle exceptions in coroutines?

Save to ▾
Show answer
Use try-catch around suspend calls for expected failures like network errors; this is the clearest approach. A CoroutineExceptionHandler on a root launch catches uncaught exceptions as a last resort for logging, but it does not work for async or child coroutines. For async, exceptions surface at await. Always rethrow CancellationException. Many teams wrap results in a Result or sealed type so the UI layer gets an explicit error state.
38

Write code to fetch two API results in parallel inside a ViewModel.

Save to ▾
Show answer
fun loadDashboard() { viewModelScope.launch { _state.value = UiState.Loading try { val (profile, orders) = coroutineScope { val p = async { repo.getProfile() } val o = async { repo.getOrders() } p.await() to o.await() } _state.value = UiState.Success(profile, orders) } catch (e: IOException) { _state.value = UiState.Error("Network error") } } }
39

Write a Flow that combines user settings and product list into one UI state.

Save to ▾
Show answer
val uiState: StateFlow<HomeState> = combine( settingsRepo.settings, productRepo.products ) { settings, products -> HomeState( currency = settings.currency, items = products.sortedBy { it.price } ) }.stateIn( scope = viewModelScope, started = SharingStarted.WhileSubscribed(5000), initialValue = HomeState() )
40

A Room query on a large table is slow and causes jank. How do you fix it?

Save to ▾
Show answer
Make sure the query runs off the main thread using suspend or Flow. Use EXPLAIN QUERY PLAN to check for full table scans, and add an @Index on columns used in WHERE, JOIN or ORDER BY. Select only needed columns into a smaller data class instead of SELECT *, page results with Paging 3, and avoid loading large blobs into lists. Wrap bulk inserts in a single transaction.

Frequently asked questions

What does a Kotlin Developer do?

A Kotlin developer specialises in writing idiomatic, safe Kotlin across Android and sometimes shared Kotlin Multiplatform modules. They focus on language features like coroutines, generics, sealed hierarchies and DSLs, and often help teams migrate Java code to Kotlin. This role is common in companies modernising legacy Android apps or sharing business logic across Android and iOS with Kotlin Multiplatform. Strong testing habits are expected.

Which skills are tested in a Kotlin Developer interview?

Expect questions on Idiomatic Kotlin, Coroutines, Java to Kotlin migration, Kotlin Multiplatform, Code review. Most rounds also ask you to explain a project and solve one practical problem live.

How do I prepare for a Kotlin Developer interview?

Revise Kotlin Fundamentals, Coroutines and Flow, Room, DataStore and WorkManager first using the topic sheets, practise the questions on this page aloud, then do one full mock interview to practise timing.