androidinterview.com

Kotlin Interview Questions

47 questions

Tier
Difficulty
Level

Showing all 47 questions

Language Basics & Keywords

Interviews open here more often than anywhere else, usually with val versus var or with null safety.

What is the advantage of using const in Kotlin? (const val vs val)

Tier: EssentialDifficulty: EasyAsked at: meesho

const val gets inlined by the compiler at compile time, so there's no runtime lookup cost, while a plain val in an object is a real property that gets read through a getter call every time you use it.

object Constants {
    const val BASE_URL = "https://api.example.com"
    val cachedValue = "computed"
}

If you decompile the bytecode, every place that references BASE_URL has the literal string "https://api.example.com" baked directly into it. There's no Constants object being touched at all at that call site. A reference to cachedValue, on the other hand, compiles down to Constants.INSTANCE.getCachedValue(), an actual method call on a singleton instance, every single time.

The tradeoff is that const val only works for compile time constants. That means primitive types and String, declared at the top level or inside an object, never inside a class instance or computed from a function call. val can hold anything, including a value computed at runtime. So the rule of thumb is this. Use const val for true fixed constants like API base URLs or keys, and reach for regular val when the value isn't known until runtime.

What is the difference between == and === in Kotlin?

Tier: EssentialDifficulty: Easy

== checks structural equality, meaning it compares values by calling equals(). === checks referential equality, meaning it only returns true if both variables point to the exact same object in memory.

data class Point(val x: Int, val y: Int)

val a = Point(1, 2)
val b = Point(1, 2)

a == b   // true, same values, equals() says they're equal
a === b  // false, two different objects on the heap

This is the reverse of Java, where == on objects checks reference identity and you're supposed to reach for .equals() for value comparison. Kotlin flips the default so that == is the one you use for everyday comparisons, and under the hood it's actually null-safe too, == on two nulls returns true and never throws.

For a data class specifically, == works out of the box because the compiler generates equals() based on the constructor properties. For a regular class with no custom equals(), == falls back to reference comparison, so it behaves the same as ===.

What is the difference between lateinit and lazy in Kotlin?

Tier: EssentialDifficulty: EasyAsked at: meesho

Both let you skip initializing a non-null property at declaration time, but lateinit is for a var you'll assign later yourself, and lazy is for a val that initializes itself the first time it's read.

lateinit var adapter: MyAdapter   // you assign it, e.g. in onCreate()

val config: Config by lazy {      // computed once, on first access
    loadExpensiveConfig()
}

lateinit only works on var, only on non-primitive types, and gives no callback when it happens. If you read it before assigning, you get an UninitializedPropertyAccessException at runtime. You can check ::adapter.isInitialized if you need to guard against that. It's the right tool for things like view binding or dependency injection targets, where the object genuinely can't exist until some later lifecycle callback runs.

lazy only works on val, and the initializer block runs exactly once, the first time the property is accessed, then the result is cached. It's the right tool when creating the object is expensive and you want to defer that cost until it's actually needed, and you don't need to reassign it afterward. By default lazy is also thread safe. It uses a lock so only one thread ever runs the initializer, though you can pass LazyThreadSafetyMode.NONE if you know you're single threaded and want to skip that overhead.

What is the difference between val and var?

Tier: EssentialDifficulty: Easy

val declares a read-only reference. You can assign it once and never reassign it. var declares a mutable reference. You can reassign it as many times as you want.

val name = "Amit"   // name = "Bob" would not compile
var count = 0
count = 1           // fine

The distinction is about the reference, not the object it points to. A val list is still a MutableList if you declared it as one. You can add and remove items. You just can't point that val at a different list afterward. That trips people up, so it's worth saying explicitly in an interview. val gives you immutability of the binding, not necessarily immutability of the object.

The practical guidance is to default to val everywhere and only reach for var when you actually need to reassign. It makes code easier to reason about, since you know a val won't change out from under you, which matters a lot once coroutines or multiple threads are touching the same state.

How do you do lazy initialization of variables in Kotlin?

Tier: CommonDifficulty: Easy

You use the lazy delegate, val something by lazy { ... }, and the block only runs the first time the property is read, then the result is cached for every read after that.

val config: Config by lazy {
    loadExpensiveConfig()
}

This only works on val, since the whole point is you compute the value once and never change it again. It's the right call whenever creating something is expensive, and you're not sure the property will even be used this run, like a heavy object built from disk or network data that half your users' sessions never touch.

By default, lazy is thread safe, only one thread can ever run the initializer block, and other threads block until it's done and then get the cached result. If you know for certain only one thread will ever touch the property, you can skip that locking overhead with lazy(LazyThreadSafetyMode.NONE) { ... }. There's also PUBLICATION mode, which allows the initializer to run more than once from multiple threads but guarantees every reader ends up seeing the same first result.

What are the visibility modifiers in Kotlin?

Tier: CommonDifficulty: Easy

Kotlin has four visibility modifiers, and which ones are legal, and what the default is, depends on whether you're looking at a top level declaration or a class member.

  • public, visible everywhere. This is the default for both top level declarations and class members if you write nothing at all.
  • internal, visible anywhere in the same module, invisible outside it.
  • protected, visible in the class it's declared in and any subclass, never legal at top level since there's no class to subclass.
  • private, visible only inside the file it's declared in, for a top level declaration, or only inside the class itself, for a member.

The detail that trips people up is that private means something different depending on where you write it. A private top level function is scoped to the whole file, so two functions in the same file can call each other's private declarations, but a private class member is scoped to the class body itself, invisible even to an extension function written lower down in the same file.

What does the ?: (Elvis) operator do in Kotlin?

Tier: CommonDifficulty: Easy

?: returns the expression on its left if that's not null, otherwise it evaluates and returns the expression on its right. It's Kotlin's shorthand for a null default.

val name: String? = getName()
val displayName = name ?: "Guest"

The right side isn't limited to a plain default value, it can be any expression, including one that never returns normally, which is a common pattern for guard clauses.

val user = findUser(id) ?: return
val config = loadConfig() ?: throw IllegalStateException("missing config")

Because return and throw are expressions in Kotlin, both of those compile fine, ?: just needs its right side to type check as whatever the left side's non-null type is, and Nothing, the type of code that never completes normally, satisfies that for any type. That's why ?: return reads so naturally, if the left side is null, the function exits right there, otherwise execution just continues with a guaranteed non-null value.

What does the open keyword do in Kotlin?

Tier: CommonDifficulty: Easy

open marks a class, function, or property as allowed to be subclassed or overridden. Without it, everything in Kotlin is final by default, the opposite of Java, where you have to explicitly add final to lock something down.

open class Animal {
    open fun makeSound() = "..."
}

class Dog : Animal() {
    override fun makeSound() = "Woof"
}

Marking Animal open lets Dog extend it at all, a non-open class can't be subclassed no matter what. Marking makeSound() open on top of that lets Dog override just that function, if it were left non-open, Dog could still extend Animal but couldn't touch makeSound(). Both the class and the specific member need open independently, one doesn't imply the other.

The reason Kotlin flips the default from Java is design intent. Every open class is an invitation for someone else to extend it and break assumptions you made about how it behaves, so Kotlin makes you opt into that deliberately instead of getting it by accident on every class you write. data class, sealed class, and enum class all still can't be marked open at all, since being subclassable contradicts what those keywords already guarantee about them.

What is an init block in Kotlin?

Tier: CommonDifficulty: Easy

An init block holds initialization code that runs as part of the primary constructor, since Kotlin's primary constructor can't contain a body of its own, only a parameter list.

class User(name: String) {
    val displayName: String

    init {
        displayName = name.trim().ifEmpty { "Guest" }
    }
}

A class can have more than one init block, and property initializers and init blocks both run in the exact order they're written in the class body, top to bottom, interleaved with each other. That ordering matters, an init block can only read a property declared above it, reading one declared below still sees its default or uninitialized value at that point, which is a real source of subtle bugs if you reorder code without thinking about it.

The common use for init is validation and derived state, things a simple property initializer can't express in one expression, like checking a constructor argument is valid and throwing if it isn't, or computing a property from more than one constructor parameter.

When should the lateinit keyword be used in Kotlin?

Tier: CommonDifficulty: Easy

Use lateinit when a property genuinely can't be initialized at the point you declare it, but you know it will be assigned before anything actually reads it, and you don't want to make it nullable just to work around that timing.

class UserFragment : Fragment() {
    private lateinit var binding: FragmentUserBinding

    override fun onCreateView(/* ... */): View {
        binding = FragmentUserBinding.inflate(inflater)
        return binding.root
    }
}

The classic cases are Android view binding, where the binding object can't exist until onCreateView runs, and dependency injection, where a field gets assigned by the framework right after construction rather than in the constructor itself. It also shows up a lot in test setup, a @Before method assigning a fresh instance of the class under test before every test method runs.

It's the wrong tool if there's real uncertainty about whether the property will be assigned before it's read, that's what a nullable type is for, since reading an unassigned lateinit var throws an UninitializedPropertyAccessException at runtime instead of forcing you to handle the missing case at compile time. It's also restricted to var properties of non-primitive types, so it's not an option for anything you want to treat as read-only, or for Int, Boolean, and the other primitives, since Kotlin needs a real object reference to represent the uninitialized state internally.

Functions & Lambdas

What are extension functions in Kotlin?

Tier: EssentialDifficulty: Easy

An extension function lets you add a new function to an existing class without inheriting from it or modifying its source, by defining the function outside the class with the type it extends as a receiver.

fun String.isValidEmail(): Boolean {
    return this.contains("@") && this.contains(".")
}

"[email protected]".isValidEmail()   // true

Inside the function, this refers to the receiver, the object it was called on, and you can call it with normal dot syntax as if it were a real member. Under the hood there's no magic, the compiler turns it into a plain static function that takes the receiver as its first parameter, so it can't access private or protected members of the class it extends.

The main reason to use them is keeping utility code close to where it reads naturally, without bloating a class you don't own, like adding .isValidEmail() to String instead of writing a free floating isValidEmail(str) function. The one gotcha worth knowing, extension functions are resolved statically based on the declared type at compile time, not the runtime type, so if a subclass has a member function with the same signature, the member always wins, and which extension gets called depends on the static type of the variable, not what it actually points to.

What are higher-order functions in Kotlin?

Tier: CommonDifficulty: Easy

A higher order function is a function that takes another function as a parameter, returns a function, or both. Kotlin treats functions as regular values you can pass around, so this is built into the language rather than something you fake with interfaces.

fun processList(items: List<Int>, action: (Int) -> Unit) {
    for (item in items) action(item)
}

processList(listOf(1, 2, 3)) { println(it) }

filter, map, and forEach on collections are all higher order functions in the standard library, each one takes a lambda that describes what to do per element instead of you writing the loop yourself. That's the main benefit, they let you describe what should happen without repeating the boilerplate of how to iterate, and they compose cleanly, list.filter { it > 0 }.map { it * 2 } reads like a pipeline.

Most of the standard library's higher order functions, map, filter, let, and friends, are also marked inline, which means the lambda gets copied directly into the call site at compile time instead of being allocated as a real object. That removes the runtime overhead you'd otherwise pay for passing a function around, so using them in a hot loop is cheap.

What are Lambdas in Kotlin?

Tier: CommonDifficulty: Easy

A lambda is an anonymous function you can pass around as a value, written as a block of code in curly braces instead of a named fun declaration.

val square: (Int) -> Int = { number -> number * number }

square(4)   // 16

If a lambda takes exactly one parameter, you can skip naming it and refer to it as it instead, { it * it } means the same thing as the example above. And when a lambda is the last parameter of a function, Kotlin lets you pull it outside the parentheses, which is why list.filter { it > 0 } is legal and reads more like a language feature than a function call.

The other thing worth knowing is that a lambda captures variables from the scope it's defined in, and unlike Java, those variables don't need to be final, a lambda can read and reassign a var from its enclosing function. That's what makes patterns like counting matches inside a forEach block possible without an external wrapper class.

What is an inline function in Kotlin?

Tier: CommonDifficulty: EasyAsked at: meesho

An inline function tells the compiler to copy the function's bytecode directly into every call site, instead of generating a real function call. Its main use is avoiding the runtime cost of the lambda objects that Kotlin would otherwise allocate for higher order functions.

inline fun measureTime(block: () -> Unit): Long {
    val start = System.currentTimeMillis()
    block()
    return System.currentTimeMillis() - start
}

Normally, passing a lambda to a function means the compiler generates a real object implementing a function interface, plus a virtual call to invoke it, which adds up when it happens in a hot path like a loop over a big list. Marking the function inline removes that entirely, both the function's own code and the lambda passed to it get pasted straight into the call site at compile time, so there's no object and no extra call, just a straight line of bytecode.

Inlining also unlocks two things a normal function can't do. A lambda parameter can use a non-local return, returning straight out of the calling function rather than just the lambda. And the function can take a reified type parameter, keeping the generic type available at runtime, since there's no real generic method left after inlining for type erasure to touch. The tradeoff is code size, every call site gets its own full copy of the function's body, so inline is meant for small functions, not something with a large body called from many places.

What is the reified keyword in Kotlin?

Tier: CommonDifficulty: Easy

reified keeps a generic type parameter's actual type available at runtime, inside an inline function, which normal generics in Kotlin and Java can't do because of type erasure.

inline fun <reified T> Gson.fromJson(json: String): T {
    return this.fromJson(json, T::class.java)
}

val user = gson.fromJson<User>(jsonString)

Ordinarily, T only exists at compile time, by the time your code runs on the JVM, generic type information has been erased, which is why a plain generic function can't write T::class.java or x is T, the runtime has no idea what T was. reified fixes that, but only inside an inline function, because the function's body gets pasted directly into every call site, and at each call site the compiler already knows the concrete type being used there, so it can substitute the real type in place of T when it inlines the code.

The most common real use is exactly the JSON parsing example above, letting fromJson<User>(json) know it should deserialize into User without you passing User::class.java as a separate argument every time. It also shows up for type checking, inline fun <reified T> Any.isInstanceOf() = this is T, and for filtering collections by type. The tradeoff is the same one every inline function has, the function's body gets duplicated at every call site, so it's meant for small utility functions, not something with a large implementation.

Classes & Objects

Data classes and sealed classes carry this section. Expect at least one of them in any Kotlin round.

How do you create a Singleton class in Kotlin (the object keyword)?

Tier: EssentialDifficulty: Easy

You declare it with object instead of class, and Kotlin guarantees there is exactly one instance of it for the whole application, created automatically the first time it's referenced.

object NetworkClient {
    val client = OkHttpClient()
    fun get(url: String) = client.newCall(/* ... */)
}

NetworkClient.get("https://api.example.com")

There's no constructor to call, you just reference the object by name and Kotlin gives you the instance. Under the hood, it compiles to a regular Java class with a private constructor and a static INSTANCE field, the same singleton pattern you'd hand write in Java, except the compiler writes it for you and there's no way to accidentally create a second instance.

An object can extend a class or implement interfaces, so it works fine anywhere a normal instance is expected, like an event listener or a repository implementation. The one thing to remember is that it's created lazily on first access but always eagerly once that happens, there's no way to defer or parameterize its construction the way you could with a factory function.

What are sealed classes in Kotlin and when should you use them?

Tier: EssentialDifficulty: Easy

A sealed class is a class whose full set of subclasses is known at compile time. All of them have to be declared in the same file (or, since Kotlin 1.5, the same module and package). That lets the compiler check a when expression over it exhaustively, with no else branch needed.

sealed class UiState {
    object Loading : UiState()
    data class Success(val data: List<Item>) : UiState()
    data class Error(val message: String) : UiState()
}

fun render(state: UiState) = when (state) {
    is UiState.Loading -> showSpinner()
    is UiState.Success -> showList(state.data)
    is UiState.Error -> showError(state.message)
}

The reason this matters is what happens later. If someone adds a new subclass, say UiState.Empty, every when over UiState that doesn't handle it stops compiling. With a regular open class and an else branch, that new case would silently fall into else and you might not notice for a while. Sealed classes turn a missed case into a compile error instead of a runtime surprise.

The classic use case is exactly that UI state example, modeling loading, success, and error as a closed set, and it's also common for one time navigation or side effect events. It's different from an enum in that each subclass can hold its own different data. An enum entry can't carry a different shape of payload per case the way Success here carries a list and Error carries a message.

What is a data class in Kotlin and what makes it interesting?

Tier: EssentialDifficulty: Easy

A data class is a class whose whole job is to hold data, and Kotlin automatically generates the boilerplate you'd otherwise write by hand for it. That means equals(), hashCode(), toString(), a copy() function, and componentN() functions for destructuring, all generated for you.

data class User(val id: String, val name: String)

val u1 = User("1", "Amit")
val u2 = u1.copy(name = "Bob")   // new object, id kept, name changed
val (id, name) = u2              // destructuring, uses component1/component2

Compare that to Java, where getting value equality and a readable toString() meant writing all of that yourself or leaning on a library. In Kotlin you get it for free just by putting data in front of class and having at least one constructor parameter.

There are two things worth mentioning in an interview. First, equals() and hashCode() are generated only from the properties declared in the primary constructor. A property added in the class body is ignored by both, which is a real footgun if you're not aware of it. Second, copy() makes data classes a natural fit for immutable state, especially in Compose or a ViewModel's UI state, where you never mutate the object in place. You produce a new copy with one field changed and swap it in.

What are companion objects in Kotlin?

Tier: CommonDifficulty: Easy

A companion object is a single object tied to a class declaration, and it's Kotlin's replacement for Java's static, since Kotlin classes have no static members of their own.

class User private constructor(val name: String) {
    companion object {
        fun create(name: String) = User(name)
    }
}

User.create("Amit")

You access its members through the class name directly, User.create(...), without needing an instance of User first, which is why it's the natural place for factory functions, constants, and constant sets tied to that class. There can only be one companion object per class, and if you don't give it a name it defaults to Companion, so User.Companion.create(...) also works.

Unlike a Java static block, a companion object is a real object, it can implement interfaces and extend classes, which lets you pass it somewhere that expects an interface implementation. And because it's just an object, it doesn't get compiled to actual static methods on the JVM by default, calling it from Java goes through the Companion.INSTANCE field unless you add @JvmStatic to expose true static methods.

Explain inline classes (value classes) in Kotlin.

Tier: CommonDifficulty: Medium

An inline class, now called a value class and declared with @JvmInline value class, wraps a single value in a type without paying for a wrapper object at runtime. The compiler represents it as the underlying value directly wherever it can, and only creates a real object when it genuinely has to.

@JvmInline
value class UserId(val value: String)

fun fetchUser(id: UserId) { /* ... */ }

The point is type safety without allocation cost. Passing a raw String around for a user id, an order id, and an email address all at once is easy to mix up by accident, the compiler can't tell them apart. Wrapping each in its own value class makes the type checker catch that mistake, fetchUser(orderId) won't compile, while at runtime the JVM mostly just sees a String again.

That said, boxing still happens in a few situations you should be able to name, when the value class is used as a generic type argument, when it's stored in a nullable type, or when it's referenced through an interface it implements. Those cases fall back to allocating a real wrapper object because the JVM has no other way to represent it.

Is a Kotlin object singleton thread-safe?

Tier: CommonDifficulty: Medium

Yes. An object in Kotlin compiles to a class whose single instance is created in a static initializer, and the JVM guarantees static initializers run exactly once, under a lock, no matter how many threads try to access the class at the same time.

object Repository {
    val cache = mutableMapOf<String, Data>()
}

That guarantee comes from the class loading mechanism itself, not from anything Kotlin adds on top. The first thread to touch Repository triggers class initialization, any other thread that tries to use it at the same moment blocks until that finishes, and every thread afterward just sees the already initialized instance. So you get thread safe, exactly once construction for free, without writing any locking code yourself.

What that guarantee does not cover is the object's mutable state after construction. In the example above, cache is a regular MutableMap, and multiple threads reading and writing it concurrently can still race and corrupt it. The singleton itself is safe, what you put inside it is your own problem to synchronize.

What are common use-cases for sealed classes in Android?

Tier: CommonDifficulty: Medium

Sealed classes show up anywhere Android code needs to model a closed, finite set of states or events, and let the compiler enforce that every case is handled.

  • UI state, modeling a screen as Loading, Success, or Error, each carrying whatever data that state needs.
  • One time events, like showing a toast or navigating to another screen, sent once through a Channel or SharedFlow so a configuration change doesn't replay them.
  • Network or repository results, wrapping a response as Result.Success or Result.Failure instead of relying on nullable types or thrown exceptions to signal failure.
  • Navigation destinations, representing each screen a NavHost can go to as its own subclass, often carrying the arguments that screen needs.

The common thread across all of these is a when expression somewhere downstream that has to handle every case. Because the compiler knows the full set of subclasses, forgetting a case in that when is a compile error, not a bug you find in production. That's the main reason teams reach for sealed classes over a plain enum or a set of booleans, an enum entry can't carry different data per case, and a Success state carrying a list looks nothing like an Error state carrying a message.

Collections

Tell me about the Collections API in Kotlin.

Tier: CommonDifficulty: Easy

Kotlin's collections split into three shapes, and each comes in a read-only and a mutable version.

  • List, an ordered collection that allows duplicates. MutableList adds add, remove, and index based writes.
  • Set, a collection with no duplicates. MutableSet adds add and remove.
  • Map, key to value pairs with unique keys. MutableMap adds put and remove.

The read-only interfaces, List, Set, Map, don't expose any mutating methods, but that doesn't mean the underlying object is actually immutable, another reference to the same collection typed as the mutable interface can still change it out from under you. True immutability comes from functions like listOf(), which under the hood returns a fixed size, truly unmodifiable list.

On top of the collection types, Kotlin gives you a large standard library of operators for transforming them without loops, map, filter, sortedBy, groupBy, reduce, fold, and dozens more, most of which run eagerly and build a new collection at each step. When you're chaining several of these over a large collection and want to avoid building an intermediate list at every step, asSequence() switches to lazy, one element at a time evaluation instead.

What is the difference between List and Array types in Kotlin?

Tier: CommonDifficulty: Easy

Array is a fixed size, mutable container backed directly by Java's array type. List is a Kotlin interface, read-only by default, with MutableList as the interface that adds mutation, and it's backed by a real collection class like ArrayList under the hood, not a raw array.

val array = arrayOf(1, 2, 3)      // fixed size, elements mutable, no add/remove
val list = listOf(1, 2, 3)        // read-only, no set, add, or remove at all
val mutable = mutableListOf(1, 2, 3)  // can add and remove freely

Array's size is fixed the moment you create it, you can overwrite an element with array[0] = 5, but you can never grow or shrink it, there's no add or remove at all. List and MutableList don't have that limitation, a MutableList can grow and shrink freely, since it's backed by a resizable structure rather than a raw array.

The other difference worth knowing is around primitives. An Array<Int> boxes every element as an Integer object on the JVM, the same overhead you'd get in Java. Kotlin gives you specialized types, IntArray, DoubleArray, BooleanArray, and so on, that store actual primitives with no boxing, which matters for large numeric arrays where allocation and memory overhead add up. In everyday Android code, List and MutableList are what you reach for almost always, Array mostly shows up at Java interop boundaries or in that primitive array case.

What is the difference between map and flatMap in Kotlin?

Tier: CommonDifficulty: Easy

map transforms each element into exactly one new element, one in, one out. flatMap transforms each element into a collection, then flattens all of those collections into a single flat list.

val words = listOf("hello world", "kotlin")

words.map { it.split(" ") }
// [[hello, world], [kotlin]]   a list of lists

words.flatMap { it.split(" ") }
// [hello, world, kotlin]        one flat list

Reach for map whenever the transformation naturally produces one result per input element, like turning a list of User objects into a list of their names. Reach for flatMap whenever the transformation naturally produces zero or more results per input element and you want them all merged into one collection, like turning a list of Order objects into a single flat list of every LineItem across all of them.

A useful way to remember it, flatMap { transform(it) } is exactly equivalent to map { transform(it) }.flatten(), flatMap just does both steps in one pass instead of building the intermediate nested list first.

Scope Functions

What are the different scope functions in Kotlin?

Tier: EssentialDifficulty: Medium

Kotlin has five, let, run, with, apply, and also, and they all do the same basic thing, execute a block of code against an object, they just differ in how you refer to that object inside the block and what the whole expression returns.

  • let, object as it, returns the lambda's result.
  • run, object as this, returns the lambda's result.
  • with, object as this, returns the lambda's result, called as with(obj) { } instead of obj.with { }.
  • apply, object as this, returns the object itself.
  • also, object as it, returns the object itself.

Here is each one, with what it refers to the object as and what it hands back.

val name: String? = "Ada"

// let, object is `it`, returns the lambda result
val length = name?.let { it.length }                  // 3

// run, object is `this`, returns the lambda result
val shouted = name?.run { uppercase() }               // "ADA"

// with, object is `this`, returns the lambda result,
// and takes the object as an argument instead of being called on it
val label = with(StringBuilder()) {
    append("Hello, ")
    append(name)
    toString()                                        // "Hello, Ada"
}

// apply, object is `this`, returns the object itself
val intent = Intent(context, DetailActivity::class.java).apply {
    putExtra("id", 42)
    flags = Intent.FLAG_ACTIVITY_CLEAR_TOP
}                                                     // returns the Intent

// also, object is `it`, returns the object itself
val numbers = mutableListOf(1, 2, 3).also {
    Log.d("TAG", "built a list of ${it.size}")
}                                                     // returns the list

Two of them, let and also, use it, so they read best when the block passes the object to another function rather than setting properties on it. The other three, run, with, and apply, use this, so they read best when the block is a series of property assignments or calls on the object directly, since you can drop the receiver name entirely inside.

Which one to reach for comes down to what you need back afterward. Need the object itself, to keep chaining or return it, use apply or also. Need a different, computed value, use let, run, or with. with is the odd one out mechanically since it's not an extension function, so it's the wrong choice on a nullable receiver, run covers that case instead with myNullable?.run { }.

Read more Scope functions (opens in a new tab)

How do you choose between apply and with?

Tier: CommonDifficulty: Easy

apply is an extension function you call on the object itself and it always returns that same object, so use it when you're configuring something and want to keep it as the result. with is not an extension function, you pass the object in as an argument, and it returns whatever the lambda's last expression evaluates to, so use it when you just want to run a batch of calls on an object and get a computed result back, not the object itself.

val paint = Paint().apply {
    color = Color.RED
    style = Paint.Style.FILL
}  // returns the configured Paint

val area = with(rect) {
    width * height
}  // returns an Int, not the Rect

The other practical difference is nullability. Because apply is an extension function, it works fine on a nullable receiver with the safe call operator, myView?.apply { ... }. with takes a plain argument, so it doesn't chain off ?. the same way, you'd need to null check before calling it. In practice, reach for apply when building or configuring an object, and reach for with when you already have a non-null object and want to compute something from it without repeating its name.

What is the apply scope function and what are its use cases?

Tier: CommonDifficulty: Easy

apply runs a block of code against an object, referring to it as this inside the block, and always returns that same object afterward. It's the scope function for configuring something right after you create it.

val textView = TextView(context).apply {
    text = "Hello"
    textSize = 16f
    setTextColor(Color.BLACK)
}

Because the block uses this as the receiver, you can drop the receiver name on every line inside, text = "Hello" instead of textView.text = "Hello", which is what makes a chain of property assignments read cleanly as a single configuration step instead of a sequence of separate statements repeating the variable name. And because it returns the same object it was called on, you can assign the whole expression straight to a val, as above, rather than declaring the variable first and configuring it in separate statements after.

The main use cases are object construction and configuration, building a view, a Paint, an Intent, or a builder style object where the library itself doesn't offer a fluent builder API. It's the wrong choice when you actually need a different, computed value back instead of the object itself, let or run are the right calls for that, since apply always hands you back what you started with.

What is the let scope function and what are its use cases?

Tier: CommonDifficulty: Easy

let runs a block of code against an object, referring to it as it inside the block, and returns whatever the block's last line evaluates to. It's the scope function most associated with null checks.

val name: String? = getName()

name?.let {
    println("Name is $it")
}

Combined with the safe call operator, ?.let { }, the block only runs at all if the object isn't null, so it's the standard way to execute code conditionally on a nullable value without writing an explicit if (name != null) check. Inside the block, it is smart cast to the non-null type, so you can call non-null-only functions on it directly.

The other common use is transforming a value inline as part of a chain, since let returns the block's result rather than the object itself, val length = name?.let { it.trim() }?.length. It's also useful for narrowing the scope of a temporary variable, computing something once and using it only within the block, without leaking that intermediate name into the surrounding function. Where let falls short is when you're setting several properties on the object rather than passing it somewhere, apply or run, which use this instead of it, read better for that.

Explain the use-cases of let, run, with, also and apply in Kotlin.

Tier: CommonDifficulty: Medium

Kotlin has five scope functions, and picking between them comes down to two questions, what does the block refer to the object as, and what does it return.

  • let, refers to the object as it, returns the lambda's result. Common for null checks with the safe call operator, value?.let { ... }, or for transforming one value into another inline.
  • run, refers to the object as this, returns the lambda's result. Good when you need to both configure an object and compute something from it in one block.
  • with, refers to the object as this, returns the lambda's result, but it's not an extension function so you pass the object in rather than calling it on the object. Good for grouping a batch of calls on an object you already know is non-null.
  • also, refers to the object as it, returns the object itself. Good for a side effect in the middle of a chain, like logging, that shouldn't change what gets passed downstream.
  • apply, refers to the object as this, returns the object itself. The standard choice for configuring an object right after constructing it.

A rule of thumb that covers most cases, if you need the object back, reach for apply or also. If you need a computed result back, reach for let, run, or with. Then pick this versus it based on whether you're mostly setting properties on the object, which reads better with this, or passing the object to other functions, which reads better with it.

Less common, worth knowing

These come up less often. Skim them once you are comfortable with everything above.

Language Basics & Keywords

How do you check if a lateinit variable has been initialized?

Tier: Less commonDifficulty: Easy

You check it with ::propertyName.isInitialized, a reference to the property followed by that special property that only exists on lateinit vars.

lateinit var adapter: MyAdapter

if (::adapter.isInitialized) {
    adapter.notifyDataSetChanged()
}

This avoids catching the UninitializedPropertyAccessException that reading an unassigned lateinit var throws, which is the alternative and much uglier way to guard against it. One restriction worth knowing, you can only check isInitialized from code that already has access to the property, inside the same class it's declared in, or from top level and same module code if it's a top level property. You can't check it on an arbitrary object from outside, obj::someLateinitVar.isInitialized only compiles if the property is visible and lexically reachable at that call site.

What are Labels in Kotlin?

Tier: Less commonDifficulty: Easy

A label lets you target a specific loop or lambda with break, continue, or return, instead of only ever affecting the nearest enclosing one. You write it as an identifier followed by @ right before the loop or lambda.

outer@ for (i in 1..3) {
    for (j in 1..3) {
        if (j == 2) continue@outer
        println("i=$i j=$j")
    }
}

Without the label, continue there would only skip to the next iteration of the inner loop. With continue@outer, it skips straight to the next iteration of the outer loop instead, which is the only way to reach that behavior in Kotlin since there's no unlabeled way to affect anything but the innermost loop.

The other common use is returning from a lambda passed to an inline function like forEach. A plain return inside that lambda would actually return from the enclosing function entirely, not just skip the current element, which surprises people coming from Java's for loops. Writing return@forEach returns only from the lambda, behaving like continue would in a real loop. Kotlin also lets you use the function name itself as an implicit label for exactly this case, so list.forEach { return@forEach } works without you declaring a label.

What does the internal visibility modifier do in Kotlin?

Tier: Less commonDifficulty: Easy

internal makes a declaration visible anywhere inside the same module, but invisible to any code outside it, sitting between public and private in how wide its reach is.

internal class AnalyticsLogger {
    internal fun log(event: String) { /* ... */ }
}

A module here means a Gradle module or a set of files compiled together, so code in your app module can freely use an internal class from another module you also own, but a separate library consuming your compiled artifact cannot see it at all, even though public classes right next to it are visible. That makes internal the right choice for implementation details you need to share across your own package boundaries, like between a data package and a domain package in the same module, but never want exposed as part of your library's public API.

It's a compile time check, not a runtime one, so it doesn't add any actual security, someone with access to bytecode can still call an internal member through reflection. Its real value is keeping your API surface honest, so consumers of your module only see what you intended to expose.

What is infix notation in Kotlin?

Tier: Less commonDifficulty: Easy

Infix notation lets you call a function without the dot and parentheses, writing a to b instead of a.to(b). It's just syntax sugar over a normal function call, and you can define your own with the infix keyword.

infix fun Int.times(str: String) = str.repeat(this)

3 times "ab"   // "ababab", same as 3.times("ab")

To be callable this way, a function has to satisfy a few requirements, it must be a member function or an extension function, it must take exactly one parameter, that parameter can't have a default value, and it can't accept a variable number of arguments. That's why to, which builds a Pair, and, or, xor on booleans, and until and step in ranges all read like keywords even though they're regular library functions marked infix.

The main reason to reach for it in your own code is readability for small, focused two argument operations, especially ones that read naturally as a verb between two things, like user shouldBe expectedUser in a test assertion library. It's easy to overuse though, and Kotlin style guides generally recommend keeping it to cases that genuinely read better without the dot.

What is the @JvmOverloads annotation in Kotlin?

Tier: Less commonDifficulty: Easy

@JvmOverloads tells the compiler to generate a separate Java-visible overload for every parameter that has a default value, so Java code can call the function with any of the shorter argument lists Kotlin callers already get for free.

class Greeting @JvmOverloads constructor(
    val name: String,
    val greeting: String = "Hello"
)

Kotlin code can already call Greeting("Amit") or Greeting("Amit", "Hi") without the annotation, since Kotlin understands default parameter values natively. Java has no concept of default parameters at all, so without @JvmOverloads, Java only ever sees a single constructor requiring every parameter, new Greeting("Amit", "Hello"), forcing you to always pass the default explicitly. Adding the annotation makes the compiler generate the shorter overloads too, one for each trailing default parameter dropped off in order, so new Greeting("Amit") compiles from Java as well.

It's purely an interop convenience, it changes nothing about how the function behaves or is called from Kotlin. It shows up constantly on Android custom View constructors, since a View needs the multi-argument constructors the Android framework calls via XML inflation, and @JvmOverloads is the easiest way to generate those from one Kotlin constructor with defaults instead of writing each overload by hand.

What is the @JvmStatic annotation in Kotlin?

Tier: Less commonDifficulty: Easy

@JvmStatic makes a function or property inside a companion object (or a plain object) compile to a real static member on the JVM, so Java code can call it directly on the class, without going through Companion.

class MathUtils {
    companion object {
        @JvmStatic
        fun square(x: Int) = x * x
    }
}

From Kotlin, MathUtils.square(4) already works with or without the annotation, Kotlin resolves companion object members through the class name either way. The difference only shows up from Java. Without @JvmStatic, Java has to write MathUtils.Companion.square(4), since without the annotation the method genuinely only exists as an instance method on the generated Companion object. With it, Java can call MathUtils.square(4) directly, because the compiler generates a true static method in addition to the instance one on Companion.

So the annotation exists purely for interop, it's there for library authors who want their Kotlin companion object functions to feel like ordinary static methods to any Java code consuming them, and it's a no-op in terms of how the same code behaves when called from Kotlin.

What is the difference between open and public in Kotlin?

Tier: Less commonDifficulty: Easy

They control two different things entirely. public is a visibility modifier, it decides who can see and use a declaration. open is about inheritance, it decides whether a class can be subclassed or a member can be overridden. A class can be public and still completely closed to subclassing, which is actually the default for every class in Kotlin.

public class Repository { }        // visible everywhere, but final, can't extend it

open class BaseRepository { }      // visible everywhere (public is implicit) and extendable

That's the detail that trips people coming from Java. In Java, public gets you both, visible everywhere and overridable by default, you have to add final to lock a class down. Kotlin separates the two concepts and defaults every class and member to closed for extension, final behavior, unless you say open. Visibility is the independent, separate question, and defaults to public if you write nothing.

So the two are orthogonal, you can combine them any way you like, public and open, public and final, internal and open, and so on. In an interview, the useful way to say it, public answers who can see this, open answers who can change what this does.

What is the equivalent of Java static methods in Kotlin?

Tier: Less commonDifficulty: Easy

Kotlin has no static keyword at all, and there are three different ways to get equivalent behavior depending on what you're trying to do.

  • Top level functions, a function declared directly in a file, outside any class. This is the most common replacement, and the idiomatic one, fun formatDate(date: Date): String { ... } sitting in its own file needs no class wrapper at all.
  • A companion object, companion object { fun create() = ... } inside a class, when the function is conceptually tied to that specific class, like a factory function.
  • An object declaration, when you want a full singleton with its own state, not just a couple of stateless helper functions.
// top level, no class needed
fun square(x: Int) = x * x

Under the hood, top level functions compile to static methods on an automatically generated class named after the file, MathUtilsKt for a file called MathUtils.kt, so from Java they already look and call exactly like ordinary static methods with no extra work. Companion object functions don't get that treatment automatically, they're instance methods on a generated Companion object unless you add @JvmStatic, which is the annotation that makes them true static methods for Java callers too.

What are Delegates in Kotlin?

Tier: Less commonDifficulty: Medium

A delegate hands off a property's get and set logic to another object, using the by keyword, instead of writing that logic inline in the property itself.

var name: String by Delegates.observable("initial") { _, old, new ->
    println("changed from $old to $new")
}

Kotlin ships several built in delegates you'll actually use. lazy computes a value once on first read and caches it. Delegates.observable runs a callback every time the property changes. Delegates.vetoable lets you reject a change before it happens. There's also a Map delegate, which reads and writes a property straight out of a Map<String, Any>, handy for parsing loosely typed data like JSON into a class.

You can write your own delegate too, any class that implements getValue and, for a var, setValue with the right signature works with by. That's how libraries build things like Android's by viewModels() or Jetpack Compose's by remember { mutableStateOf(...) }, both are ordinary property delegates under the hood, not compiler magic specific to those libraries.

Kotlin also supports class delegation, class Repo(impl: DataSource) : DataSource by impl, which forwards every method of an interface to another object automatically, so you only need to override the handful of methods you actually want to change.

What is the @JvmField annotation in Kotlin?

Tier: Less commonDifficulty: Medium

@JvmField exposes a Kotlin property to Java as a plain field, instead of the getter and setter methods Kotlin normally generates for it.

class Config {
    @JvmField
    var apiKey: String = ""
}

Without it, Java code calling into that class sees getApiKey() and setApiKey(String), never a real apiKey field. With @JvmField, Java sees an actual public field named apiKey it can read and assign directly, which matters for interop with Java libraries or frameworks that expect a real field through reflection, like some serialization or dependency injection libraries predating Kotlin support.

There are restrictions on when you're allowed to use it. The property can't have a custom getter or setter, can't be private, protected, or open, and can't be const (which already compiles to something similar) or lateinit (which is already exposed as a field). Kotlin code calling the same property still just sees a normal property either way, @JvmField only changes what Java sees.

Functions & Lambdas

What does crossinline do in Kotlin?

Tier: Less commonDifficulty: Easy

crossinline marks a lambda parameter of an inline function to say the lambda still gets inlined, but it's not allowed to use a non-local return, because it's going to be called from inside another execution context, like another lambda or a local object, where a non-local return wouldn't make sense.

inline fun runInBackground(crossinline action: () -> Unit) {
    Thread {
        action()   // called from inside another lambda, not directly
    }.start()
}

Without crossinline there, this wouldn't compile. Normally an inline function's lambda parameter is allowed to return straight out of the caller's enclosing function, because the compiler literally pastes the lambda's code into the call site. But once that lambda is invoked from inside the Thread { } block instead of directly inline, that non-local return has nowhere valid to jump back to, since the Thread runs on its own execution path. crossinline tells the compiler to forbid that non-local return at compile time and only allow a local one, return@action, instead of letting it compile into something that would break at runtime.

It's a narrower cousin of noinline. noinline opts a lambda out of inlining entirely so it can be stored or passed elsewhere as a real object. crossinline keeps the lambda inlined, it just changes what kind of return is legal inside it.

What does noinline do in Kotlin?

Tier: Less commonDifficulty: Easy

noinline marks one specific lambda parameter of an inline function to say don't inline this one, keep it as a real function object, while the rest of the function still gets inlined as normal.

inline fun setup(crossinline onReady: () -> Unit, noinline onError: (Throwable) -> Unit) {
    // onReady gets inlined, onError stays a real object
    errorHandlers.add(onError)
}

You need this whenever a lambda parameter has to be treated as an actual object rather than pasted inline, most commonly when you want to store it in a field, pass it on to a non-inline function, or return it from the function. An inlined lambda doesn't exist as a real object at the call site, the compiler just splices its code directly in, so there's nothing to hold onto if you wanted to keep a reference to it for later.

Marking a parameter noinline also means it loses the special inline-only powers the others still have, like a non-local return straight out of the caller. It behaves like a lambda parameter of a normal, non-inline function, because at that point, functionally, it is one.

Write a higher-order function that returns a function.

Tier: Less commonDifficulty: Medium

Here's a function that builds and returns a multiplier function, taking a factor and handing back a function that multiplies whatever it's given by that factor.

fun multiplier(factor: Int): (Int) -> Int {
    return { number -> number * factor }
}

val double = multiplier(2)
val triple = multiplier(3)

double(5)   // 10
triple(5)   // 15

The return type, (Int) -> Int, describes a function that takes an Int and returns an Int, and the function body returns a lambda matching that shape. What makes this work is closure, the returned lambda keeps a reference to factor from the outer function's scope even after multiplier itself has finished running, so each function you get back remembers its own factor independently.

The practical use for this pattern is building specialized, reusable functions from a shared piece of logic, a validator factory that takes a set of rules and returns a validation function, or a logger factory that takes a tag and returns a logging function already bound to that tag.

Collections

How do you remove duplicates from an array in Kotlin?

Tier: Less commonDifficulty: Easy

Call .distinct() on it, which returns a List with duplicates removed, keeping the first occurrence of each value and preserving order.

val numbers = arrayOf(1, 2, 2, 3, 3, 3)
val unique = numbers.distinct()   // [1, 2, 3]

If you specifically need an Array back rather than a List, chain .toTypedArray() onto the end. Converting to a Set first, numbers.toSet(), does the same deduplication and is a common shorthand, though it doesn't carry the same ordering guarantee that distinct() documents, so prefer distinct() when order matters.

For a list of objects where you want uniqueness based on one property rather than the whole object, use distinctBy, users.distinctBy { it.id }, which keeps the first user for each distinct id and drops the rest.

What does associateBy do (List to Map) in Kotlin?

Tier: Less commonDifficulty: Easy

associateBy turns a list into a map, using a selector function you provide to compute the key for each element, while the element itself becomes the value.

data class User(val id: String, val name: String)

val users = listOf(User("1", "Amit"), User("2", "Bob"))
val byId = users.associateBy { it.id }
// {"1": User("1", "Amit"), "2": User("2", "Bob") }

It's the direct fix for the common pattern of looping over a list to build a lookup map by hand. If two elements produce the same key, the later one in the list wins and overwrites the earlier entry in the resulting map, silently, so it's worth making sure your key selector is actually unique for your data.

There's also a two argument overload, associateBy(keySelector, valueTransform), when you want the map's values to be something other than the original element, and a related function, associate { it.id to it.name }, when you want full control over both the key and the value in one lambda that returns a Pair.

What does the partition filtering function do in Kotlin?

Tier: Less commonDifficulty: Easy

partition splits a collection into two lists in one pass, based on a predicate, returning a Pair where the first list holds everything that matched and the second holds everything that didn't.

val (adults, minors) = listOf(12, 25, 17, 30).partition { it >= 18 }
// adults = [25, 30], minors = [12, 17]

It's the tool to reach for whenever you'd otherwise write the same filter call twice with the condition negated the second time, list.filter { it >= 18 } and list.filter { it < 18 }. partition walks the collection once and gives you both results together, and destructuring the returned Pair straight into two named variables, like adults and minors above, makes the result read clearly at the call site.

It's specifically for a two way split. If you need to group elements into more than two buckets, groupBy is the right function instead, it takes a key selector and returns a Map with one list per distinct key rather than a fixed pair.

Generics & Variance

What is covariance in Kotlin?

Tier: Less commonDifficulty: Hard

Covariance is what lets a generic type with a more specific type argument be used where a more general one is expected, List<String> usable as List<Any>, and in Kotlin you opt into it explicitly with the out keyword.

class Box<out T>(val value: T)

fun printBox(box: Box<Any>) = println(box.value)

val stringBox: Box<String> = Box("hello")
printBox(stringBox)   // fine, Box<String> is a Box<Any>

By default, generic types in Kotlin are invariant, Box<String> and Box<Any> are unrelated types even though String is an Any, because without out, the compiler has no way to know T is never accepted as an input, only ever produced as an output. Marking the type parameter out promises exactly that, T only ever appears in out position, as a return type, never as a parameter type you could pass something unsafe into. That's why List<T> is declared out T in the standard library, since it has no methods that take a T as input, while MutableList<T> isn't covariant, because add(T) takes one in.

You can also apply this at the call site instead of the declaration, called use-site variance, fun copy(from: Array<out Any>, to: Array<Any>), when you only want to read from a specific array parameter and don't own the class to mark it covariant everywhere. The mirror of out is in, contravariance, for a type parameter that only ever appears as an input, like Comparator<in T>.