Kotlin Roadmap

Kotlin is a modern, statically typed language that runs on the JVM, in the browser and as native code. It is the main language for Android, a strong choice for back-end services, and a way to share code between mobile, desktop, web and server projects. Companies like it because it is concise, safe with null values and works smoothly with existing Java code.

This roadmap takes you from your first program to what interviews test: null safety, classes and sealed hierarchies, the collections library, coroutines and Flow, interoperability with Java, build tools and testing. The last stage covers the main platforms so you can pick a direction. Each topic has a short explanation, usually an example, and an exercise. Core topics are expected of everyone, recommended topics are common in jobs and optional ones can wait.

By the end you will be able to build and test a real Kotlin application and explain why you wrote it the way you did. Practise in coding practice and track your progress in progress page.

Level
Beginner to advanced
Time
10–14 weeks at about 8 hours a week, less if you already know Java or another typed language
Scope
59 topics in 10 stages
Before you start
Basic programming ideas: variables, conditions, loops and functions in any language; Helpful: object-oriented concepts such as classes and interfaces, as in Java Roadmap; Comfort with a terminal and Git and GitHub Roadmap; For Android work, a willingness to learn the Android basics afterwards

What you will be able to do

  • Write idiomatic Kotlin using val, data classes, when expressions, lambdas and extension functions.
  • Use null safety to remove null pointer errors and handle values from Java safely.
  • Transform data with the collections and sequences libraries.
  • Write concurrent code with coroutines and Flow, with correct cancellation and error handling.
  • Build, test and package a project with Gradle and call between Kotlin and Java.
  • Choose between the JVM, Android, multiplatform and server options and start a project in one of them.

The roadmap at a glance

  1. Stage 1Getting Started with KotlinBeginner · 1 week · 4 topics
  2. Stage 2Language BasicsBeginner · 2 weeks · 6 topics
  3. Stage 3Functions and Functional StyleBeginner · 2 weeks · 6 topics
  4. Stage 4Null Safety and Type ChecksBeginner · 1 week · 3 topics
  5. Stage 5Classes and Object-Oriented KotlinIntermediate · 3 weeks · 10 topics
  6. Stage 6Collections and SequencesIntermediate · 2 weeks · 8 topics
  7. Stage 7Errors, I/O and the Standard LibraryIntermediate · 2 weeks · 5 topics
  8. Stage 8Coroutines and Asynchronous ProgrammingAdvanced · 3 weeks · 5 topics
  9. Stage 9Build Tools, Java Interop and TestingIntermediate · 2 weeks · 5 topics
  10. Stage 10Platforms and ApplicationsAdvanced · 3–4 weeks · 7 topics

Stage 1 Getting Started with Kotlin

Beginner1 week4 topics

Goal. Install a toolchain, run a first program and explain where Kotlin fits and why teams choose it.

Introduction to Kotlin, Why Use Kotlin? and History of Kotlin Core

Kotlin is a statically typed language from JetBrains. It first targeted the JVM and is now also compiled to JavaScript, WebAssembly and native code. Google made it the preferred language for Android, and it is widely used for back-end services and shared code across platforms.

The reasons teams adopt it: concise syntax with less boilerplate, null safety built into the type system, full interoperability with Java, first-class functions and coroutines for asynchronous code. History in brief: JetBrains announced it in the early 2010s, version 1.0 shipped in 2016, and the 2.x line introduced the rewritten K2 compiler. The language is governed openly with a public evolution process. See Java Roadmap for the platform it grew beside.

Practice. List three pieces of Java boilerplate you know and guess how a modern language could remove each.

Setting up the Kotlin Environment: IntelliJ IDEA, Android Studio and IDEs Core

IntelliJ IDEA, from the same company as Kotlin, has the best support: the free Community edition is enough to learn, and recent releases also cover Kotlin in the unified edition. Android Studio is built on IntelliJ and is the tool for Android work. Other IDEs and editors work through a language server, but the experience is thinner. VS Code has community extensions.

You also need a JDK, because most Kotlin runs on the JVM; the IDE can download one. Create a Gradle project with the Kotlin JVM plugin, or use the online playground for quick experiments. Learn the IDE's quick-fix key, the "Show Kotlin Bytecode" action and the REPL-like scratch files; they teach you what the compiler does with your code.

Practice. Install the IDE, create a Gradle Kotlin project and run the generated main function.

Main Function and Printing Data in Kotlin: print and println Core

A Kotlin program starts at main. Since Kotlin 1.3 the arguments are optional.

fun main() {
    println("Hello, Kotlin")
    print("No newline")
}

println writes text and a line break; print writes without one. Both accept any value and call its toString. Top-level functions do not need a class. Command-line arguments are available with fun main(args: Array<String>). A file's main compiles to a static method on a generated class named after the file with a Kt suffix, which matters when you call it from Java.

Statements do not need semicolons. Formatting follows the official coding conventions that the IDE can apply for you.

Practice. Print a small multiplication table using println and string templates.

Comments, Documentation, KDoc and Dokka Recommended

Kotlin supports // line comments and /* */ block comments, which can nest. KDoc is the documentation format: a /** */ block placed before a declaration, with Markdown-style text and tags such as @param, @return, @throws and @sample. Square brackets link to other declarations, as in [List].

Dokka is the documentation engine for Kotlin. It reads KDoc and builds HTML or other formats, and it also handles Java sources. Add it as a Gradle plugin and run its task in your build. Document the public API: what each function promises, what it does with null and errors, and which thread it expects. Keep comments for the reasons behind code; the code shows what it does.

Practice. Write KDoc for a public function with a parameter, a return value and an exception, and generate HTML with Dokka.

Stage 2 Language Basics

Beginner2 weeks6 topics

Goal. Write programs with typed variables, strings, conditions and loops, and use ranges and expressions the Kotlin way.

Variables: Val vs Var and Type Inference Core

val declares a read-only reference that you assign once; var can be reassigned. Prefer val and switch to var only when needed, because fewer moving parts means fewer bugs. Note that val does not make the object immutable, only the reference: a val list may still be a mutable list.

val name = "Asha"        // inferred as String
var count: Int = 0       // explicit type
count += 1

Type inference removes repeated type names, but public APIs should state their types so changes do not alter them silently. A const val at top level or inside an object is a compile-time constant for primitives and strings. A val can be assigned later if the compiler can prove it is set exactly once.

Practice. Rewrite a snippet that uses var everywhere so it uses val wherever the value never changes.

Data Types in Kotlin: Integers in Kotlin, Unsigned Integers, Floats in Kotlin, Booleans and Characters Core

Numbers are objects in the language but compile to JVM primitives when possible. Integer types: Byte, Short, Int, Long; a literal without a suffix is Int unless too large, L marks a Long. Kotlin does not widen implicitly, so you convert with toLong() and similar functions. Unsigned types UByte, UShort, UInt and ULong use the u suffix, as in 42u.

Floating point: Double is the default and Float needs an f suffix. Equality on floats follows IEEE rules, so compare with a tolerance. Boolean has &&, || and !, with short-circuit evaluation. Char is a single UTF-16 code unit written in single quotes; arithmetic like 'a' + 1 gives a Char.

Integer overflow wraps silently, so use Math.addExact or checked helpers where it matters.

Practice. Convert between Int, Long and Double, and show an overflow with Int.MAX_VALUE + 1.

Strings and String Templates Core

A String is immutable. Use string templates to insert values: "Hello, $name" for a variable and "${a + b}" for an expression. Raw strings use triple quotes, can span lines and ignore escapes, and trimIndent() removes common leading space.

val text = """
    |Name: $name
    |Items: ${items.size}
    """.trimMargin()

Useful functions: length, substring, split, trim, startsWith, contains, replace, lowercase, uppercase and format. Strings are indexable and iterable by character. Equality == compares content; === compares identity. For building text in a loop use buildString { } or a StringBuilder. Regular expressions use the Regex class.

Practice. Parse the line "name=Asha;age=31" into a map using split and print it with a template.

Conditional Expressions: if and the When Expression Core

In Kotlin if is an expression, so it returns a value and there is no ternary operator: val max = if (a > b) a else b.

when replaces switch and is far more flexible. Branches can match values, ranges, types and arbitrary conditions, with no fall-through.

fun describe(x: Any): String = when (x) {
    0 -> "zero"
    in 1..9 -> "small number"
    is String -> "text of ${x.length}"
    else -> "other"
}

When when is used as an expression it must be exhaustive: add else, or cover every case of an enum or sealed type and the compiler checks it. A when without a subject takes boolean conditions and replaces long if chains. Smart casts mean that after is String the variable can be used as a String.

Practice. Write a function that classifies a temperature as cold, mild or hot using a subject-less when.

Loops in Kotlin: Kotlin For Loop, While and Do-While Loops, Break and Continue in Kotlin Loops, Label Loops Core

for iterates over anything that provides an iterator: ranges, collections, strings and maps. There is no C-style three-part for.

for (i in 1..3) println(i)
for ((index, ch) in "abc".withIndex()) println("$index $ch")

while and do-while behave as in Java. break and continue work on the nearest loop. To control an outer loop, label it and refer to the label (a name followed by @), as in outer@ for (...) and break@outer. Label loops sparingly; extracting a function with return is often clearer.

forEach with a lambda is common, but break and continue do not work inside it, while a non-local return leaves the enclosing function. Use a for loop when you need those controls.

Practice. Find the first pair of numbers in a 2D list that sum to 10 using a labelled break, then redo it with a function.

Ranges and Progressions Core

A range is a sequence between two ends. 1..5 includes both ends; 1 until 5 excludes the last; 5 downTo 1 counts backwards; step skips values. These produce progressions, which are iterable arithmetic sequences. The operator ..< is a modern way to write a half-open range.

for (i in 0..<10 step 2) print("$i ")
val ok = 5 in 1..10
val letters = 'a'..'e'

Ranges work on any Comparable type, so you can use them for characters and dates. The in check is fast for integer ranges because it compiles to simple comparisons. Ranges are lazy and do not allocate a list. Use indices to loop over an array's valid positions and coerceIn to clamp a value.

Practice. Print the even numbers from 100 down to 0 and check whether a user age lies in 18..65.

Stage 3 Functions and Functional Style

Beginner2 weeks6 topics

Goal. Define flexible functions, use lambdas and higher-order functions and extend existing types.

Function Parameters, Return in Kotlin Functions and Varargs Core

A function is declared with fun, with parameter types after names and the return type after a colon. Parameters can have defaults, and callers can use named arguments, which removes many overloads and makes calls readable.

fun greet(name: String, greeting: String = "Hello") = "$greeting, $name"
greet("Asha")
greet(greeting = "Hi", name = "Ravi")

A function with a single expression body can use = and infer its return type. Functions that return nothing return Unit, which can be omitted. Use return for explicit exits, and Nothing for functions that never return normally. A vararg parameter accepts any number of values; pass an existing array with the spread operator *arr. Functions can be top-level, members or local. Parameters are read-only.

Practice. Write a function with default and named parameters and a vararg that joins values with a separator.

Local Functions and Tail-Recursive Functions Recommended

A function can be declared inside another function. A local function sees the outer function's variables, which keeps helper logic next to where it is used without making it part of a class's API.

For recursion, Kotlin offers tailrec. When a function's last action is a call to itself, the compiler turns it into a loop, so there is no stack growth.

tailrec fun gcd(a: Int, b: Int): Int =
    if (b == 0) a else gcd(b, a % b)

The compiler reports an error if a tailrec function is not actually tail-recursive, which is a useful check. Without it, deep recursion causes a StackOverflowError on the JVM. For mutual recursion or recursion over trees, use an explicit stack or DeepRecursiveFunction.

Practice. Write a tail-recursive sum of a list and show a normal recursive version failing on a very long list.

Lambda Functions, Anonymous Functions and Function Types Core

Functions are values. A function type is written (Int, Int) -> Int. A lambda is a function literal in braces: { a, b -> a + b }. With one parameter you can use the implicit name it. If the last argument is a lambda, it can go outside the parentheses.

val add: (Int, Int) -> Int = { a, b -> a + b }
val evens = listOf(1, 2, 3, 4).filter { it % 2 == 0 }
val sq = fun(x: Int): Int { return x * x }   // anonymous function

An anonymous function allows an explicit return type and uses return to leave itself, whereas return in a lambda leaves the enclosing function unless labelled (return@filter). Lambdas capture variables from their scope, including vars, which can then be modified inside the lambda. Function types can be nullable and can have receivers, as in String.() -> Unit.

Practice. Write a function that takes a (Int) -> Int and applies it to each element of a list, and call it three different ways.

Higher-Order Functions and Inline Functions Core

A higher-order function takes functions as parameters or returns one. The standard library is full of them: map, filter, fold, forEach, sortedBy.

Lambdas normally create objects and add call overhead. Marking a higher-order function inline makes the compiler copy the function body and the lambda into the call site, avoiding that cost and allowing non-local returns and reified type parameters. Use noinline and crossinline to control individual lambda parameters.

inline fun <reified T> List<*>.firstOfType(): T? =
    firstOrNull { it is T } as T?

Do not make every function inline: large bodies grow the bytecode. It pays off for small functions that take lambdas. Public inline functions expose their body, so changing them can break binary compatibility.

Practice. Write your own retry(times) { } higher-order function and make it inline.

Standard Functions: let, run, with, apply and also Core

The scope functions run a block on an object. They differ in how the object is referred to (it or this) and what they return.

  • let: object as it, returns the lambda result. Common with null checks: name?.let { use(it) }.
  • run: object as this, returns the lambda result.
  • with(obj) { }: like run, but takes the object as an argument.
  • apply: object as this, returns the object. Good for configuration.
  • also: object as it, returns the object. Good for side effects such as logging.
val user = User().apply { name = "Asha"; age = 31 }

Nesting several scope functions makes code hard to read, because this and it become ambiguous. Pick the one that matches your intent and keep blocks short. Also see takeIf and takeUnless, repeat and TODO.

Practice. Refactor a block of repeated object.property assignments with apply, and a null check with let.

Extension Functions Core

An extension function adds a function to an existing type without inheriting from it or changing its source. It is declared with a receiver type.

fun String.isPalindrome(): Boolean {
    val s = lowercase().filter { it.isLetter() }
    return s == s.reversed()
}
println("Level".isPalindrome())

Extensions are resolved statically by the declared type, not the runtime type, and they cannot access private members. A member function always wins over an extension with the same signature. Extension properties are also possible, but cannot have backing fields. Put extensions near their use or in a clearly named file, and import them like any other declaration. They are how much of the standard library, such as collection operations, is built. Use them for utilities, not to hide complex behaviour.

Practice. Write extension functions that capitalise each word in a string and that compute the median of a list of integers.

Stage 4 Null Safety and Type Checks

Beginner1 week3 topics

Goal. Use the type system to remove null pointer errors and check and convert types safely.

Null Safety: Nullable vs. Non-nullable Types Core

Kotlin splits every type into non-nullable (String) and nullable (String?). A non-nullable variable can never hold null, and the compiler rejects code that tries. A nullable variable must be handled before you use it. This moves a large class of runtime crashes to compile time.

var a: String = "text"
// a = null            // error
var b: String? = "text"
b = null               // fine
// b.length            // error: b may be null

Generic type parameters are nullable unless bounded: T may be String?, while T : Any excludes null. Types from Java without annotations are "platform types" shown with an exclamation mark after the type in IDE hints; the compiler lets you treat them either way, so decide explicitly at the boundary. Prefer non-null types in your own APIs and use null only for real absence.

Practice. Convert a Java-style method that returns null on failure into one that returns a nullable type, and handle it at the call site.

Nullability Check Operators and Safe Casts Core

Several operators deal with null:

  • Safe call ?. returns null if the receiver is null: user?.address?.city.
  • Elvis ?: supplies a default: val len = name?.length ?: 0. It can also return or throw: val n = x ?: return.
  • Not-null assertion !! throws if the value is null. Avoid it except where null is a true bug.
  • let with a safe call runs a block only for non-null values.

An explicit check if (x != null) smart-casts x to non-null inside the branch, as long as the compiler can prove it does not change (a local val, for instance). For a nullable property you often copy it to a local val first.

as? is the safe cast: it returns null instead of throwing if the cast fails. Collections have filterNotNull, mapNotNull and firstOrNull for the same purpose.

Practice. Rewrite a chain of nested null checks using safe calls and Elvis, and replace a !! with proper handling.

Type Checks and Casts Core

is tests a type at run time, !is tests the opposite. After a successful check the compiler smart-casts the variable, so you need no cast.

fun len(x: Any): Int =
    if (x is String) x.length else 0

An explicit cast x as String throws ClassCastException on failure; x as? String returns null. Generic type arguments are erased at run time on the JVM, so x is List<String> is not allowed; use is List<*> or a reified inline function. Casting numbers does not convert them: x as Int fails for a Long, so call toInt() to convert.

Use when with is branches to branch by type. With sealed hierarchies the compiler can prove all cases are handled and no cast is needed at all.

Practice. Write a function that accepts Any and returns a description by type, using when with is.

Stage 5 Classes and Object-Oriented Kotlin

Intermediate3 weeks10 topics

Goal. Model a domain with classes, interfaces, data classes, sealed hierarchies and generics.

Defining Classes, Constructors, Creating Instances and Properties in Kotlin Classes Core

A class is declared with class. The primary constructor sits in the header, and parameters marked val or var become properties at once. There is no new keyword: you create an instance by calling the class like a function.

class Account(val id: String, var balance: Long = 0) {
    init { require(balance >= 0) }
    constructor(id: String) : this(id, 0)
}
val acc = Account("A1", 100)

init blocks run when an instance is created, in order with property initialisers. Secondary constructors must delegate to the primary one. Properties have getters and setters generated for you; write custom ones with get() and set(value), and the field identifier refers to the backing field. lateinit var defers initialisation of a non-null property, and by lazy computes a value on first use. Classes are final by default.

Practice. Write a class with a validated property, a custom getter that computes a value and a secondary constructor.

Class Members in Kotlin and Methods in Kotlin Classes Core

Members are properties, member functions (methods), nested types and init blocks. Member functions are written with fun inside the class and can read the instance with this, which is usually implicit.

Operators can be overloaded by naming a member operator fun plus(other: Money) and similar names such as get, invoke, compareTo and contains. A member marked infix allows calls like a add b. A function marked open can be overridden, and override is mandatory in the subclass. Use companion object for members that belong to the class rather than the instance, like factories. Keep classes small and cohesive; move behaviour that is not about the state into extension functions or separate types. Expose read-only views and keep mutation inside the class.

Practice. Give a Money class plus and compareTo operators and a companion factory that parses a string.

Inheritance in Kotlin, Abstract Classes and Interfaces Core

Classes are closed by default. To allow subclassing, mark the class open; to allow overriding, mark the member open. A subclass calls the superclass constructor in its header: class Dog : Animal("dog").

An abstract class cannot be instantiated and may contain abstract members that subclasses implement. An interface declares behaviour with abstract members and optional default implementations, has no state of its own, and a class may implement many.

interface Shape { fun area(): Double }
class Circle(val r: Double) : Shape {
    override fun area() = Math.PI * r * r
}

If two interfaces provide the same default method, the class must override it and may call super<A>.f(). Prefer composition and interface delegation (class X(b: B) : B by b) over deep inheritance. Design for substitutability: a subclass must honour its parent's contract.

Practice. Model three shapes with an interface, then use delegation to add logging to one of them without subclassing.

Visibility Modifiers, Packages, Importing Packages and Default Imports Core

Kotlin has four visibility levels. public is the default. private limits access to the containing class or file. protected allows subclasses. internal means anywhere inside the same module (a Gradle or Maven module), which is the right tool for hiding implementation between modules.

Packages group declarations; the package line comes first in a file, and by convention matches the directory structure. Use import a.b.Name to bring a declaration in, import a.b.* for everything, and as to rename on a clash. Several packages are imported by default: kotlin.*, kotlin.collections.*, kotlin.text.*, kotlin.io.*, kotlin.ranges.* and kotlin.sequences.*, plus java.lang.* on the JVM.

For code organisation, keep one main concept per file but small related classes can share a file. Make declarations as private as they can be, and expose a small public API.

Practice. Split a small program into two packages and an internal helper, and verify what the other package can and cannot see.

Data Classes Core

A data class is for holding data. Mark it data and the compiler generates equals, hashCode, toString, copy and componentN functions from the properties in the primary constructor.

data class User(val name: String, val age: Int)
val u = User("Asha", 30)
val older = u.copy(age = 31)
val (n, a) = older          // destructuring

Use copy to make changed versions of immutable data. Destructuring takes components in order. Only properties declared in the primary constructor take part in the generated functions. Data classes can implement interfaces but cannot be abstract, open, sealed or inner. Prefer val properties so instances are effectively immutable, which is safer in collections, maps and across threads. They suit DTOs and values; entities with identity may need their own equals.

Practice. Create a data class, use it as a map key, copy it with a change and destructure it in a loop.

Enum Class and Sealed Class Core

An enum class defines a fixed set of constants. Each constant is an object and can hold properties and implement abstract members. entries lists them, valueOf finds one by name.

enum class Level(val code: Int) { LOW(1), HIGH(2) }

A sealed class (or sealed interface) restricts which subtypes can exist: all direct subclasses must be in the same package and module. Unlike enums, each subtype can carry different data. A when over a sealed type needs no else and the compiler flags a missing case.

sealed interface Result
data class Ok(val value: Int) : Result
data class Err(val msg: String) : Result

Sealed hierarchies are the standard way to model results, UI state and messages. Choose an enum when the set of values is fixed and each has the same shape.

Practice. Model a network result as a sealed interface and write a function that handles every case with when.

Object Declarations, Nested and Inner Classes Core

An object declaration creates a singleton: one instance, created lazily and thread-safely on first access. A companion object inside a class holds members that act like statics. An object expression object : Interface { ... } creates an anonymous object, like Java's anonymous class.

A class declared inside another is nested by default and has no access to the outer instance. Mark it inner to hold a reference to the outer object and use this@Outer. Nested classes are the better default because they avoid hidden references and memory leaks.

Singletons make testing harder, since they are global state. Use them for stateless utilities, constants or true singletons, and otherwise pass dependencies in through constructors. For Java callers use @JvmStatic and @JvmField to expose static-like members.

Practice. Create a companion object factory, an object singleton and an inner class that reads outer state.

Class Generics Core

Generics let a class or function work with many types while keeping type safety. Declare parameters in angle brackets: class Box<T>(val value: T). A bound limits them: <T : Comparable<T>>.

Variance describes subtyping of generic types. A type Producer<out T> only returns T, so a Producer<String> is a Producer<Any> (covariance). A Consumer<in T> only takes T, so a Consumer<Any> can be used where a Consumer<String> is expected (contravariance). Kotlin's read-only List<out E> is covariant, while MutableList<E> is invariant. At the use site you can write projections such as List<out Number>; star projection <*> means unknown type.

Remember that type arguments are erased at run time on the JVM, except through reified inline functions.

Practice. Write a generic pair class and a bounded generic max function, and use them with integers and strings.

Inline Classes, Type Aliases and Property Delegates Recommended

An inline class, now written as a @JvmInline value class, wraps a single value in a distinct type without allocating a wrapper object in most cases. It prevents mixing up two String or Long values, such as UserId and OrderId.

@JvmInline value class UserId(val raw: Long)
typealias Handler = (Event) -> Unit

A type alias gives a new name to an existing type, mostly to shorten long function or generic types. It creates no new type and adds no safety.

Property delegates move getter and setter logic into reusable objects with by. The standard ones are lazy (compute once), Delegates.observable and vetoable (react to change) and map (back properties with a map). You can write your own by providing getValue and setValue operators. Class delegation by also forwards interface calls to another object.

Practice. Write a value class for an email address with validation and a custom delegate that logs each write.

Opt-in Requirements and Annotations Optional

Opt-in requirements warn users of an API that it needs deliberate acceptance, usually because it is experimental or has special rules. A library author marks an annotation class with @RequiresOptIn, then annotates the API with it. Callers must write @OptIn(MarkerName::class) at the use site, or propagate the requirement by annotating their own function with the marker.

Many standard-library and coroutine features use this, for example @ExperimentalCoroutinesApi. Compiler arguments can opt a whole module in, but doing that hides the warning, so use it carefully. Annotations also drive code generation and runtime checks: @Deprecated with a replacement, @Suppress, @JvmName and many framework annotations. When you maintain a library, treat opt-in markers as part of your versioning promise.

Practice. Define an opt-in marker for an unstable function and use it from a second file with @OptIn.

Stage 6 Collections and Sequences

Intermediate2 weeks8 topics

Goal. Transform, filter, group and aggregate data with the collections library, and know when to switch to sequences.

Collections Overview: Lists in Kotlin, Sets in Kotlin, Maps in Kotlin and Arrays in Kotlin Core

The main collection types are List (ordered, duplicates allowed), Set (unique elements) and Map (key to value). Create them with listOf, setOf and mapOf, and mutableListOf, mutableSetOf and mutableMapOf, or emptyList() and friends. Maps are built with to: mapOf("a" to 1).

val names = listOf("Asha", "Ravi")
val ages = mutableMapOf("Asha" to 30)
ages["Ravi"] = 28
val first = names[0]

Arrays (Array<T>, IntArray, DoubleArray and so on) map to JVM arrays, have a fixed size and are mutable; the specialised ones avoid boxing and are good for performance work. Sets and maps default to hash-based and insertion-ordered implementations (LinkedHashSet, LinkedHashMap). Use getOrDefault, getOrPut and withDefault on maps. All of this builds on the Java collections.

Practice. Count word frequency in a sentence into a mutable map, then print the three most common words.

Read-Only vs. Mutable Collections and Iterators Core

A List or Map is a read-only view: no add or remove functions exist. A MutableList adds them. Read-only does not mean immutable: the same underlying list may be changed through a mutable reference. For real immutability, copy with toList() and do not share the mutable reference.

Prefer read-only types in parameters and return types so callers cannot change your state by accident. Use buildList { }, buildMap { } and buildSet { } to create a collection with mutation in a block and expose it as read-only.

An iterator walks a collection: iterator(), hasNext() and next(). A for loop uses it. A MutableIterator also supports remove(); use it, or removeAll and removeIf, to delete during iteration, since changing a list in a for loop throws ConcurrentModificationException.

Practice. Remove all negative numbers from a list while iterating, first the wrong way and then with removeAll.

Transformations in Kotlin Collections: map, flatMap, associate, zip Core

Transformations produce a new collection from an existing one. map applies a function to each element; mapIndexed also gives the index; mapNotNull drops nulls. flatMap maps each element to a collection and joins the results; flatten joins a list of lists. associate, associateBy and associateWith build maps. zip pairs two lists; unzip reverses it. joinToString makes text.

val words = listOf("kotlin", "coroutines")
val lengths = words.associateWith { it.length }
val pairs = words.zip(listOf(1, 2))
val text = words.joinToString(", ") { it.uppercase() }

Each call creates an intermediate collection, so for long chains on large inputs consider sequences. Transformations return new collections; the original is unchanged. Read the documentation names carefully: map with a function returning a list gives a list of lists.

Practice. Turn a list of orders into a map of customer to total spend using the transformation functions.

Filtering in Kotlin Collections Core

filter keeps elements that match a predicate, filterNot the opposite. filterIsInstance<T>() keeps elements of a type, filterNotNull() removes nulls, and partition splits one list into two by a condition. For maps, filterKeys and filterValues work on one side.

val nums = listOf(1, 2, 3, 4, 5, 6)
val (evens, odds) = nums.partition { it % 2 == 0 }
val big = nums.filter { it > 3 }
val ok = nums.any { it > 5 }    // all, none, count too

Testing predicates: any, all, none and count. They short-circuit where they can. Filtering and then mapping is idiomatic, and Kotlin has combined forms such as mapNotNull that avoid two passes. Keep predicates pure. For large data or lazily produced data, asSequence() before filter avoids building intermediate lists.

Practice. From a list of users, find those who are adults and active, and also whether any user is missing an email.

Retrieving Collection Parts and Retrieving single elements Core

To take parts of a collection: take(n), drop(n), takeLast, dropLast, takeWhile, dropWhile, slice and subList. chunked(n) splits into groups and windowed(size) gives sliding windows.

For single elements: first() and last() throw on an empty collection or a missing match, while firstOrNull() and lastOrNull() return null. elementAt, elementAtOrNull, getOrNull and getOrElse handle indexes safely. single() expects exactly one element. find is the same as firstOrNull with a predicate. random() picks one at random.

val xs = listOf(5, 3, 8, 1)
xs.take(2)               // [5, 3]
xs.firstOrNull { it > 6 } // 8
xs.windowed(2)           // [[5, 3], [3, 8], [8, 1]]

Prefer the OrNull forms when absence is a normal case.

Practice. Compute a moving average over a list of numbers using windowed.

Grouping, Ordering in Kotlin Collections and Aggregate Operations Core

groupBy returns a map from key to the list of elements with that key, and groupingBy followed by eachCount or fold aggregates without building the lists. Ordering: sorted, sortedDescending, sortedBy, sortedWith and a compareBy comparator with thenBy for tie breakers. reversed and shuffled also exist. The sort forms without ed sort a mutable list in place.

Aggregates reduce a collection to a value: sum, sumOf, min, max, minByOrNull, maxByOrNull, average, count, fold (with an initial value) and reduce (without; it throws on an empty list). fold is the general tool and the others are shortcuts.

val byLen = words.groupBy { it.length }
val total = orders.sumOf { it.price }
val sorted = people.sortedWith(compareBy({ it.age }, { it.name }))

Sorting is stable, which keeps equal elements in their original order.

Practice. Group a list of employees by department, sort each group by salary descending and print the highest paid in each.

Plus & Minus Operators in Kotlin Collections, Set Operations and List Operations in Kotlin Recommended

+ and - on collections return new ones. list + element appends, list + otherList joins, and list - element removes the first match. For a var collection, += reassigns a new one, while for a mutable collection held in a val, += calls add in place. This distinction causes ambiguity errors if you declare a var of a mutable type.

Set operations: union, intersect, subtract and the infix forms. They work on any collections and return sets.

List operations: indexOf, contains, subList, binarySearch on sorted lists, reversed, distinct, distinctBy, rotate via drop and take, and fill or sortBy in place on mutable lists. Use Set when you do repeated membership tests, since list contains is linear.

Practice. Compute the common and the unique tags between two articles using set operations.

Sequences Recommended

A Sequence evaluates lazily. Chained operations on a collection create a full intermediate list at each step. With a sequence, each element goes through the whole chain one at a time, and nothing happens until a terminal operation such as toList(), first() or sum() runs.

val firstSquares = generateSequence(1) { it + 1 }
    .map { it * it }
    .filter { it % 2 == 1 }
    .take(5)
    .toList()

Sequences suit large inputs, infinite streams (generateSequence, sequence { yield(...) }), and chains that finish early. For small collections, plain collection operations are usually faster because of lower overhead. Sequences can be consumed more than once only if the source allows it. Measure before changing to them.

Practice. Read a large text file line by line as a sequence and find the first line that contains an error message.

Stage 7 Errors, I/O and the Standard Library

Intermediate2 weeks5 topics

Goal. Handle failures, work with files and time, and use the Kotlin Standard Library and serialization.

Exceptions: Throwing Exceptions and Catching Exceptions Core

Kotlin has no checked exceptions: you are never forced to catch or declare them. Throw with throw IllegalStateException("message"). try, catch and finally work as in Java and try is an expression, so it can return a value.

val n = try { text.toInt() } catch (e: NumberFormatException) { null }

The standard library gives helpers: require checks arguments and throws IllegalArgumentException, check checks state and throws IllegalStateException, error("msg") throws, and TODO() marks unfinished code. toIntOrNull and similar functions avoid exceptions for expected failures. Use use { } on a Closeable to close it even when something fails.

For expected, recoverable failures, prefer return types: a nullable, a sealed result type or the standard Result. Reserve exceptions for unexpected conditions. In coroutines, never swallow CancellationException; rethrow it.

Practice. Write a parse function that returns a sealed result instead of throwing, and a second that throws with require.

I/O in Kotlin: Files, Buffered Streams and the I/O Library Core

Kotlin builds on java.io and java.nio and adds extension functions that make common work short. Read input with readln() and readlnOrNull(). For files, File("a.txt").readText(), readLines(), writeText() and appendText() handle small files in one call. Large files need streaming: bufferedReader().useLines { lines -> ... } reads lazily and closes the file at the end.

File("in.txt").bufferedReader().useLines { lines ->
    lines.filter { it.isNotBlank() }.forEach(::println)
}

Buffered streams reduce the number of calls to the operating system: wrap input or output streams in BufferedReader, BufferedWriter or BufferedInputStream. The kotlin.io.path API wraps java.nio.file.Path with functions like readText, walk and createDirectories. Always close resources with use. Kotlin/Native and multiplatform code use the separate kotlinx-io library.

Practice. Write a function that reads a log file as a stream and counts lines per severity level.

Creating Files, Writing & Reading Files Core

Create a file with File("name").createNewFile() or simply by writing to it. writeText replaces content; appendText adds to the end; writer().use { it.write(...) } gives a stream for more control. Create directories with mkdirs(), test with exists() and isDirectory, and delete with delete() or deleteRecursively().

With the path API:

val dir = Path("data").createDirectories()
val file = dir / "notes.txt"
file.writeText("first\n")
println(file.readLines())

Specify the character set when it matters (Charsets.UTF_8 is the default). Handle IOException for missing files and permissions. To write safely, write to a temporary file and move it into place. Never build paths by concatenating strings; use the path API and be careful with user-supplied names to avoid path traversal.

Practice. Write a program that saves a list of notes to a text file and loads it on the next run.

Date & Time, and the Kotlin Standard Library Recommended

On the JVM you can use java.time (LocalDate, Instant, Duration, ZonedDateTime). The kotlinx-datetime library provides a multiplatform version of the same ideas. The standard library has kotlin.time with Duration literals like 5.seconds and measureTime { } for timing code. Store instants in UTC and apply the time zone only at display.

The Kotlin Standard Library is the base set: collections, strings, ranges, sequences, scope functions, Result, lazy, Pair and Triple, text and I/O extensions, and kotlin.math. Know it well before reaching for other libraries. Many tasks such as groupBy, chunked, runCatching and buildString are already there. The library is shared across the JVM, JavaScript and native targets, with a few platform-specific parts.

Practice. Time a function with measureTime and compute the number of days between two dates.

Serialization Recommended

kotlinx.serialization converts objects to and from formats such as JSON, ProtoBuf and CBOR. It works through a compiler plugin that generates serialisers at build time, so it needs no reflection and works on all Kotlin targets.

@Serializable
data class User(val name: String, val age: Int)

val json = Json.encodeToString(User("Asha", 30))
val back = Json.decodeFromString<User>(json)

Annotate classes with @Serializable, rename fields with @SerialName and skip them with @Transient. Configure Json { ignoreUnknownKeys = true } for tolerant parsing of API responses. Polymorphism needs sealed classes or a registered module. On the JVM, Jackson and Gson also work, and Moshi is common in Android. Validate input at the boundary: decoding can succeed with default values you did not expect.

Practice. Serialize a nested data class to JSON and parse a JSON response with extra fields.

Stage 8 Coroutines and Asynchronous Programming

Advanced3 weeks5 topics

Goal. Write correct concurrent code with structured concurrency, dispatchers, cancellation and Flow.

Coroutines and Kotlin Coroutines Basics Core

A coroutine is a lightweight unit of work that can suspend without blocking a thread, so thousands can run on a few threads. Coroutine support comes from the compiler and a library, kotlinx.coroutines, which you add as a dependency.

The key idea is structured concurrency: coroutines run inside a scope, a parent waits for its children, and cancelling the parent cancels them all. This stops leaks and lost errors that plague loose threads.

fun main() = runBlocking {
    launch { delay(100); println("world") }
    println("hello")
}

runBlocking bridges blocking code and coroutines and belongs in main and tests, not in application code. Coroutines are not threads: they are state machines the compiler generates, resumed by a dispatcher on a thread pool. See Java Roadmap for the thread model they build on.

Practice. Write a program that launches 10,000 coroutines that each delay one second and compare the time with 10,000 threads.

Suspending Functions and Coroutine Builders Core

A suspending function is marked suspend and can pause at a suspension point, such as delay, and resume later. It can only be called from another suspending function or a coroutine. The suspend keyword changes the compiled signature, but it does not make blocking code non-blocking: calling Thread.sleep inside one still blocks the thread.

Coroutine builders start coroutines: launch starts one that returns a Job and no result; async returns a Deferred whose await() gives the result; runBlocking blocks the thread; coroutineScope { } creates a child scope and waits for it, which is the standard way to run parallel work inside a suspend function.

suspend fun load(): Pair<A, B> = coroutineScope {
    val a = async { fetchA() }
    val b = async { fetchB() }
    a.await() to b.await()
}

If one child fails, the scope cancels its siblings and rethrows.

Practice. Fetch two simulated slow values in parallel with async and show that the total time is the maximum, not the sum.

Coroutines Behavior: Dispatchers, Context, Cancellation and Exceptions Core

A coroutine context holds elements such as the Job and the dispatcher. Dispatchers choose the thread: Dispatchers.Default for CPU-bound work, Dispatchers.IO for blocking input and output, Dispatchers.Main for the UI thread on Android and desktop, and Dispatchers.Unconfined for special cases. Switch with withContext(Dispatchers.IO) { ... }.

Cancellation is cooperative. A coroutine stops at the next suspension point, or when it checks isActive or calls ensureActive(). A tight loop without a suspension point never cancels. Clean up in finally, and use withContext(NonCancellable) for cleanup that must suspend.

Exceptions in launch go to the parent and cancel the scope. With async, they surface on await. SupervisorJob and supervisorScope stop one child's failure from cancelling its siblings. Use withTimeout for deadlines. Catching Exception broadly also catches CancellationException, so rethrow it.

Practice. Write a loop that does CPU work and make it cancellable, then add a timeout of one second.

Asynchronous Flow Core

A Flow is a cold stream of values produced asynchronously, built on suspending functions. It starts only when collected, and each collector triggers a new run.

fun ticks(): Flow<Int> = flow {
    for (i in 1..3) { delay(100); emit(i) }
}
ticks().map { it * 2 }.collect { println(it) }

Operators such as map, filter, take, onEach, catch, flatMapLatest, combine and zip transform flows. Change the producer's dispatcher with flowOn, not withContext inside flow. Handle failures with catch. For hot streams that hold state or broadcast events use StateFlow and SharedFlow; stateIn and shareIn convert cold flows. buffer and conflate handle fast producers. Flow replaces most uses of callbacks and, on Android, LiveData.

Practice. Build a flow that emits search text with debounce and switches to the latest result using flatMapLatest.

Coroutines Best Practices Recommended

Practices that prevent most coroutine bugs:

  • Do not use GlobalScope; tie work to a scope with a lifecycle, such as a CoroutineScope you cancel, or a framework scope such as viewModelScope.
  • Inject dispatchers rather than hard-coding them, so tests can replace them.
  • Make suspend functions main-safe: a function that does blocking work should switch to the right dispatcher itself.
  • Expose suspend functions or flows, not Deferred or Job, from APIs.
  • Do not catch CancellationException and swallow it.
  • Avoid runBlocking outside main and tests.
  • Test with runTest from kotlinx-coroutines-test, which skips delays with virtual time.

Shared mutable state still needs protection: use a Mutex, confine state to one thread or use atomic types. Prefer immutable data passed between coroutines.

Practice. Review a small Android or server snippet that uses GlobalScope and rewrite it with a proper scope and an injected dispatcher.

Stage 9 Build Tools, Java Interop and Testing

Intermediate2 weeks5 topics

Goal. Build and test Kotlin projects, call between Kotlin and Java, and automate delivery.

Build Tools: Gradle, Maven and Gradle Plugins in Kotlin Core

Gradle is the default build tool for Kotlin. Build scripts are written in the Kotlin DSL (build.gradle.kts), which gives completion and type checking in the IDE. Apply the Kotlin plugin, add repositories and dependencies, set the JVM toolchain, and run tasks such as build, test and run.

plugins { kotlin("jvm") version "2.0.0" }
repositories { mavenCentral() }
dependencies { testImplementation(kotlin("test")) }
kotlin { jvmToolchain(17) }

Maven also supports Kotlin through its plugin and suits teams with existing Maven standards. You can write Gradle plugins in Kotlin as well: put logic for shared build conventions in a buildSrc directory or an included build with convention plugins, so many modules reuse one set of rules. Use version catalogs (libs.versions.toml) to keep dependency versions in one place.

Practice. Create a two-module Gradle project that shares a convention plugin and runs tests for both.

Build Tool API, CI/CD Tools and TeamCity Optional

The Build Tool API is an interface that lets build systems talk to the Kotlin compiler in a stable way, rather than depend on compiler internals. Most developers never call it directly; it matters to authors of build tools and plugins.

For continuous integration and delivery, run the same Gradle commands on a server for every commit: compile, test, static analysis, packaging and publishing. TeamCity, also from JetBrains, supports Kotlin in an unusual way: its pipelines can be defined as code with a Kotlin DSL. GitHub Actions, GitLab CI and Jenkins are common alternatives. Use the Gradle build cache and a wrapper (gradlew) so builds are reproducible, and fail the build on warnings that matter. See Git and GitHub Roadmap for the workflow side.

Practice. Write a CI configuration that builds, tests and caches Gradle for a Kotlin project.

Java Interoperability from Kotlin and Kotlin from Java Core

Kotlin and Java classes can call each other in one project. From Kotlin, Java getters and setters appear as properties, Java types are platform types, and SAM interfaces like Runnable accept lambdas. Annotate Java code with nullability annotations so Kotlin sees String or String? rather than a platform type.

From Java, a top-level function in Utils.kt is a static method on UtilsKt. Annotations make calls natural: @JvmStatic, @JvmField, @JvmOverloads (generates overloads for default parameters), @JvmName and @Throws (for checked exceptions Java callers expect). Kotlin properties become getters and setters. Companion members need @JvmStatic to appear static.

Watch for collections: Kotlin's read-only List is a java.util.List at run time, so Java code can mutate it. Migrate gradually, file by file.

Practice. Convert one Java class from a small project to Kotlin and call it from a remaining Java file using the annotations.

JVM Metadata and Kotlin Scripting Optional

Kotlin classes compiled for the JVM carry extra data in a @Metadata annotation: it records things the JVM class format cannot express, such as nullability, inline and visibility, data and sealed. The kotlin-metadata-jvm library reads and writes it. Frameworks, code generators and tools that inspect Kotlin classes need it; ordinary reflection on Kotlin classes can miss these details, and Kotlin reflection (kotlin-reflect) understands them.

Kotlin scripting runs .kts files without a project setup, and Gradle build files use the same machinery. Scripts are handy for small automation with the full standard library and Java libraries. Dependency annotations and tools such as kotlin-main-kts let a script declare what it needs. For larger tools, a normal compiled project is easier to test and maintain.

Practice. Write a .kts script that lists the largest files in a directory.

Testing with the Kotlin Test Library Core

kotlin.test is a multiplatform assertion library that works with JUnit, TestNG and the native and JavaScript runners. Mark a test with @Test and use assertEquals, assertTrue, assertFailsWith and friends. JUnit 5 is the usual runner on the JVM; Kotest and MockK are popular libraries for expressive assertions and mocking.

class MoneyTest {
    @Test
    fun adds() = assertEquals(Money(5), Money(2) + Money(3))
}

Test names in backticks may contain spaces. For coroutine code use runTest. Prefer fakes to mocks where practical. Keep logic in pure functions, which are easy to test. Run tests in a Gradle build with ./gradlew test and in CI.

Practice. Write tests for your sealed result parser covering success, error and edge cases, including a thrown exception.

Stage 10 Platforms and Applications

Advanced3–4 weeks7 topics

Goal. Choose a target platform, share code across platforms and build a real application with a relevant framework.

Kotlin Multiplatform and Compose Multiplatform Recommended

Kotlin Multiplatform (KMP) shares code between targets while keeping native code where needed. A project has a commonMain source set with shared code and platform source sets such as androidMain, iosMain, jvmMain and jsMain. The expect and actual mechanism declares an API in common code and implements it per platform.

KMP shares business logic, networking and storage (Ktor client, kotlinx.serialization, SQLDelight), while the UI can stay native or be shared with Compose Multiplatform, a declarative UI toolkit based on Jetpack Compose that targets Android, iOS, desktop and web. Teams usually start by sharing a data or domain module and expand from there. Check the current stability status of each target and library in the official documentation before committing.

Practice. Create a KMP project that shares a function and shows its result on Android and the desktop with a tiny Compose UI.

Kotlin/Native, Kotlin/JavaScript and Kotlin/WASM Recommended

Kotlin has several compilation targets. Kotlin/JVM produces bytecode. Kotlin/JavaScript compiles to JavaScript for browsers and Node.js, with interop through external declarations. Kotlin/WASM compiles to WebAssembly and is used with Compose Multiplatform for the web. Kotlin/Native compiles to machine code through LLVM, with no JVM, for iOS, macOS, Linux and Windows.

Kotlin/Native has its own memory manager with automatic garbage collection and supports sharing objects across threads. Each target supports most of the common standard library, plus platform libraries. Targets differ in maturity and library support, so choose libraries that are multiplatform. A command-line tool, a shared iOS framework and a web front end can all come from one language, but each has its own build and debugging details.

Practice. Compile a hello-world to JVM, JavaScript and native, and compare the build outputs.

C Interoperability in Kotlin/Native, and Swift/Objective-C Interop in Kotlin/Native Optional

Kotlin/Native can call C libraries directly. You describe a header in a .def file and the cinterop tool generates Kotlin bindings; you then call C functions and use structs and pointers from Kotlin in an unsafe style with memScoped and CPointer. This is how Kotlin reaches system APIs and existing C code. See C Roadmap for the C side.

For Apple platforms, Kotlin/Native generates an Objective-C header for a framework, so Swift and Objective-C code can use your shared Kotlin classes. Some Kotlin features are mapped imperfectly: generics, sealed classes, suspend functions and default arguments need care or helper libraries. Design the public API of a shared module to be simple for the Swift side, and test it from Swift early.

Practice. Bind one C function with cinterop and call it from Kotlin/Native.

Server-Side Applications in Kotlin: Ktor, Spring Boot with Kotlin, Quarkus and Vert.x Recommended

Kotlin is a strong back-end language. Ktor is the JetBrains framework built around coroutines, configured with a small Kotlin DSL for routing, plugins and serialisation. Spring Boot has first-class Kotlin support, including coroutine-aware controllers, and reaches the large Spring ecosystem: see Spring Boot Roadmap. Quarkus and Vert.x also support Kotlin and suit fast start-up and reactive designs. Micronaut and http4k are other options.

fun Application.module() {
    routing { get("/health") { call.respondText("ok") } }
}

For Spring, use the kotlin-spring and kotlin-jpa compiler plugins so classes can be proxied despite being final by default. Whichever you choose, the structure is similar: routes, services, repositories, configuration and tests. See API Design Roadmap and SQL Roadmap for the surrounding skills.

Practice. Build a REST endpoint in Ktor or Spring Boot that stores items and returns them as JSON.

Android Development: Android SDK, Android Jetpack and Android Studio Recommended

Android development is Kotlin's biggest use. The Android SDK provides the platform APIs, and Android Studio is the IDE with the emulator, profilers and layout tools. Jetpack is the set of libraries Google recommends: Compose for UI, ViewModel and lifecycle components, Navigation, Room for databases, WorkManager for background tasks, and DataStore for preferences.

A typical modern app uses Compose, a ViewModel exposing StateFlow, repositories and coroutines for data work, and dependency injection with Hilt or Koin. Learn the activity lifecycle, how configuration changes affect state, permissions and background limits. Kotlin features that matter most here are coroutines, Flow, sealed classes for UI state and null safety. See Android Developer for a full role roadmap.

Practice. Build a small Compose screen with a ViewModel that loads data with a coroutine and shows loading, success and error states.

Data Analysis with Kotlin: Kotlin Notebooks, DataFrame and Kotlin Kandy Optional

Kotlin is also used for data work. Kotlin Notebook is an interactive notebook inside IntelliJ IDEA where you mix code, results and charts, and can use any JVM library. Kotlin DataFrame is a typed library for loading, filtering, grouping and joining tabular data, with the compiler helping to catch column mistakes. Kandy is a plotting library for building charts in a declarative style.

These tools make Kotlin a reasonable choice when the rest of your code is already on the JVM, or when you want type-checked data pipelines. Python still has the largest ecosystem for machine learning; see Python for Data Analysis Roadmap. Use notebooks for exploration and move stable logic into tested modules.

Practice. Load a CSV into a DataFrame in a notebook, group it by one column and plot the result.

AI Development with Koog, Libraries and Competitive Programming with Kotlin Optional

Koog is a JetBrains framework for building AI agents in Kotlin, with support for tools, memory and multiple model providers. It brings agent design, typed tool definitions and tests to the Kotlin ecosystem. See AI Agents Roadmap for agent concepts that apply to any language.

Kotlin Libraries beyond the standard library are listed on the package repository and in the Kotlin documentation: Ktor, kotlinx.coroutines, kotlinx.serialization, Arrow for functional programming, Exposed and SQLDelight for databases, Koin for injection and MockK for testing. Check that a library supports the targets you need.

For competitive programming, Kotlin is accepted on most judges. Use fast input with BufferedReader, avoid heavy boxing in hot loops with IntArray, and rely on the collection functions to write short solutions. See Data Structures and Algorithms Roadmap for the algorithms.

Practice. Solve three practice problems in Kotlin with fast input, then pick one library from the list and add it to a sample project.

Projects to build

Build these in order. Each one should end up in a public repository with a short README that explains your decisions.

  1. Command-line expense trackerBeginner

    Build a program that reads commands such as add, list and total, stores expenses in a file and prints a summary by category. Use data classes, collections and file I/O.

    Data classes · Collections · File I/O · When expressions

  2. Text adventure with sealed statesBeginner

    Write a small text game where rooms, items and commands are modelled with sealed interfaces and enums. Every command handler should be an exhaustive when and the game state should be immutable.

    Sealed classes · Enums · Immutability · Extension functions

  3. JSON API clientIntermediate

    Call a public JSON service with an HTTP client, decode responses with kotlinx.serialization and show results from a command-line interface. Add retries, timeouts and tests with a fake server.

    Serialization · Coroutines · Error handling · Testing

  4. Concurrent web crawlerAdvanced

    Crawl pages from a start address with a limit on concurrency using coroutines and channels or Flow. Support cancellation, a timeout per request and a report of failures.

    Structured concurrency · Flow · Dispatchers · Cancellation

  5. Notes app with a shared moduleAdvanced

    Build a notes application with a Ktor or Spring Boot back end and an Android Compose or desktop client that share a model and validation module through Kotlin Multiplatform.

    Multiplatform · Ktor · Compose · Gradle modules

Interview questions

Try to answer each question out loud before you open the answer.

What is the difference between `val` and `var`, and between `val` and `const val`?Junior

val is a read-only reference assigned once; var can be reassigned. The object a val points to may still be mutable. const val is a compile-time constant limited to primitives and strings, declared at top level or in an object, and inlined where used. A plain val is initialised at run time and can hold any type.

How does Kotlin handle null safety?Junior

Types are non-nullable by default, and T? marks a nullable type. The compiler rejects use of a nullable value without a check. Tools: safe call ?., Elvis ?:, smart casts after a null check, let, as? and the not-null assertion !!, which throws and should be rare. Values from Java are platform types, so I handle them explicitly at the boundary.

What are data classes and what do they generate?Junior

A data class holds values. From the primary constructor properties the compiler generates equals, hashCode, toString, copy and componentN for destructuring. They cannot be abstract, open, sealed or inner. I use them with val properties for immutable data, and copy to create modified versions.

When would you use a sealed class instead of an enum?Mid

Enums are a fixed set of single instances sharing one shape. Sealed hierarchies have a fixed set of subtypes, but each can have its own properties and several instances. They suit results, UI state and messages. With when as an expression the compiler checks that every subtype is handled, so adding one breaks the build where it matters.

Explain scope functions and how to choose one.Mid

let, run, with, apply and also run a block with the object. They differ by how the object is referred to (it or this) and the return value. I use let for null checks and transformations, apply for configuring an object, also for side effects like logging, and run or with to compute a result using the object. I avoid nesting them.

How do extension functions work and what are their limits?Mid

An extension is a function with a receiver, compiled to a static method that takes the receiver as its first argument. It is resolved by the declared type at compile time, not dynamically, cannot access private members and loses to a member with the same signature. They are good for utilities and fluent APIs, but I avoid hiding heavy logic in them.

What is the difference between `launch` and `async`?Mid

Both start coroutines. launch returns a Job and is for work whose result you do not need; exceptions propagate to the parent scope. async returns a Deferred and await() gives the result or rethrows its exception. I use async inside coroutineScope to run independent calls in parallel and launch for fire-and-track work.

How does structured concurrency help?Mid

Each coroutine belongs to a scope. The parent does not finish until its children do, cancelling the parent cancels the children, and a failing child cancels its siblings and reports to the parent. That prevents leaked work and lost errors. In practice I tie scopes to a lifecycle, avoid GlobalScope and use supervisorScope where failures should be isolated.

How is cancellation handled in coroutines?Senior

It is cooperative. Suspending functions from the library check for cancellation, and in my own loops I call ensureActive() or check isActive. A cancelled coroutine throws CancellationException at a suspension point, which must not be swallowed. Cleanup goes in finally, with NonCancellable if the cleanup suspends.

What is the difference between a cold Flow, StateFlow and SharedFlow?Senior

A cold Flow runs its producer for every collector and does nothing until collected. StateFlow is hot, always holds a current value and replays it to new collectors, so it suits UI state. SharedFlow is hot and broadcasts events to many collectors, with configurable replay and buffer. I convert cold flows with stateIn or shareIn when a stream must be shared.

What is Kotlin Multiplatform and what does it share?Senior

It is a way to share code across targets while writing native code where needed. Common source sets hold shared logic, and expect and actual declarations connect to platform-specific code. Teams typically share data models, networking, storage and business rules, and keep the UI native or share it with Compose Multiplatform. The benefit is one tested implementation of the logic.

How would you design error handling in a Kotlin service?Senior

I use exceptions for unexpected faults and typed results for expected failures such as validation errors, modelled as a sealed interface so callers must handle each case. At the edge, a handler maps those results to responses. I use require and check for preconditions, never catch CancellationException, and log with enough context.

Common mistakes

  • Using !! to silence the compiler; handle null with ?:, let or a better type.
  • Declaring var and mutable collections by default; start with val and read-only types.
  • Starting coroutines in GlobalScope; use a scope with a lifecycle and cancel it.
  • Calling blocking code on Dispatchers.Default or the main thread; switch to Dispatchers.IO with withContext.
  • Swallowing CancellationException in a broad catch; rethrow it.
  • Chaining many scope functions until this and it are unclear; keep one level and name things.
  • Long collection chains on huge inputs; use a sequence or a single pass.
  • Forgetting nullability annotations on Java code, which leads to surprise crashes; annotate or wrap at the boundary.
  • Making everything public; use private and internal to keep the API small.
  • Testing coroutines with real delays; use runTest and virtual time.

Ready-for-interview checklist

  • I can explain null safety and use ?., ?: and smart casts correctly.
  • I can model data with data classes and results with sealed interfaces.
  • I can write lambdas, higher-order functions and extension functions.
  • I can choose the right scope function for a task.
  • I can transform, filter, group and aggregate collections and explain when to use sequences.
  • I can explain the difference between read-only and immutable collections.
  • I can write coroutines with launch, async and coroutineScope and explain structured concurrency.
  • I can handle cancellation, timeouts and exceptions in coroutines.
  • I can build a Flow pipeline and explain StateFlow.
  • I can set up a Gradle Kotlin DSL build with tests.
  • I can mix Kotlin and Java in one project using the interop annotations.
  • I can write unit tests for plain and suspending code.

Related roadmaps

FAQ

How long does it take to learn Kotlin?

If you already know Java or a similar language, about three to four weeks gets you writing useful Kotlin. Becoming interview-ready, including coroutines, collections and interop, typically takes two to three months at around eight hours a week. For a first language, plan for four to five months.

Should I learn Java before Kotlin?

It is not required, but you will meet Java in most Kotlin jobs because the libraries, build tools and many codebases are Java-based. Learn Kotlin first if your goal is Android or modern back-end work, and pick up Java reading skills as you go.

Is Kotlin only for Android?

No. Android is the biggest use, but Kotlin is also used for back-end services with Ktor and Spring Boot, for command-line tools, for data work in notebooks, and for sharing code between Android, iOS, desktop and web with Kotlin Multiplatform.

Is Kotlin better than Java?

It is more concise and has null safety, coroutines and better defaults, so many teams prefer it for new code. Java has a larger pool of existing code and developers and has added features such as records and virtual threads. They interoperate, so the choice is rarely all or nothing.

What should I learn after the basics?

Learn coroutines and Flow next, then one platform: Android with Compose, or a server framework such as Ktor or Spring Boot. Add Gradle, testing and a small multiplatform project when you have time.

Practise while you learn.Solve interview problems in the browser, with hints and a timer.
CodingSQLSystem design
Next Post Previous Post
No Comment
Add Comment
comment url