Whether you’re preparing for your first Android developer interview or aiming for a senior role, Kotlin questions are guaranteed to come up. Interviewers don’t just want textbook definitions — they want to see that you understand the why behind language features and can apply them in real code. This guide covers the 30 most commonly asked Kotlin interview questions with clear, concise answers and code examples that demonstrate real understanding.
Basics
1. What is Kotlin and why is it preferred over Java for Android?
Kotlin is a statically typed language developed by JetBrains that runs on the JVM. Google made it the officially recommended language for Android in 2019. It’s preferred over Java because of built-in null safety, concise syntax (roughly 40% fewer lines of code), coroutines for async programming, extension functions, data classes, and full Java interoperability. You can gradually migrate a Java codebase to Kotlin without rewriting everything.
2. What is the difference between val and var?
val name = "Alice" // read-only reference — cannot be reassigned
// name = "Bob" // ❌ compile error
var age = 25 // mutable reference — can be reassigned
age = 26 // ✅ works
// Important: val means the REFERENCE is fixed, not the object itself
val list = mutableListOf(1, 2, 3)
list.add(4) // ✅ works — the list contents change, but the reference doesn't
// list = mutableListOf() // ❌ can't reassign the reference
3. What are Kotlin’s basic data types?
// Kotlin has no primitive types at the language level
// Everything is an object — but the compiler optimises to JVM primitives where possible
val i: Int = 42 // → int on JVM
val d: Double = 3.14 // → double on JVM
val b: Boolean = true // → boolean on JVM
val s: String = "Hello" // String object
val c: Char = 'A' // → char on JVM
val l: Long = 100L // → long on JVM
// Nullable versions are always boxed (objects on JVM)
val ni: Int? = null // → Integer (boxed) on JVM
4. What is the difference between == and === in Kotlin?
// == checks STRUCTURAL equality (calls .equals() under the hood)
val a = "hello"
val b = "hello"
println(a == b) // true — same content
// === checks REFERENTIAL equality (same object in memory)
val x = Integer(127)
val y = Integer(127)
println(x === y) // false — different objects (may vary with caching)
// For data classes, == compares all properties
data class User(val name: String, val age: Int)
val u1 = User("Alice", 25)
val u2 = User("Alice", 25)
println(u1 == u2) // true — same property values
println(u1 === u2) // false — different objects
5. What is String interpolation in Kotlin?
val name = "Alice"
val age = 25
// Simple variable
println("Name: $name") // Name: Alice
// Expression
println("Next year: ${age + 1}") // Next year: 26
// Function call
println("Uppercase: ${name.uppercase()}") // Uppercase: ALICE
// This replaces Java's String.format() and concatenation
Null Safety
6. How does Kotlin handle null safety?
// By default, types CANNOT be null
var name: String = "Alice"
// name = null // ❌ compile error
// Add ? to allow null
var nullableName: String? = "Alice"
nullableName = null // ✅ allowed
// Safe call operator ?.
val length = nullableName?.length // null if nullableName is null, otherwise .length
// Elvis operator ?:
val len = nullableName?.length ?: 0 // use 0 if null
// Non-null assertion !!
val len2 = nullableName!!.length // throws NullPointerException if null
// ⚠️ Avoid !! in production code — it defeats the purpose of null safety
// Smart cast — compiler tracks null checks
if (nullableName != null) {
println(nullableName.length) // no ?. needed — compiler knows it's not null
}
7. What is the difference between safe call (?.) and non-null assertion (!!)?
val name: String? = null
// Safe call — returns null if receiver is null (SAFE)
val length = name?.length // null — no crash
// Non-null assertion — throws NPE if null (UNSAFE)
val length2 = name!!.length // 💥 NullPointerException!
// Use ?. in production code — always
// Use !! only when you're 100% certain the value is not null
// (and even then, prefer ?. with ?: for a default value)
8. What is the Elvis operator?
// ?: returns the left side if non-null, otherwise the right side
val name: String? = null
val displayName = name ?: "Guest" // "Guest"
// Commonly used with return for early exit
fun getUser(id: String): User {
val user = repository.findUser(id) ?: return User.DEFAULT
return user
}
// Can also throw
val user = repository.findUser(id) ?: throw UserNotFoundException(id)
Functions
9. What are default and named arguments?
// Default arguments — provide default values
fun greet(name: String, greeting: String = "Hello") {
println("$greeting, $name!")
}
greet("Alice") // "Hello, Alice!"
greet("Alice", "Hey") // "Hey, Alice!"
// Named arguments — specify parameter names at call site
fun createUser(name: String, age: Int, isActive: Boolean = true) { /* ... */ }
createUser(name = "Alice", age = 25) // isActive defaults to true
createUser(age = 30, name = "Bob", isActive = false) // order doesn't matter
// This eliminates the need for method overloading in most cases
10. What are extension functions?
// Add new functions to existing classes without modifying them
fun String.addExclamation(): String {
return this + "!"
}
println("Hello".addExclamation()) // "Hello!"
// Practical Android example
fun View.show() { visibility = View.VISIBLE }
fun View.hide() { visibility = View.GONE }
// Usage
textView.show()
progressBar.hide()
// Extension functions are resolved STATICALLY (at compile time)
// They don't actually modify the class — they're syntactic sugar
// They cannot access private members of the class
11. What are higher-order functions?
// A function that takes another function as parameter or returns one
fun performOperation(a: Int, b: Int, operation: (Int, Int) -> Int): Int {
return operation(a, b)
}
val sum = performOperation(3, 4) { a, b -> a + b } // 7
val product = performOperation(3, 4) { a, b -> a * b } // 12
// Real Android example — retry logic
suspend fun <T> retry(times: Int, block: suspend () -> T): T {
repeat(times - 1) {
try { return block() } catch (e: Exception) { /* retry */ }
}
return block() // last attempt
}
val data = retry(3) { api.fetchData() }
12. What are scope functions (let, run, apply, also, with)?
// let — transform an object, access with "it"
val length = "Hello".let { it.length } // 5
user?.let { saveToDatabase(it) } // safe call + operation
// run — execute a block with "this" as context
val result = user.run { "$name is $age years old" }
// apply — configure an object, returns the object itself
val user = User().apply {
name = "Alice"
age = 25
}
// also — side effects, access with "it", returns the object
val user = createUser().also { log("Created: ${it.name}") }
// with — call multiple methods on an object
with(textView) {
text = "Hello"
visibility = View.VISIBLE
setOnClickListener { /* ... */ }
}
// Quick rule:
// Need the result of transformation? → let or run
// Need to configure an object? → apply
// Need side effects? → also
// Need to call methods on an object? → with
OOP
13. What is a data class? How is it different from a regular class?
// data class auto-generates: equals(), hashCode(), toString(), copy(), componentN()
data class User(val name: String, val age: Int)
val user = User("Alice", 25)
println(user) // "User(name=Alice, age=25)" — auto toString()
val copy = user.copy(age = 26) // copy with modification
val (name, age) = user // destructuring
// Regular class — none of this is generated
class RegularUser(val name: String, val age: Int)
println(RegularUser("Alice", 25)) // "RegularUser@1a2b3c" — default toString()
// data class rules:
// - Primary constructor must have at least one val/var parameter
// - Cannot be abstract, open, sealed, or inner
// - Only properties in primary constructor are used in equals/hashCode/toString
14. What are sealed classes/interfaces?
// Sealed = restricted hierarchy — all subtypes must be defined in the same package
sealed interface UiState {
data object Loading : UiState
data class Success(val data: List<Article>) : UiState
data class Error(val message: String) : UiState
}
// Compiler knows ALL possible subtypes — when is exhaustive
fun render(state: UiState) {
when (state) {
is UiState.Loading -> showLoading()
is UiState.Success -> showArticles(state.data)
is UiState.Error -> showError(state.message)
// no else needed — compiler knows all cases are covered
}
}
// Sealed vs Enum:
// Enum — each value is a SINGLE instance
// Sealed — each subtype can be a class with its own state and multiple instances
15. What is the difference between object and companion object?
// object — singleton (one instance for the entire app)
object Analytics {
fun track(event: String) { /* ... */ }
}
Analytics.track("click") // call directly — no instantiation
// companion object — static-like members inside a class
class User(val name: String) {
companion object {
const val MAX_NAME_LENGTH = 50
fun create(name: String): User = User(name.take(MAX_NAME_LENGTH))
}
}
User.MAX_NAME_LENGTH // access like static field
User.create("Alice") // factory method
// companion object is an actual object — can implement interfaces
class User(val name: String) {
companion object : Parcelable.Creator<User> {
override fun createFromParcel(source: Parcel): User = /* ... */
override fun newArray(size: Int): Array<User?> = arrayOfNulls(size)
}
}
16. What is the difference between abstract class and interface in Kotlin?
// Interface — no state, multiple inheritance allowed
interface Clickable {
fun click() // abstract
fun showRipple() { println("Ripple effect") } // default implementation
}
// Abstract class — can hold state, single inheritance only
abstract class Animal(val name: String) { // constructor with state
abstract fun sound(): String
fun describe() = "$name says ${sound()}" // concrete method using state
}
// Key differences:
// Interface: no constructor, no state, multiple allowed
// Abstract class: has constructor, can hold state, only one parent
// Use interface when: defining a contract/capability (Clickable, Serializable)
// Use abstract class when: sharing state and behavior among related classes
17. What are visibility modifiers in Kotlin?
// public — visible everywhere (DEFAULT in Kotlin, unlike Java's package-private)
// private — visible inside the file (top-level) or class only
// protected — visible in the class and subclasses (NOT package)
// internal — visible in the same module (Gradle module)
class MyClass {
public val a = 1 // everywhere
private val b = 2 // only inside MyClass
protected val c = 3 // MyClass + subclasses
internal val d = 4 // same Gradle module
}
// Kotlin vs Java:
// Kotlin default = public | Java default = package-private
// Kotlin has internal | Java has no module-level visibility
// Kotlin protected = class + subclass | Java protected = class + subclass + package
Kotlin-Specific Features
18. What is the difference between lazy and lateinit?
// lazy — for val, initialised on first access
val heavyObject: ExpensiveObject by lazy {
println("Initialising...")
ExpensiveObject() // only created when first accessed
}
// Thread-safe by default (LazyThreadSafetyMode.SYNCHRONIZED)
// lateinit — for var, initialised later (not in constructor)
lateinit var adapter: RecyclerAdapter
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
adapter = RecyclerAdapter() // initialised here
}
// Key differences:
// lazy → val only, auto-initialised on first access, thread-safe
// lateinit → var only, manually initialised, NOT nullable, NOT primitive types
// Check if lateinit is initialised:
if (::adapter.isInitialized) { /* safe to use */ }
19. What are inline functions and why use them?
// inline copies the function body to the call site at compile time
// Avoids the overhead of creating lambda objects and function calls
inline fun measureTime(block: () -> Unit): Long {
val start = System.currentTimeMillis()
block()
return System.currentTimeMillis() - start
}
val time = measureTime {
// compiler pastes this lambda's code directly here
// no Function object created, no invoke() call
heavyWork()
}
// Use inline when:
// - Function takes lambda parameters (avoids lambda object allocation)
// - Function is small (large inline functions bloat bytecode)
// - You need reified type parameters
20. What is destructuring in Kotlin?
// Break an object into its components
data class User(val name: String, val age: Int)
val (name, age) = User("Alice", 25) // destructuring
println(name) // "Alice"
println(age) // 25
// Works in for loops
val users = listOf(User("Alice", 25), User("Bob", 30))
for ((name, age) in users) {
println("$name is $age")
}
// Works with maps
val map = mapOf("a" to 1, "b" to 2)
for ((key, value) in map) {
println("$key = $value")
}
// Works with Pair and Triple
val (first, second) = Pair("hello", 42)
21. What is the difference between const val and val?
// const val — compile-time constant (inlined by compiler)
// Must be: top-level or in companion object, primitive or String only
const val MAX_RETRIES = 3 // replaced with "3" everywhere at compile time
// val — runtime read-only (computed at runtime)
val currentTime = System.currentTimeMillis() // different value each time
companion object {
const val TAG = "MyClass" // ✅ compile-time
val instance = MyClass() // ✅ runtime, computed once
// const val obj = MyClass() // ❌ const only for primitives/String
}
22. What are type checks and smart casts?
// is — check type at runtime
fun process(value: Any) {
if (value is String) {
// Smart cast — compiler knows value is String here
println(value.length) // no explicit cast needed
}
if (value is Int) {
println(value + 1) // smart cast to Int
}
}
// when with smart cast
fun describe(obj: Any): String = when (obj) {
is String -> "String of length ${obj.length}"
is Int -> "Int with value $obj"
is List<*> -> "List of size ${obj.size}"
else -> "Unknown"
}
// as — explicit (unsafe) cast
val str = value as String // throws ClassCastException if wrong
// as? — safe cast
val str = value as? String // null if wrong type
Collections
23. What is the difference between List and MutableList?
// List — read-only (cannot add/remove/modify)
val readOnly: List<String> = listOf("a", "b", "c")
// readOnly.add("d") // ❌ no add method — compile error
// MutableList — read-write
val mutable: MutableList<String> = mutableListOf("a", "b", "c")
mutable.add("d") // ✅ works
// Same pattern for Set/MutableSet and Map/MutableMap
// Best practice: expose read-only, keep mutable private
class Repository {
private val _items = mutableListOf<Item>()
val items: List<Item> = _items // external code can't modify
}
24. What is the difference between map, flatMap, and filter?
val numbers = listOf(1, 2, 3, 4, 5)
// map — transform each element
val doubled = numbers.map { it * 2 } // [2, 4, 6, 8, 10]
// filter — keep elements matching a condition
val evens = numbers.filter { it % 2 == 0 } // [2, 4]
// flatMap — transform + flatten nested lists
val nested = listOf(listOf(1, 2), listOf(3, 4))
val flat = nested.flatMap { it } // [1, 2, 3, 4]
// Real example: get all tags from all articles
val allTags = articles.flatMap { it.tags } // flattened list of all tags
25. What is the difference between Sequence and Iterable?
// Iterable (List) — EAGER: each operation creates a new list
val result = (1..1_000_000)
.toList()
.filter { it % 2 == 0 } // creates intermediate list
.map { it * 2 } // creates another intermediate list
.take(10) // only needed 10!
// Sequence — LAZY: processes elements one at a time, no intermediate lists
val result = (1..1_000_000)
.asSequence()
.filter { it % 2 == 0 } // no intermediate list
.map { it * 2 } // no intermediate list
.take(10) // stops after finding 10
.toList() // terminal operation triggers execution
// Use Sequence when: large collections + chained operations
// Use List when: small collections or single operations
Coroutines (Quick Hits)
26. What is the difference between launch and async?
// launch — fire and forget, returns Job, no result
viewModelScope.launch {
repository.saveData(data) // don't need the result
}
// async — returns Deferred<T>, call await() to get result
viewModelScope.launch {
val user = async(Dispatchers.IO) { api.getUser() }
val posts = async(Dispatchers.IO) { api.getPosts() }
showProfile(user.await(), posts.await()) // parallel execution
}
// Key: async is for PARALLEL work INSIDE a coroutine
// Don't use viewModelScope.async {} as an entry point — use launch
27. What is the difference between withContext and async?
// withContext — SUSPEND FUNCTION, sequential, switches dispatcher
val data = withContext(Dispatchers.IO) { api.getData() } // waits here
processData(data) // runs AFTER withContext returns
// async — COROUTINE BUILDER, parallel, creates new coroutine
val a = async { api.getA() } // starts immediately
val b = async { api.getB() } // starts immediately
use(a.await(), b.await()) // waits for both
// withContext: sequential, one thing at a time
// async: parallel, multiple things at once
28. What is structured concurrency?
// Structured concurrency means: every coroutine has a parent scope
// and follows three rules:
// 1. Parent waits for all children to complete
// 2. Cancelling parent cancels all children
// 3. Child failure cancels parent and siblings (with regular Job)
viewModelScope.launch { // parent
launch { fetchUser() } // child — automatically cancelled if parent cancels
launch { fetchPosts() } // child — automatically cancelled if parent cancels
}
// When ViewModel is destroyed → viewModelScope cancelled → both children cancelled
// No manual cleanup needed — that's the power of structured concurrency
29. What is the difference between StateFlow and SharedFlow?
// StateFlow — holds current value, replays latest to new collectors
// Use for: UI state (loading, success, error)
val _uiState = MutableStateFlow(UiState.Loading)
// Always has a value, skips duplicate emissions
// SharedFlow — no current value, configurable replay
// Use for: one-time events (snackbar, navigation)
val _events = MutableSharedFlow<UiEvent>()
// No initial value, delivers every emission including duplicates
// StateFlow = observable state (like LiveData)
// SharedFlow = event bus (like SingleLiveEvent)
30. Why should you always re-throw CancellationException?
// CancellationException is how coroutines communicate cancellation
// Swallowing it breaks structured concurrency:
// ❌ BAD — coroutine thinks it's still running after cancellation
try {
suspendingCall()
} catch (e: Exception) {
log(e) // catches CancellationException too!
}
// ✅ GOOD — cancellation propagates correctly
try {
suspendingCall()
} catch (e: CancellationException) {
throw e // always re-throw
} catch (e: Exception) {
log(e) // handle real errors
}
// If you swallow CancellationException:
// - Parent thinks child is still running
// - Resources aren't freed
// - Scope cancellation doesn't work properly
Summary
- These 30 questions cover the core topics interviewers ask about: null safety, functions, OOP, collections, and coroutines
- Don’t just memorise answers — understand the why behind each feature
- Be ready to write code on a whiteboard — practice the examples above
- For deeper coroutine questions, see our dedicated Coroutines Interview Questions post
- For deeper Flow questions, see our dedicated Flow Interview Questions post
- For deeper OOP questions, see our dedicated OOP Interview Questions post
The best interview preparation is building real apps. Every question above maps to a real Android development scenario — and interviewers can tell the difference between someone who memorised an answer and someone who’s used it in production.
Happy coding!
Comments (0)