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
| Feature | Kotlin | Java |
|---|---|---|
| Null safety | Built into type system | Manual checks |
| Syntax | Concise | More verbose |
| Data models | data class | Record/manual class |
| Extension functions | Supported | Not directly supported |
| Default arguments | Supported | Requires overloads |
| Checked exceptions | Not enforced | Enforced |
| Classes | Final by default | Open by default |
| Asynchronous code | Coroutines | Threads/futures |
| Static members | Top-level/object/companion | static |
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| Feature | val | var | const val |
|---|---|---|---|
| Reassignment | No | Yes | No |
| Runtime value | Yes | Yes | No |
| Compile-time value | Not required | No | Required |
| Local declaration | Yes | Yes | No |
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" // StringKotlin 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 && eligibleEquality:
a == b // Structural equality
a === b // Referential equalityUse case
const val MINIMUM_AGE = 18
fun isEligible(age: Int): Boolean =
age >= MINIMUM_AGECommon 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, 9Collection 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 + bDefault 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
| Feature | Class | Object | Companion object |
|---|---|---|---|
| Instances | Multiple | One | One per containing class |
| Constructor | Yes | No public constructor | No direct constructor |
| Typical use | Domain objects | Singleton service | Factory/constants |
| Initialization | Per instance | First access | With 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? = nullA non-null variable cannot hold null.
4.2 Safe call ?.
val city = user?.address?.cityIf 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!!.lengthIt 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 StringSafe cast:
val text = value as? String4.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
| Type | Meaning |
|---|---|
Any | Root of all non-null Kotlin types |
Any? | Root that also allows null |
Unit | Successful completion without a meaningful result |
Nothing | Function 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") // AllowedThe 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) -> IntNullable callback:
val callback: (() -> Unit)? = null5.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
| Function | Context | Returns | Use |
|---|---|---|---|
let | it | Block result | Null handling/transformation |
run | this | Block result | Configure and calculate |
with | this | Block result | Multiple operations |
apply | this | Receiver | Object configuration |
also | it | Receiver | Logging/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 TAndroid 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>) -> UnitA type alias creates another name, not another type.
6.9 Destructuring
data class User(val id: Int, val name: String)
val (id, name) = user6.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 printer7.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.18Side-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
| Area | OOP | Functional |
|---|---|---|
| Main abstraction | Object | Function/value |
| State | Often encapsulated | Prefer immutable values |
| Reuse | Composition/inheritance | Function composition |
| Testing | May require collaborators | Pure functions are easy |
| Side effects | Inside methods | Isolated 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 itInterview 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
| Area | Collection | Sequence |
|---|---|---|
| Execution | Eager | Lazy |
| Intermediate collections | Often created | Usually avoided |
| Small data | Usually simple and fast | Can add overhead |
| Early termination | Less efficient in chains | Can stop early |
| Reuse | Result already created | Pipeline 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 = valuePotential 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 AuditedUse-site targets:
data class User(
@field:JsonName("user_name")
val name: String
)Common targets:
* field:
* get:
* set:
* param:
* property:
10.8 KAPT vs KSP
| Area | KAPT | KSP |
|---|---|---|
| Processing model | Java annotation processing | Kotlin symbol processing |
| Java stubs | Required | Not normally required |
| Kotlin understanding | Limited | Better |
| Performance | Usually slower | Usually faster |
| Usage | Legacy processors | Modern 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
| Modifier | Visibility |
|---|---|
public | Everywhere; default |
private | Current declaration or file |
protected | Class and subclasses |
internal | Same 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: UserAdapterRules:
* 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()
}| Mode | Behaviour |
|---|---|
SYNCHRONIZED | Thread-safe single initialization |
PUBLICATION | Multiple computations possible; one result stored |
NONE | No thread-safety guarantee |
lateinit vs lazy
| Area | lateinit | lazy |
|---|---|---|
| Property | var | val |
| Initialization | Manually later | On first access |
| Reinitialization | Yes | No |
| Primitive support | No | Yes |
| Thread safety | Not automatic | Default 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 atomicUse:
* 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(): StringAndroid:
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.nameOverusing 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 comparisonShallow 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 |