Complete Kotlin Tutorial

Part 1: Kotlin Language Fundamentals

1.1 Kotlin overview

Kotlin is a statically typed language developed by JetBrains. It supports object-oriented and functional programming and is officially supported for Android development.

Main benefits:

* Concise syntax

* Compile-time null safety

* Java interoperability

* Coroutines

* Extension functions

* Smart casts

* Multiplatform support

1.2 Kotlin vs Java

FeatureKotlinJava
Null safetyBuilt into type systemManual checks
SyntaxConciseMore verbose
Data modelsdata classRecord/manual class
Extension functionsSupportedNot directly supported
Default argumentsSupportedRequires overloads
Checked exceptionsNot enforcedEnforced
ClassesFinal by defaultOpen by default
Asynchronous codeCoroutinesThreads/futures
Static membersTop-level/object/companionstatic
data class User(
    val id: Int,
    val name: String
)

1.3 val, var and const val

val name = "Mahesh" // Cannot be reassigned

var score = 10
score = 20          // Can be reassigned

const val MAX_RETRIES = 3 // Compile-time constant
Featurevalvarconst val
ReassignmentNoYesNo
Runtime valueYesYesNo
Compile-time valueNot requiredNoRequired
Local declarationYesYesNo

const val can be declared:

* At top level

* Inside an object

* Inside a companion object

1.4 Data types and inference

val age: Int = 35
val count: Long = 10L
val price: Double = 99.99
val rating: Float = 4.5F
val active: Boolean = true
val grade: Char = 'A'
val language: String = "Kotlin"

Type inference:

val age = 35           // Int
val price = 99.99      // Double
val language = "Kotlin" // String

Kotlin does not perform automatic numeric widening:

val number: Int = 10
val largeNumber: Long = number.toLong()

1.5 Operators

val sum = 10 + 5
val eligible = age >= 18
val allowed = active && eligible

Equality:

a == b  // Structural equality
a === b // Referential equality

Use case

const val MINIMUM_AGE = 18

fun isEligible(age: Int): Boolean =
    age >= MINIMUM_AGE

Common mistakes

* Using var when val is enough

* Assuming val makes an object immutable

* Using === for content comparison

* Expecting automatic Int-to-Long conversion

* Forgetting that 5 / 2 produces 2

Interview Q&A

Q1. What is the difference between val and var?

val cannot be reassigned; var can be reassigned.

Q2. Does val guarantee immutability?

No. It only prevents reference reassignment.

Q3. What is const val?

A compile-time constant declared at top level or inside an object/companion object.

Q4. What is the difference between == and ===?

== compares values; === compares object references.

Q5. Why is Kotlin safer than Java?

It provides null safety, smart casts, final-by-default classes and other compile-time checks.

---

Part 2: Strings, Control Flow, Loops and Ranges

2.1 Strings

Strings are immutable.

val name = "Mahesh"
val message = "Hello, $name"
val total = "Total: ${10 + 20}"

Multiline string:

val query = """
    SELECT *
    FROM users
    WHERE active = 1
""".trimIndent()

String operations:

val text = " Kotlin "

text.trim()
text.uppercase()
text.lowercase()
text.contains("Kotlin")
text.substring(1, 4)

For repeated concatenation:

val output = buildString {
    append("Hello")
    append(" Kotlin")
}

2.2 if as an expression

val category = if (age >= 18) {
    "Adult"
} else {
    "Minor"
}

Unlike traditional Java statements, Kotlin if can return a value.

2.3 when

val result = when (statusCode) {
    200 -> "Success"
    401 -> "Unauthorized"
    404 -> "Not found"
    in 500..599 -> "Server error"
    else -> "Unknown"
}

Type-based when:

fun describe(value: Any): String = when (value) {
    is String -> "String of length ${value.length}"
    is Int -> "Integer"
    is Boolean -> "Boolean"
    else -> "Unknown"
}

2.4 Loops

for (number in 1..5) {
    println(number)
}
var count = 0

while (count < 5) {
  count++
}
do {
    count--
} while (count > 0)

2.5 Ranges

1..5          // 1, 2, 3, 4, 5
1..<5         // 1, 2, 3, 4
1 until 5     // 1, 2, 3, 4
5 downTo 1    // 5, 4, 3, 2, 1
1..10 step 2  // 1, 3, 5, 7, 9

Collection iteration:

for ((index, user) in users.withIndex()) {
    println("$index: ${user.name}")
}

2.6 Jump expressions

for (number in 1..10) {
    if (number == 3) continue
    if (number == 8) break
}

Labeled return:

users.forEach userLoop@ { user ->
    if (!user.isActive) return@userLoop
    println(user.name)
}

Diagram

flowchart TD
    A["Input condition"] --> B{"Multiple cases?"}
    B -->|No| C["Use if/else"]
    B -->|Yes| D["Use when"]
    D --> E["Exact value"]
    D --> F["Range"]
    D --> G["Type check"]

Use case

fun mapTransactionStatus(code: Int): String = when (code) {
    200 -> "Successful"
    202 -> "Processing"
    400 -> "Invalid request"
    401 -> "Authentication required"
    in 500..599 -> "Temporary server problem"
    else -> "Unknown"
}

Common mistakes

* Using long if-else chains instead of when

* Forgetting the inclusive end of ..

* Repeated string concatenation inside loops

* Using nested forEach with confusing returns

* Forgetting an else branch when when must produce a value

Interview Q&A

Q1. Is if an expression in Kotlin?

Yes. It can calculate and return a value.

Q2. How is when better than a traditional switch?

It supports types, ranges, conditions, multiple values and expression results.

Q3. What is the difference between 1..5 and 1 until 5?

1..5 includes 5; 1 until 5 excludes 5.

Q4. Are strings mutable?

No. String operations return new strings.

Q5. What does return@forEach mean?

It returns from the current lambda invocation instead of the enclosing function.

---

Part 3: Classes, Objects and Basic Functions

3.1 Functions

fun add(a: Int, b: Int): Int {
  return a + b
}

Expression body:

fun add(a: Int, b: Int): Int = a + b

Default and named arguments:

fun createUser(
    name: String,
    active: Boolean = true,
    role: String = "USER"
) {
    // Creation logic
}

createUser(name = "Mahesh", role = "ADMIN")

3.2 Classes and constructors

class BankAccount(
    val accountNumber: String,
    initialBalance: Double
) {
    private var balance = initialBalance

    fun deposit(amount: Double) {
        require(amount > 0)
        balance += amount
    }

    fun getBalance(): Double = balance
}

Secondary constructor:

class User(
    val name: String,
    val age: Int
) {
    constructor(name: String) : this(name, 0)
}

Prefer default arguments or factory functions when they are clearer.

3.3 Initializer block

class User(val name: String) {
    init {
        require(name.isNotBlank())
    }
}

3.4 Data class

data class Product(
    val id: Int,
    val name: String,
    val price: Double
)

Generated functions:

* `equals()`
* `hashCode()`
* `toString()`
* `copy()`
* `componentN()`

3.5 object

Singleton:

object AnalyticsManager {
    fun track(event: String) {
        println(event)
    }
}

Anonymous object:

val listener = object : ClickListener {
    override fun onClick() {
        println("Clicked")
    }
}

3.6 Companion object

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

Usage:

val user = User.create("Mahesh")

Comparison

FeatureClassObjectCompanion object
InstancesMultipleOneOne per containing class
ConstructorYesNo public constructorNo direct constructor
Typical useDomain objectsSingleton serviceFactory/constants
InitializationPer instanceFirst accessWith containing class usage

Common mistakes

* Putting mutable global state in an object

* Creating secondary constructors when defaults are sufficient

* Exposing mutable properties publicly

* Using data classes for entities with complex identity rules

* Assuming data-class copy() performs a deep copy

Interview Q&A

Q1. What is a primary constructor?

The constructor declared in the class header.

Q2. What is an init block?

A block executed during object construction.

Q3. What is the difference between object and class?

An object has one singleton instance; a class can have multiple instances.

Q4. What is a companion object?

An object associated with a class, commonly used for factories and class-level members.

Q5. Is data-class copy() deep?

No. It performs a shallow copy.

---

Part 4: Immutability, Null Safety and Type System

4.1 Nullable and non-null types

val name: String = "Kotlin"
val optionalName: String? = null

A non-null variable cannot hold null.

4.2 Safe call ?.

val city = user?.address?.city
If any receiver is null, the expression returns null.

4.3 Elvis operator ?:

val displayName = user?.name ?: "Guest"

Early return:

fun process(user: User?) {
    val validUser = user ?: return
    println(validUser.name)
}

4.4 Not-null assertion !!

val length = name!!.length

It throws an NPE if name is null. Avoid it when null can occur normally.

4.5 let

user?.let { validUser ->
  repository.save(validUser)
}

4.6 Casts

Unsafe cast:

val text = value as String

Safe cast:

val text = value as? String

4.7 Smart casts

fun printValue(value: Any) {
    if (value is String) {
        println(value.length)
    }
}

The compiler automatically treats value as String inside the branch.

4.8 Platform types

Java declarations without reliable nullability information become platform types.

val javaName: String? = javaApi.getName()
Validate Java values at the boundary instead of allowing uncertain types to spread.

4.9 Any, Unit and Nothing

TypeMeaning
AnyRoot of all non-null Kotlin types
Any?Root that also allows null
UnitSuccessful completion without a meaningful result
NothingFunction never completes normally
fun log(message: String): Unit {
    println(message)
}

fun fail(message: String): Nothing {
    throw IllegalStateException(message)
}

4.10 Reference vs object immutability

val users = mutableListOf("A")
users.add("B") // Allowed

The reference is read-only, but the object is mutable.

Diagram

flowchart TD
    A["Nullable value"] --> B{"Is null?"}
    B -->|No| C["Use value"]
    B -->|Yes| D["Default with ?:"]
    B -->|Yes| E["Return or throw"]
    B -->|Unknown| F["Use ?."]

Common mistakes

* Excessive use of !!

* Treating platform types as safe

* Assuming val provides deep immutability

* Using unsafe as instead of as?

* Chaining safe calls when missing data should be treated as an error

Interview Q&A

Q1. Can Kotlin still throw an NPE?

Yes, through !!, platform types, Java interop or explicit exceptions.

Q2. What is the Elvis operator?

?: returns a fallback when the left side is null.

Q3. What is a smart cast?

Automatic type refinement after a compiler-verifiable check.

Q4. What is a platform type?

A Java type whose nullability Kotlin cannot determine reliably.

Q5. What is Nothing used for?

Functions that always throw or never return normally.

---

Part 5: Functions, Lambdas and Scope Functions

5.1 Function types

val operation: (Int, Int) -> Int

Nullable callback:

val callback: (() -> Unit)? = null

5.2 Lambdas

val add: (Int, Int) -> Int = { a, b ->
  a + b
}
val activeUsers = users.filter { it.isActive }

5.3 Closures

A lambda can capture surrounding variables:

var total = 0

numbers.forEach {
  total += it
}

Captured mutable state can become unsafe across threads.

5.4 Higher-order functions

fun calculate(
    a: Int,
    b: Int,
    operation: (Int, Int) -> Int
): Int = operation(a, b)

val result = calculate(10, 5) { x, y -> x + y }

Use cases:

* Callbacks

* Validation

* Collection operations

* Retry strategies

* UI events

5.5 Extension functions

fun String.isValidEmail(): Boolean =
    contains("@") && contains(".")

Extensions:

* Do not modify the original class

* Cannot access private members

* Are resolved statically

* Lose to real member functions with the same signature

5.6 Scope functions

FunctionContextReturnsUse
letitBlock resultNull handling/transformation
runthisBlock resultConfigure and calculate
withthisBlock resultMultiple operations
applythisReceiverObject configuration
alsoitReceiverLogging/side effects
val intent = Intent(context, HomeActivity::class.java).apply {
  putExtra("USER_ID", userId)
}
val user = repository.loadUser().also {
  logger.log("Loaded user ${it.id}")
}

Common mistakes

* Nesting scope functions

* Confusing this and it

* Using let only to rename a variable

* Creating broad or misleading extensions

* Capturing an Activity in a long-lived callback

Interview Q&A

Q1. What is a higher-order function?

A function that accepts or returns another function.

Q2. What is a closure?

A lambda that captures variables from its surrounding scope.

Q3. How are extensions dispatched?

Statically, based on the compile-time receiver type.

Q4. What is the difference between apply and also?

Both return the receiver; apply uses this, while also uses it.

Q5. What is the difference between let and run?

Both return the block result; let uses it, while run uses this.

---

Part 6: Inline, Reified and Advanced Features

6.1 inline

inline fun measure(block: () -> Unit) {
    val start = System.nanoTime()
    block()
    println(System.nanoTime() - start)
}

Inlining can reduce lambda allocation and invocation overhead, but may increase bytecode size.

6.2 noinline

inline fun execute(
    immediate: () -> Unit,
    noinline stored: () -> Unit
) {
    immediate()
    saveCallback(stored)
}

Use noinline when a lambda must be stored or passed as an object.

6.3 crossinline

inline fun runTask(crossinline task: () -> Unit) {
    Runnable {
        task()
    }.run()
}

It prevents a non-local return from the lambda.

6.4 Reified generics

inline fun <reified T> Any?.isType(): Boolean =
    this is T

Android example:

inline fun <reified T : Activity> Context.openActivity() {
  startActivity(Intent(this, T::class.java))
}

6.5 Value classes

@JvmInline
value class UserId(val value: String)

They provide domain type safety and may avoid wrapper allocation.

fun loadUser(id: UserId) {}

Boxing may still occur with nullable values, generics and interfaces.

6.6 Contracts

Contracts give the compiler information about function behaviour.

@OptIn(ExperimentalContracts::class)
fun requireUser(user: User?): User {
    contract {
        returns() implies (user != null)
    }
    return requireNotNull(user)
}

Incorrect contracts can mislead callers and the compiler.

6.7 Tail recursion

tailrec fun factorial(
    number: Int,
    accumulator: Long = 1
): Long =
    if (number <= 1) {
        accumulator
    } else {
        factorial(number - 1, accumulator * number)
    }

The recursive call must be the final operation.

6.8 Type aliases

typealias UserCallback = (Result<User>) -> Unit

A type alias creates another name, not another type.

6.9 Destructuring

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

val (id, name) = user

6.10 Operator functions

data class Money(val amount: Int) {
    operator fun plus(other: Money) =
        Money(amount + other.amount)
}

6.11 DSL concepts

class RequestBuilder {
    var url = ""
    var timeout = 5_000
}

fun request(block: RequestBuilder.() -> Unit): RequestBuilder =
    RequestBuilder().apply(block)

val request = request {
    url = "/users"
    timeout = 10_000
}

DSLs commonly use lambdas with receivers, extensions and builders.

Common mistakes

* Marking every function inline

* Confusing noinline with crossinline

* Assuming value classes are never boxed

* Using type aliases when actual type safety is needed

* Writing confusing operator overloads

* Building an unnecessary DSL for simple operations

Interview Q&A

Q1. Why use inline?

To reduce some higher-order function overhead and enable reified generics.

Q2. What is noinline?

It prevents a selected lambda from being inlined.

Q3. What is crossinline?

It prevents non-local returns from an inline lambda.

Q4. Why does reified require inline?

The type is inserted at the call site, allowing access despite JVM type erasure.

Q5. Type alias or value class?

A type alias only renames a type; a value class creates a distinct domain type.

---

Part 7: Object-Oriented vs Functional Kotlin

7.1 Inheritance

Classes are final by default.
open class Employee {
    open fun work() {
        println("Working")
    }
}

class AndroidDeveloper : Employee() {
    override fun work() {
        println("Developing Android application")
    }
}

7.2 Interfaces

interface Logger {
  fun log(message: String)
}

An interface can have default methods:

interface Logger {
    fun log(message: String)

    fun logError(error: Throwable) {
        log(error.message ?: "Unknown error")
    }
}

7.3 Composition

class UserRepository(
    private val api: UserApi,
    private val cache: UserCache
)

7.4 Delegation

interface Printer {
    fun print()
}

class ConsolePrinter : Printer {
    override fun print() = println("Printing")
}

class ReportPrinter(
    printer: Printer
) : Printer by printer

7.5 Polymorphism

fun printReport(printer: Printer) {
  printer.print()
}

Different Printer implementations can be supplied.

7.6 Pure functions and side effects

Pure function:

fun calculateTax(amount: Double): Double =
    amount * 0.18

Side-effecting function:

fun saveUser(user: User) {
  database.insert(user)
}

Side effects include:

* Network calls

* Database changes

* File operations

* Logging

* UI updates

* Shared-state mutation

7.7 Functional transformations

val names = users
    .filter { it.isActive }
    .map { it.name }
    .sorted()

Comparison

AreaOOPFunctional
Main abstractionObjectFunction/value
StateOften encapsulatedPrefer immutable values
ReuseComposition/inheritanceFunction composition
TestingMay require collaboratorsPure functions are easy
Side effectsInside methodsIsolated at boundaries
flowchart LR
    A["UI event"] --> B["ViewModel object"]
    B --> C["Pure validation"]
    C --> D["Use-case object"]
    D --> E["Repository side effect"]
    E --> F["Immutable UI state"]

Common mistakes

* Deep inheritance hierarchies

* Using inheritance only for code reuse

* Hidden side effects

* Mutable global singleton state

* Long functional chains that reduce readability

* Using functional style where a named loop is clearer

Interview Q&A

Q1. Why are classes final by default?

To prevent accidental inheritance and encourage safer composition.

Q2. Composition or inheritance?

Prefer composition for flexible collaboration; use inheritance for a true and stable “is-a” relationship.

Q3. What is delegation?

Forwarding interface implementation to another object using by.

Q4. What is a pure function?

A function that produces the same output for the same input without external side effects.

Q5. Are side effects always bad?

No. They are necessary, but should be isolated and controlled.

---

Part 8: Collections and Functional Transformations

8.1 Core collections

List

Ordered and allows duplicates:

val names = listOf("A", "B", "A")

Set

Stores unique values:

val roles = setOf("ADMIN", "USER")

Map

Stores key-value pairs:

val users = mapOf(
    1 to "Mahesh",
    2 to "Amit"
)

8.2 Read-only vs mutable

val names: List<String> = listOf("A", "B")
val mutableNames: MutableList<String> =
    mutableListOf("A", "B")

A read-only interface does not guarantee deep immutability:

val source = mutableListOf("A")
val view: List<String> = source

source.add("B")
println(view) // [A, B]

8.3 map

val names = users.map { it.name }

It transforms every element.

8.4 filter

val activeUsers = users.filter { it.isActive }

It retains matching elements.

8.5 fold

val total = prices.fold(0.0) { sum, price ->
  sum + price
}

fold starts with an initial value and accumulates a result.

8.6 reduce

val total = numbers.reduce { result, number ->
  result + number
}

Unlike fold, reduce uses the first element as its initial accumulator and fails on an empty collection.

8.7 flatMap

val permissions = users.flatMap { it.permissions }

It transforms each element into a collection and flattens the result.

8.8 Useful operations

users.firstOrNull()
users.find { it.id == targetId }
users.any { it.isActive }
users.all { it.isVerified }
users.none { it.isBlocked }
users.groupBy { it.role }
users.associateBy { it.id }
users.sortedBy { it.name }
users.distinctBy { it.id }

Use case

data class Transaction(
    val amount: Double,
    val successful: Boolean
)

val totalSuccessfulAmount = transactions
    .filter { it.successful }
    .sumOf { it.amount }

Common mistakes

* Assuming `List` is completely immutable
* Exposing `MutableList` through public APIs
* Calling `first()` when a collection may be empty
* Using `reduce()` on empty collections
* Creating unnecessarily long transformation chains
* Mutating a collection while iterating over it

Interview Q&A

Q1. What is the difference between List and MutableList?

List exposes read operations; MutableList also exposes modification operations.

Q2. What is the difference between map and flatMap?

map returns one transformed value per element; flatMap combines nested results into one collection.

Q3. What is the difference between fold and reduce?

fold takes an initial accumulator; reduce uses the first element.

Q4. What does associateBy do?

It creates a map using a selected property as the key.

Q5. Is listOf() always deeply immutable?

No. Its elements may still be mutable.

---

Part 9: Sequences, Persistent Collections and Performance

9.1 Eager collections

val result = numbers
    .map { it * 2 }
    .filter { it > 10 }
    .take(5)

Collection transformations are normally eager and may create intermediate collections.

9.2 Lazy sequences

val result = numbers
    .asSequence()
    .map { it * 2 }
    .filter { it > 10 }
    .take(5)
    .toList()

Sequence operations run when a terminal operation consumes the sequence.

Comparison

AreaCollectionSequence
ExecutionEagerLazy
Intermediate collectionsOften createdUsually avoided
Small dataUsually simple and fastCan add overhead
Early terminationLess efficient in chainsCan stop early
ReuseResult already createdPipeline may run again
flowchart TD
    A["Input"] --> B{"Processing style"}
    B -->|Collection| C["Transform all"]
    C --> D["Intermediate list"]
    D --> E["Next transformation"]
    B -->|Sequence| F["One element through pipeline"]
    F --> G["Stop when enough results"]

9.3 Persistent collections

Persistent collections create modified versions while reusing unchanged internal structure.

val original = persistentListOf("A", "B")
val updated = original.add("C")

The original remains unchanged.

Use cases:

* Immutable UI state

* Compose

* StateFlow

* Concurrent snapshots

* Undo/redo models

9.4 Shallow copying

data class Team(
    val name: String,
    val members: MutableList<String>
)

val first = Team("Android", mutableListOf("A"))
val second = first.copy()

second.members.add("B")
println(first.members) // [A, B]

Safer:

val second = first.copy(
    members = first.members.toMutableList()
)

9.5 Boxing

Boxing may occur with:

val nullableNumber: Int? = 10
val numbers: List<Int> = listOf(1, 2, 3)
fun <T> identity(value: T): T = value

Potential allocation sources:

* Intermediate collections

* Capturing lambdas

* Data-class copies

* Nullable primitives

* Generic containers

* Varargs

* Reflection

* String concatenation

Performance approach

1. Write clear code.

2. Measure performance.

3. Identify the real bottleneck.

4. Optimize the hot path.

5. Measure again.

Common mistakes

* Using sequences for every collection

* Repeatedly converting list → sequence → list

* Assuming copy() is deep

* Optimizing without profiling

* Ignoring boxing in genuine hot paths

* Sharing mutable lists between UI states

Interview Q&A

Q1. Are sequences always faster?

No. For small collections or simple operations, their iterator overhead may make them slower.

Q2. When are sequences useful?

For large inputs, multiple transformations or early termination.

Q3. What is structural sharing?

Reusing unchanged internal data when creating a modified immutable collection.

Q4. What is boxing?

Wrapping a primitive representation in an object representation.

Q5. What is a shallow copy?

A new outer object that still shares nested object references.

---

Part 10: JVM Interoperability, Annotations, KAPT and KSP

10.1 Java interoperability

Kotlin can call Java directly:

val user = JavaUser()
println(user.name)

Java getters and setters appear as Kotlin properties.

10.2 SAM conversion

SAM means Single Abstract Method.

button.setOnClickListener {
    println("Clicked")
}

Kotlin SAM interface:

fun interface Validator {
  fun validate(value: String): Boolean
}

val validator = Validator { it.isNotBlank() }

10.3 Checked exceptions

Kotlin does not enforce checked exceptions.

For Java callers:

@Throws(IOException::class)
fun readFile(): String {
  throw IOException()
}

10.4 @JvmStatic

class UserFactory {
    companion object {
        @JvmStatic
        fun create(): User = User()
    }
}

Allows Java:

UserFactory.create();

10.5 @JvmField

object Configuration {
    @JvmField
    val timeout = 5_000
}

It exposes the backing field directly to Java.

10.6 @JvmOverloads

@JvmOverloads
fun connect(
    url: String,
    timeout: Int = 5_000,
    retries: Int = 3
) {
    // ...
}

It generates overloads for Java callers.

10.7 Annotations

@Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.RUNTIME)
annotation class Audited

Use-site targets:

data class User(
    @field:JsonName("user_name")
    val name: String
)

Common targets:

* field:

* get:

* set:

* param:

* property:

10.8 KAPT vs KSP

AreaKAPTKSP
Processing modelJava annotation processingKotlin symbol processing
Java stubsRequiredNot normally required
Kotlin understandingLimitedBetter
PerformanceUsually slowerUsually faster
UsageLegacy processorsModern supported processors

Prefer KSP when the library officially supports it.

10.9 Reflection

val type = User::class
println(type.simpleName)

Reflection disadvantages:

* Runtime overhead

* Reduced compile-time safety

* Obfuscation complexity

* Possible startup cost

* Runtime failure risk

Common mistakes

* Trusting Java platform types

* Using @JvmOverloads everywhere

* Confusing @JvmField with const val

* Forgetting annotation use-site targets

* Using KAPT when KSP is supported

* Using reflection where generated code is more appropriate

Interview Q&A

Q1. What is SAM conversion?

Conversion of a lambda into an implementation of a single-abstract-method interface.

Q2. Does Kotlin enforce checked exceptions?

No.

Q3. What does @Throws do?

It exposes declared exceptions to Java callers.

Q4. KAPT or KSP?

Use KSP when supported; it understands Kotlin symbols better and is generally faster.

Q5. Why avoid excessive reflection?

It adds runtime cost and reduces compile-time safety.

---

Part 11: Memory, Visibility, Modifiers and Thread Safety

11.1 Visibility modifiers

ModifierVisibility
publicEverywhere; default
privateCurrent declaration or file
protectedClass and subclasses
internalSame Kotlin module

Top-level private means file-private.

11.2 open and final

open class BaseRepository {
    open fun load() {}
}

class UserRepository : BaseRepository() {
    override fun load() {}
}
Classes and methods are final unless marked `open`.

11.3 lateinit

private lateinit var adapter: UserAdapter

Rules:

* Only var

* Non-null type

* Not for primitive properties

* Initialized manually

* Throws if accessed before initialization

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

11.4 lazy

val repository by lazy {
  UserRepository()
}
ModeBehaviour
SYNCHRONIZEDThread-safe single initialization
PUBLICATIONMultiple computations possible; one result stored
NONENo thread-safety guarantee

lateinit vs lazy

Arealateinitlazy
Propertyvarval
InitializationManually laterOn first access
ReinitializationYesNo
Primitive supportNoYes
Thread safetyNot automaticDefault is synchronized

11.5 Lambda capture and leaks

object TaskManager {
    var callback: (() -> Unit)? = null
}

class HomeActivity : Activity() {
    fun register() {
        TaskManager.callback = {
            updateUi()
        }
    }
}

The singleton callback captures the Activity and can prevent garbage collection.

Solutions:

* Clear callbacks

* Avoid UI callbacks in singletons

* Use lifecycle-aware APIs

* Capture only required values

* Keep ownership explicit

11.6 Memory visibility

@Volatile
private var running = true

@Volatile guarantees that field reads observe recent writes.

It does not make compound operations atomic:

@Volatile
var count = 0

count++ // Not atomic

Use:

* synchronized

* Atomic classes

* Locks

* Mutex

* Thread confinement

* Immutable state

Diagram

flowchart TD
    A["Shared mutable state"] --> B{"Can it be immutable?"}
    B -->|Yes| C["Use immutable snapshots"]
    B -->|No| D{"Can one thread own it?"}
    D -->|Yes| E["Thread confinement"]
    D -->|No| F["Atomic, lock or Mutex"]

Common mistakes

* Treating internal as a security boundary

* Accessing uninitialized lateinit

* Using lateinit for lifecycle-sensitive Fragment views

* Assuming @Volatile makes increments atomic

* Storing Activity callbacks globally

* Making every class open

* Using LazyThreadSafetyMode.NONE across threads

Interview Q&A

Q1. What is the difference between private and internal?

private restricts access to the declaration/file; internal allows access within the module.

Q2. What is the difference between lateinit and lazy?

lateinit is a manually initialized var; lazy initializes a val on first access.

Q3. Is lazy thread-safe?

The default synchronized mode is thread-safe.

Q4. What does @Volatile guarantee?

Visibility and ordering for reads and writes of that field, not atomic compound operations.

Q5. How can a lambda leak an Activity?

A longer-lived object stores a lambda that strongly captures the Activity.

---

Part 12: Kotlin Multiplatform and Android Pitfalls

12.1 Kotlin Multiplatform

Kotlin Multiplatform allows selected code to be shared across platforms.

Supported target families include:

* Android

* JVM

* iOS and other native targets

* JavaScript

* WebAssembly

* Desktop

12.2 Common and platform-specific code

flowchart TD
    A["commonMain"] --> B["Models"]
    A --> C["Use cases"]
    A --> D["Validation"]
    A --> E["Interfaces"]
    E --> F["androidMain"]
    E --> G["iosMain"]
    E --> H["jvmMain"]

Typical shared code:

* Domain models

* Validation

* Use cases

* Business rules

* State machines

* Networking with supported libraries

* Repository logic

* Shared presentation logic

Platform-specific code:

* Android Context

* iOS frameworks

* Notifications

* Device APIs

* Native storage

* Platform UI

12.3 expect and actual

Common code:

expect fun platformName(): String

Android:

actual fun platformName(): String = "Android"

iOS:

actual fun platformName(): String = "iOS"
Interfaces and dependency injection may be cleaner than excessive `expect/actual` declarations.

12.4 Project structure

shared/
├── src/commonMain/
├── src/commonTest/
├── src/androidMain/
├── src/androidUnitTest/
├── src/iosMain/
└── src/iosTest/

12.5 KMP use case

class ValidateTransferUseCase {

    operator fun invoke(
        amount: Double,
        availableBalance: Double
    ): Boolean {
        return amount > 0 &&
            amount <= availableBalance
    }
}

This logic can be shared between Android and iOS.

12.6 KMP limitations

* Not every Android/JVM library supports KMP

* Platform-specific knowledge remains necessary

* Build configuration can be complex

* Debugging can cross language and platform boundaries

* Interoperability may require wrappers

* Shared UI may not match every native UX requirement

* Library maturity differs by platform

* Build-time and binary-size effects require measurement

---

12.7 Common Kotlin Android pitfalls

Excessive !!

Bad:

val name = response.body()!!.user!!.name!!

Better:

val user = response.body()?.user
    ?: return Result.failure(InvalidResponseException())

val name = user.name

Overusing scope functions

Confusing:

user?.let {
    it.address?.run {
        repository.apply {
            save(this@run)
        }
    }
}

Clearer:

val user = user ?: return
val address = user.address ?: return

repository.save(address)

Shared mutable state

Risky:

object Session {
    var token: String? = null
}

Prefer encapsulated state, dependency injection and controlled updates.

Incorrect equality

status == "SUCCESS"  // Correct content comparison
status === "SUCCESS" // Reference comparison

Shallow copying

val copied = original.copy()

Nested mutable objects may still be shared.

Unnecessary sequences

val names = smallList.map { it.name }

A sequence is unnecessary for one small operation.

Closure leaks

Do not store callbacks capturing an Activity, Fragment or View in long-lived objects.

Public API misuse

Avoid:

class UserStore {
  val users: MutableList<User> = mutableListOf()
}

Better:

class UserStore {
    private val mutableUsers = mutableListOf<User>()

    val users: List<User>
        get() = mutableUsers.toList()
}

Common mistakes

* Trying to share 100% of KMP code

* Putting Android APIs inside commonMain

* Overusing expect/actual

* Assuming KMP removes the need for native development

* Excessive !!

* Deeply nested scope functions

* Exposing mutable collections

* Using reflection in performance-sensitive paths

* Ignoring lambda lifecycle and ownership

* Optimizing without benchmarks

Interview Q&A

Q1. What problem does Kotlin Multiplatform solve?

It reduces duplication by sharing platform-independent Kotlin code.

Q2. What belongs in commonMain?

Platform-independent models, validation, business rules, use cases and supported data logic.

Q3. What are expect and actual?

expect declares required platform functionality; actual provides each platform’s implementation.

Q4. Is KMP the same as Compose Multiplatform?

No. KMP shares Kotlin code; Compose Multiplatform is a UI framework that can run on multiple platforms.

Q5. Should an application share 100% of its code?

Usually no. Native integrations and platform-specific UX often remain separate.

Q6. What is the most dangerous null-safety mistake?

Using !! without a real non-null guarantee.

Q7. Why should mutable collections not be exposed?

External code could modify internal state and violate invariants.

Q8. Why can data-class copying cause bugs?

Because copy() is shallow and nested mutable objects remain shared.

---

Final Kotlin Interview Revision Sheet

| Concept               | Key point                                             |
| --------------------- | ----------------------------------------------------- |
| `val`                 | Non-reassignable reference                            |
| `var`                 | Reassignable reference                                |
| `const val`           | Compile-time constant                                 |
| `==`                  | Structural equality                                   |
| `===`                 | Referential equality                                  |
| `?.`                  | Safe call                                             |
| `?:`                  | Default or fallback                                   |
| `!!`                  | Force non-null; may throw NPE                         |
| `as?`                 | Safe cast                                             |
| `Any`                 | Root non-null type                                    |
| `Unit`                | No meaningful return value                            |
| `Nothing`             | Never returns normally                                |
| Lambda                | Function value                                        |
| Closure               | Lambda capturing surrounding state                    |
| Extension             | Statically resolved utility syntax                    |
| Higher-order function | Accepts or returns a function                         |
| `inline`              | Copies eligible code at call site                     |
| `noinline`            | Keeps lambda as an object                             |
| `crossinline`         | Prevents non-local return                             |
| `reified`             | Makes generic type available to inline code           |
| Value class           | Domain type wrapper with possible allocation savings  |
| `List`                | Read-only interface, not guaranteed deep immutability |
| Sequence              | Lazy operation pipeline                               |
| `copy()`              | Shallow copy                                          |
| KSP                   | Kotlin-aware symbol processing                        |
| `lateinit`            | Delayed mutable initialization                        |
| `lazy`                | First-access initialization                           |
| `@Volatile`           | Memory visibility, not compound-operation atomicity   |
| KMP                   | Shares selected Kotlin code across platforms          |