📗 Core Concept·~40 min read·Intermediate → Advanced
Kotlin: Interview Deep Dive
The Kotlin topics that separate a confident Android candidate from a shaky one — null safety, data and sealed classes, scope functions, lambdas, delegation, generics, sequences, inheritance, and exceptions, all explained with the depth interviewers actually probe for.
🚧 Null Safety
📦 Data & Sealed Classes
🔧 Scope Functions
🧬 Generics
⏳ Delegation
Null safety
Kotlin’s type system bakes nullability into the type itself. String and String? are two different types at compile time. The compiler will refuse to let you call a method on a String? without first proving the value is non-null. This single decision eliminates an entire class of NullPointerExceptions that plague Java codebases.
The operators you need to know cold:
Operator
Name
What it does
Throws?
?.
Safe call
Returns null if receiver is null, otherwise calls the member
Never
?:
Elvis
Returns right side if left is null
Never
!!
Non-null assertion
Unwraps nullable or throws KotlinNullPointerException
Yes
as?
Safe cast
Returns null if cast fails instead of throwing ClassCastException
Never
let
Null-safe block
Executes block only if receiver is non-null; it = non-null value
Never
📋 Kotlin null safety — which operator to reach for
Null safety — all operators in context
// Safe call — returns null if user is null, no crashval city = user?.address?.city
// Elvis — provide a fallback when nullval displayName = user?.name ?: "Guest"// Chain safe call + Elvis — very common patternval length = name?.length ?: 0// let — execute a block only when non-null
user?.let { u ->
showProfile(u) // u is non-null String inside the block
}
// Safe cast — null instead of ClassCastExceptionval tv = view as? TextView// null if view is not a TextView// !! — the "I know what I'm doing" operator. Avoid unless truly certain.val nonNull = maybeNull!! // KotlinNullPointerException if null// lateinit — non-null var initialised after construction (e.g. DI, Android lifecycle)lateinit var binding: ActivityMainBinding// Check before use: if (::binding.isInitialized) { ... }// Platform types — Java interop returns T! (unknown nullability)// Assign to a typed variable immediately to get compile-time safetyval name: String? = intent.getStringExtra("key") // explicitly nullable
⚠️ !! is a code smell
Every !! in your codebase is a bet that this value will never be null at runtime. Interviewers notice overuse of !! — it suggests you’re writing Kotlin like Java. The correct response to a nullable value is to handle the null case explicitly with ?., ?:, or let. Reserve !! for places where a null is genuinely a programmer error that should crash fast.
Data classes
A data class is a class whose primary purpose is to hold data. Kotlin generates five things for you automatically based on the properties declared in the primary constructor: equals(), hashCode(), toString(), copy(), and componentN() functions for destructuring. This covers probably 80% of the boilerplate you write in Java model classes.
Data class — what the compiler generates and how to use it
data classProduct(val id: Int, val name: String, val price: Double)
// ── equals / hashCode ────────────────────────────────────────────────────val p1 = Product(1, "Shirt", 499.0)
val p2 = Product(1, "Shirt", 499.0)
println(p1 == p2) // true — structural equality, not reference
println(p1 === p2) // false — different objects in memory// ── copy — immutable update pattern ──────────────────────────────────────val discounted = p1.copy(price = 399.0) // new object, only price changed// ── Destructuring via componentN ──────────────────────────────────────────val (id, name, price) = p1 // calls component1(), component2(), component3()
products.forEach { (id, name, price) ->
println("$id: $name = ₹$price")
}
// ── toString ─────────────────────────────────────────────────────────────
println(p1) // Product(id=1, name=Shirt, price=499.0)// ── Data class rules ──────────────────────────────────────────────────────// 1. Properties in PRIMARY constructor are included in equals/hashCode/copy// 2. Properties in the body are NOT included — common gotcha!data classUser(val id: Int, val name: String) {
var sessionToken: String? = null// NOT part of equals/hashCode!
}
// 3. Cannot be open/abstract — data classes are implicitly final// 4. Must have at least one val/var in the primary constructor
⚠️ The body property gotcha
Properties declared in the class body are invisible to equals(), hashCode(), and copy(). Two User instances with different sessionTokens will still be == if their id and name match. This trips people up when using data classes as map keys or in sets.
Sealed classes & sealed interfaces
A sealed class restricts class hierarchies: all direct subclasses must be declared in the same package and compilation unit. This gives the compiler a closed world — it knows every possible subtype at compile time. The payoff is exhaustive when expressions: if you handle every subclass, no else branch is required, and the compiler errors if you add a new subclass without updating every when.
This makes sealed classes the perfect type for modelling states and results — the two most common use cases in Android (UI state machines and network response wrappers).
📋 Sealed class hierarchy — compiler knows every subtype
Sealed class — UiState pattern + sealed interface
// Sealed class — all subclasses in same file/packagesealed classUiState<out T> {
objectLoading : UiState<Nothing>()
data classSuccess<T>(val data: T) : UiState<T>()
data classError(val message: String, val cause: Throwable? = null) : UiState<Nothing>()
objectEmpty : UiState<Nothing>()
}
// Exhaustive when — compiler error if a subclass is added but not handledwhen (state) {
isUiState.Loading -> showSpinner()
isUiState.Success -> showData(state.data) // smart cast: state is Success<T>isUiState.Error -> showError(state.message)
isUiState.Empty -> showEmptyView()
// No else branch needed — the compiler knows these are all cases
}
// Sealed interface (Kotlin 1.5+) — allows multi-inheritance of implementationssealed interfaceNetworkResult<out T>
data classNetworkSuccess<T>(val value: T) : NetworkResult<T>
data classNetworkError(val code: Int) : NetworkResult<Nothing>
// Sealed vs Enum — know the difference cold// Enum: each constant is a singleton, same type, no per-instance dataenum classDirection { NORTH, SOUTH, EAST, WEST }
// Sealed: each subclass can be a different type with different properties// Use sealed when subclasses need to carry different data
Feature
Sealed class
Enum class
Per-instance data
Yes — each subclass has its own fields
No — all constants share the same shape
Subclass type
Each subclass is a distinct type
Every constant is the same enum type
Instantiation
Can have multiple instances (data class) or singleton (object)
Each constant is a singleton
Exhaustive when
Yes (compiler enforced)
Yes (compiler enforced)
Typical use
UI state, network results, algebraic types
Fixed set of constants: directions, days, status codes
Scope functions
Scope functions (let, run, with, apply, also) are higher-order functions that execute a block in the context of an object. They all do roughly the same thing — run a block — but differ on two axes: how the context object is referenced inside the block (this vs it), and what the function returns (the lambda result or the context object itself). Getting these two axes right is the key to using them idiomatically.
📋 Scope functions — two axes, five functions
Scope functions — the right tool for each job
// apply — configure a new object, returns the object itself (this = receiver)val dialog = AlertDialog.Builder(context).apply {
setTitle("Confirm")
setMessage("Are you sure?")
setPositiveButton("Yes") { _, _ -> onConfirm() }
}.create()
// let — null-safe execute + transform (it = parameter)val upper = name?.let { it.uppercase() } // null if name is null// run — compute a result using the object's members (this = receiver)val fullName = user.run { "$firstName $lastName" }
// with — group operations, not an extension functionval summary = with(order) {
"Order #$id: $itemCount items, total ₹$total, status: $status"
}
// also — side effects (logging, validation) without interrupting a chainval users = fetchUsers()
.also { Log.d("TAG", "Fetched ${it.size} users") }
.filter { it.isActive }
// Memory hook: apply/also return the object (good for builders + side effects)// let/run/with return the lambda result (good for transformations)// apply/run use "this"; let/also use "it"
Lambdas & higher-order functions
A higher-order function is any function that takes a function as a parameter or returns one. This is the foundation of Kotlin’s functional style — map, filter, let, apply, and every coroutine builder are all higher-order functions. A lambda is an anonymous function literal that can be passed as a value.
Lambdas & higher-order functions — syntax and function types
// Function type: (InputType) -> ReturnTypeval double: (Int) -> Int = { x -> x * 2 }
val greet: (String) -> Unit = { name -> println("Hello, $name") }
// Trailing lambda syntax — if the last param is a function, move it outside ()listOf(1, 2, 3).forEach { println(it) } // it = implicit single parameterlistOf(1, 2, 3).forEach { num -> println(num) } // explicit name// Returning from a lambda — use a label, not "return" (which exits the enclosing fn)listOf(1, 2, 3).forEach forEach@{
if (it == 2) return@forEach // returns from lambda, continues outer loop
println(it)
}
// Writing a higher-order functionfun <T> List<T>.findFirst(predicate: (T) -> Boolean): T? {
for (item inthis) if (predicate(item)) return item
return null
}
val first = products.findFirst { it.price < 500 }
// Function references — :: syntax avoids creating a lambda wrapperval names = users.map(User::name) // member referencelistOf("a", "b").forEach(::println) // top-level function reference// Nullable function type — the () -> Unit can itself be nullfundoWork(onComplete: (() -> Unit)? = null) {
// work...
onComplete?.invoke() // safe call on the function itself
}
Inline functions & reified generics
Every time you pass a lambda to a function, Kotlin creates an anonymous class object and a method call at runtime — an allocation you pay for on every invocation. For frequently-called higher-order functions this overhead adds up. The inline keyword tells the compiler to copy-paste the function body (and the lambda body) directly at the call site instead of creating a function call. No object allocation, no virtual dispatch.
reified unlocks a second superpower of inlining: access to the actual type argument at runtime. Normally, generics are erased at runtime (type erasure from the JVM). But because an inline reified function is copy-pasted at the call site, the compiler knows the concrete type — it substitutes T::class.java with the real class wherever it appears in the function body.
inline / noinline / crossinline / reified
// inline — compiler pastes the body at call site, no lambda object createdinline funmeasureMs(block: () -> Unit): Long {
val start = System.currentTimeMillis()
block()
returnSystem.currentTimeMillis() - start
}
// At call site, this becomes: val start = ...; doWork(); return ... — no allocationval ms = measureMs { doWork() }
// reified — use T as a real class inside the function bodyinline fun <reified T : ViewModel> Fragment.viewModel(): T =
ViewModelProvider(this)[T::class.java]
// Without reified you'd need to pass the class explicitly every time:// fun <T : ViewModel> Fragment.viewModel(clazz: Class<T>): T = ViewModelProvider(this)[clazz]inline fun <reified T> String.fromJson(): T = Gson().fromJson(this, T::class.java)
val product: Product = jsonString.fromJson() // T inferred as Product// noinline — opt a specific lambda param OUT of inlining// needed when you store the lambda or pass it to a non-inline functioninline funrunWithCallback(action: () -> Unit, noinline callback: () -> Unit) {
action()
handler.post(callback) // handler.post needs a Runnable object — can't inline
}
// crossinline — prevents non-local returns inside a lambda that will run in// a different execution context (e.g. passed to Runnable)inline funrunOnMain(crossinline block: () -> Unit) {
handler.post { block() } // block can't do `return` — that would escape runOnMain's caller
}
💡 When to use inline
Use inline for utility functions that accept lambdas and are called frequently — measurement helpers, retry wrappers, DSL builders, scope-function-style APIs. Don’t inline large functions; the body gets copy-pasted everywhere which inflates bytecode. The compiler will warn you if an inline function has no lambda parameters (there’s nothing to inline away).
Extension functions & properties
Extension functions let you add new functions to existing classes without inheriting from them or wrapping them. Under the hood, the compiler generates a static method that takes the receiver as its first parameter — there is no actual modification of the class. This has two important implications: extension functions cannot access private or protected members, and if an extension function has the same signature as a member function, the member always wins.
Extension functions — practical patterns
// Basic extension functionfunString.isValidEmail(): Boolean =
isNotEmpty() && Patterns.EMAIL_ADDRESS.matcher(this).matches()
funView.visible() { visibility = View.VISIBLE }
funView.gone() { visibility = View.GONE }
funView.invisible() { visibility = View.INVISIBLE }
// Extension property (computed, no backing field)valInt.dp: Intget() = (this * Resources.getSystem().displayMetrics.density).toInt()
val padding = 16.dp // 16dp in pixels// Extension on nullable receiver — can be called on null without crashingfunString?.orDash(): String = this ?: "—"val display = user?.name.orDash() // works even if user or name is null// Extension on a companion object — for factory-style helpersfunIntent.Companion.openBrowser(url: String): Intent =
Intent(Intent.ACTION_VIEW, Uri.parse(url))
// The member-wins rule: if String already has contains(), your extension won't shadow itfunString.contains(s: String): Boolean = false// never called — member wins"hello".contains("ell") // returns true — the standard member is used
Object declarations & companion objects
Kotlin’s object keyword creates a thread-safe singleton without any of the Java double-checked locking boilerplate. The instance is created lazily on first access, guaranteed by the class loader. A companion object is an object scoped to a class — it’s the Kotlin equivalent of static members, but richer because it’s a real object that can implement interfaces and have extension functions defined on it.
// Object declaration — thread-safe singleton, initialised lazilyobjectAppConfig {
const val BASE_URL = "https://api.example.com"val retrofit bylazy { /* build Retrofit */ }
funisDebug() = BuildConfig.DEBUG
}
AppConfig.BASE_URL
AppConfig.isDebug()
// Companion object — belongs to the class, not the instanceclassUserprivate constructor(val name: String, val email: String) {
companion object {
// Factory method — can validate before constructingfuncreate(name: String, email: String): User? {
if (name.isBlank() || !email.isValidEmail()) return nullreturnUser(name, email)
}
const val MAX_NAME_LENGTH = 50
}
}
val user = User.create("Alice", "alice@example.com")
// Anonymous object — one-off implementation without a class nameval listener = object : View.OnClickListener {
override funonClick(v: View) { handleClick() }
}
// Note: anonymous objects are NOT singletons — new instance each time they appear// @JvmStatic — makes companion member callable as a Java staticcompanion object {
@JvmStaticfunnewInstance(): MyFragment = MyFragment()
}
// Java: MyFragment.newInstance() — instead of MyFragment.Companion.newInstance()
Delegation
Kotlin has first-class support for the delegation pattern via the by keyword. It shows up in three places: property delegation (by lazy, by Delegates.observable), interface delegation (forwarding method calls to another object), and by viewModels() / by activityViewModels() in Android — which are all property delegates under the hood.
// by lazy — compute once, cache forever. Thread-safe by default.val prefs bylazy { context.getSharedPreferences("app", MODE_PRIVATE) }
// LazyThreadSafetyMode.NONE — slightly faster, single-thread onlyval cache bylazy(LazyThreadSafetyMode.NONE) { HashMap<String, Any>() }
// by Delegates.observable — watch a property for changesvar query: StringbyDelegates.observable("") { _, old, new ->
if (old != new) triggerSearch(new)
}
// by Delegates.vetoable — reject invalid assignmentsvar age: IntbyDelegates.vetoable(0) { _, _, new -> new >= 0 }
age = -5// rejected — age stays 0// Interface delegation — forward all MutableList methods to inner, override addclassLoggingList<T>(private val inner: MutableList<T> = mutableListOf()) :
MutableList<T> by inner {
override funadd(element: T): Boolean {
Log.d("List", "Adding $element")
return inner.add(element)
}
}
// Custom delegate — SharedPreferences-backed propertyclassPrefDelegate(private val prefs: SharedPreferences, private val key: String) :
ReadWriteProperty<Any?, String> {
override fungetValue(thisRef: Any?, property: KProperty<*>) =
prefs.getString(key, "") ?: ""override funsetValue(thisRef: Any?, property: KProperty<*>, value: String) =
prefs.edit().putString(key, value).apply()
}
var authToken: StringbyPrefDelegate(prefs, "auth_token")
Generics & variance
Generics let you write type-safe code that works across multiple types. Kotlin’s generics add declaration-site variance via in and out modifiers — a significant improvement over Java’s use-site wildcards (? extends T, ? super T). Understanding variance is the difference between a vague answer and a sharp one in an interview.
Invariant (default): MutableList<String> is NOT a MutableList<Any> — even though String is a subtype of Any. This is safe because a mutable list could have add(42) called on it, breaking the type contract.
out (covariant): out T means the class only produces T values (returns them) — it never consumes them (takes them as input). Because of this, a Producer<String> can safely be assigned to a Producer<Any>. You can only read T, not write it.
in (contravariant): in T means the class only consumes T values. A Comparator<Any> can be used as a Comparator<String> — if it can compare anything, it can certainly compare strings.
📋 Generics variance — in / out / invariant
Generics — variance, reified, upper bounds, star projection
// out (covariant) — producer, can only return TinterfaceSource<out T> { funnext(): T }
val strings: Source<String> = StringSource()
val any: Source<Any> = strings // OK — Source is covariant in T// in (contravariant) — consumer, can only accept TinterfaceSink<in T> { funaccept(item: T) }
val anySink: Sink<Any> = LogSink()
val strSink: Sink<String> = anySink // OK — Sink is contravariant in T// Upper bound constraintfun <T : Comparable<T>> max(a: T, b: T): T = if (a > b) a else b
// Multiple upper bounds with wherefun <T> process(item: T) where T : Serializable, T : Comparable<T> { /* ... */ }
// reified — access T::class at runtime (requires inline)inline fun <reified T : Any> Bundle.getParcelable(key: String): T? =
getParcelable(key, T::class.java)
// Star projection — "I don’t know or care what T is"funprintList(list: List<*>) { // accepts List<Int>, List<String>, anything
list.forEach { println(it) } // elements are Any? — can read but not write
}
Collections & sequences
Kotlin’s collection operations (map, filter, flatMap, etc.) are eager by default: each operation processes all elements immediately and creates a new intermediate list. For short collections this is perfectly fine and often faster due to simpler iteration. But for long pipelines on large collections, this wastes both memory and CPU — you might allocate several lists before ever using the final result.
Sequences are lazy: they process one element at a time through the entire pipeline before moving to the next element. No intermediate lists are created. More importantly, sequences can short-circuit — a .first() at the end stops processing as soon as one element passes all the filters, even in a list of a million items.
Collections vs sequences — eager vs lazy evaluation
// EAGER (default) — each step processes all elements, creates intermediate listsval result = (1..1_000_000)
.filter { it % 2 == 0 } // creates List of 500,000 elements
.map { it * 3 } // creates another List of 500,000 elements
.first() // returns 6 — but we processed a million items!// LAZY (sequence) — element by element through the entire pipelineval resultSeq = (1..1_000_000).asSequence()
.filter { it % 2 == 0 } // lazy — no list created
.map { it * 3 } // lazy — no list created
.first() // finds 2, multiplies by 3, returns 6. Stops. Done.// Result: checked 2 elements instead of 1,000,000// generateSequence — infinite or computed sequencesval fibonacci = generateSequence(1to1) { (a, b) -> b to a + b }
.map { it.first }
.take(10)
.toList() // [1, 1, 2, 3, 5, 8, 13, 21, 34, 55]// Common collection operators worth knowing coldval items = listOf(3, 1, 4, 1, 5)
items.groupBy { it % 2 } // Map<Int, List<Int>> — odd vs even
items.associate { it to it * it } // Map<Int,Int> — number to its square
items.partition { it > 3 } // Pair<List,List> — matching vs rest
items.fold(0) { acc, n -> acc + n } // 14 — like reduce with an initial value
items.windowed(3) // sliding windows of size 3
items.zipWithNext() // pairs of adjacent elements
items.flatMap { listOf(it, it) } // [3,3,1,1,4,4,...] — map then flatten
✅ When to use sequences
Use asSequence() when: (1) the pipeline has 3+ chained operations, (2) there’s a terminal short-circuit operation like first() or any(), or (3) the collection is large (>1000 elements). For small collections with 1–2 operations, the overhead of a Sequence object makes it slower than eager evaluation.
Inheritance
Kotlin classes are final by default — the opposite of Java. You must explicitly mark a class open to allow subclassing, and mark individual members open to allow overriding. This is a deliberate design choice: inheritance is a fragile relationship that should be intentional, not accidental.
Inheritance — open, abstract, interface, final, super
// open — opt-in to inheritance. final by default.open classAnimal(val name: String) {
open funspeak(): String = "..."// open — can overridefunbreathe() = println("$name breathes") // not open — cannot override
}
classDog(name: String) : Animal(name) {
override funspeak() = "Woof!"// breathe() cannot be overridden — compiler error if you try
}
// abstract — implicitly open, cannot be instantiated, members may be abstractabstract classShape {
abstract funarea(): Doublefundescribe() = "Area is ${area()}"// can call abstract members
}
// Interface — can have default implementations AND abstract membersinterfaceClickable {
funclick() // abstract — must implementfunshowOff() = println("I’m clickable") // default impl
}
// Multiple interface implementation — diamond problem handled by compilerinterfaceFocusable { funshowOff() = println("I’m focusable") }
classButton : Clickable, Focusable {
override funclick() = println("Clicked")
override funshowOff() { // MUST override when ambiguoussuper<Clickable>.showOff() // call specific supersuper<Focusable>.showOff()
}
}
// final override — prevent further overriding down the chainopen classRichButton : Clickable {
final override funclick() = println("RichButton click")
}
// Class vs object vs interface — key differences// class: can have state + behaviour, instantiable, can be open/abstract/final// abstract class: cannot instantiate, subclasses must implement abstract members// interface: no state (no backing fields), multiple implementation allowed// object: singleton, cannot be instantiated, can implement interfaces/extend classes
💡 Abstract class vs interface — the interview question
Abstract class: can hold state (constructor parameters, property fields with backing storage), supports single inheritance only. Use when subclasses share common state or a partial implementation. Interface: no backing state (properties are abstract or computed), supports multiple implementation. Use to define a contract. In Kotlin, interfaces can have default implementations, closing much of the gap with abstract classes.
Exception handling
Kotlin has no checked exceptions — this is a deliberate departure from Java. You never have to declare throws IOException or wrap code in try-catch just to satisfy the compiler. The philosophy: checked exceptions add noise but empirically don’t improve error handling; they’re often swallowed with empty catch blocks. In Kotlin you handle exceptions when you can actually do something about them.
Exception handling — try-catch, try as expression, runCatching
// try-catch-finally — standard formtry {
val result = parseJson(input)
processResult(result)
} catch (e: JsonParseException) {
Log.e("TAG", "Bad JSON", e)
} catch (e: NetworkException) {
showNetworkError()
} finally {
hideLoading() // always runs
}
// try is an expression — returns a valueval number = try { Integer.parseInt(input) } catch (e: NumberFormatException) { 0 }
// throw is also an expression — can appear in Elvis chainsval name = user?.name ?: throwIllegalStateException("User has no name")
// runCatching — wraps in kotlin.Result, good for functional error handlingval result = runCatching { api.fetchUser(id) }
result
.onSuccess { user -> showProfile(user) }
.onFailure { error -> showError(error.message) }
// getOrDefault / getOrElse / getOrThrowval user = runCatching { api.fetchUser(id) }.getOrDefault(User.ANONYMOUS)
val data = runCatching { parse(raw) }.getOrElse { handleError(it) }
// @Throws — annotate for Java callers who expect checked exceptions@Throws(IOException::class)
funreadFile(path: String): String = File(path).readText()
Common interview gotchas
== vs === and equals() in data classes
== in Kotlin calls equals() (structural equality). === is reference equality (same object in memory). For data classes == compares field values; for regular classes it defaults to reference equality unless you override equals(). This bites people who use regular classes in sets or as map keys without overriding both equals() and hashCode().
lateinit vs lazy — they’re not interchangeable
lateinit is for var (mutable), non-null, non-primitive properties initialised after construction — like injected dependencies or Android binding objects. It throws UninitializedPropertyAccessException if accessed before assignment. by lazy is for val (immutable), computed on first access and cached. They serve different purposes and cannot be swapped.
The Nothing type
Nothing is a type with no instances — a function that returns Nothing never returns normally (it always throws or loops forever). The compiler uses this to enable smart casts after throw expressions and to type emptyList() as List<Nothing> which is a subtype of every List<T> via covariance. In sealed classes, UiState.Loading : UiState<Nothing>() means Loading is a valid UiState<T> for any T.
Companion object initialisation order
Companion object properties are initialised when the class is loaded, before any instance is created — similar to Java’s static initialiser blocks. If companion object initialisation throws, the class becomes unusable for the rest of the JVM process. Avoid heavy computation in companion object property initialisers; use by lazy to defer it.
How Kotlin language features connect to Android architecture
Kotlin’s language features aren’t just academic — they map directly to Android patterns. Sealed classes are the backbone of the UiState pattern in MVVM: Loading, Success(data), Error(message) modelled as a sealed hierarchy give the ViewModel a type-safe way to express every possible screen state. The exhaustive when in the Fragment means a new state is never silently ignored. Delegation shows up everywhere: by viewModels(), by activityViewModels(), by lazy { binding } in Fragments, and by savedStateHandle() for state preservation are all property delegates. Inline + reified powers the extension functions that make Hilt and Navigation Compose type-safe without requiring class tokens. Extension functions let you add context-specific utilities (View.visible(), String.isValidEmail()) to Android classes without subclassing. Sequences matter in data processing pipelines — mapping and filtering large lists from a database or network before rendering.
🎯 Interview summary
Null safety: ?. safe call, ?: Elvis, !! last resort, as? safe cast, ?.let null-safe block. Data class: generates equals/hashCode/toString/copy/componentN from primary constructor only. Body properties are excluded. Cannot be open. Sealed class vs enum: sealed subclasses can be different types with different data; enums are same type, same shape. Both give exhaustive when. Scope functions: apply/run/with use this; let/also use it. apply/also return the object; let/run/with return lambda result. inline: eliminates lambda object allocation by pasting body at call site. reified: access T::class at runtime in inline functions. Extension functions: static methods under the hood; cannot access private members; member always wins over extension with same signature. by lazy: val, computed once on first access, thread-safe. lateinit: var, non-null, initialised after construction. out T: covariant (producer), can widen to supertype. in T: contravariant (consumer), can narrow to subtype. Sequences: lazy, element-by-element, short-circuit. Use for 3+ operations on large collections or when terminal is first/any. Nothing: no instances, returned by functions that always throw. Enables smart casts after throw expressions. Checked exceptions: Kotlin has none. Use runCatching / Result for functional error handling.