Kotlin Development Fundamentals
Overview
Kotlin is a modern, statically typed language targeting JVM, JavaScript, and native platforms. It emphasizes:
- Null safety at the type system level (nullable
T?vs non-nullableT) - Concise syntax with data classes, smart casts, and type inference
- Coroutines for structured asynchronous programming
- Full Java interoperability for gradual migration
When to use this skill: Writing Kotlin applications, Android development, migrating from Java, or learning Kotlin idioms.
Skill Hierarchy
lang-kotlin-dev (foundational - THIS SKILL)
├── lang-kotlin-coroutines-eng (advanced concurrency patterns)
├── lang-kotlin-library-dev (library design and publishing)
├── lang-kotlin-patterns-eng (design patterns in Kotlin)
└── lang-kotlin-multiplatform-dev (KMP development)
Quick Reference
| Pattern | When to Use | Key Syntax |
|---|---|---|
| Null Safety | Handling nullable types | Type?, ?., !!, ?: |
| Data Classes | Value objects, DTOs | data class User(val name: String) |
| Sealed Classes | Restricted hierarchies | sealed class Result<T> |
| Extension Functions | Add methods to existing types | fun String.isPalindrome() |
| Scope Functions | Context-based operations | let, run, with, apply, also |
| Coroutines | Asynchronous programming | suspend, launch, async |
| Delegation | Property/class delegation | by lazy, by keyword |
| Type-Safe Builders | DSL construction | Lambda with receiver |
Skill Routing
Route to specialized skills when:
- lang-kotlin-coroutines-eng: Deep coroutine patterns (Flow, channels, structured concurrency)
- lang-kotlin-library-dev: Building and publishing Kotlin libraries
- lang-kotlin-patterns-eng: Advanced design patterns and architectural patterns
- lang-kotlin-multiplatform-dev: Kotlin Multiplatform projects (KMP)
Module System
Kotlin organizes code into packages and provides visibility modifiers to control access.
1.1 Package Declarations
// Package declaration at file top
package com.example.myapp.utils
// Package-level functions (not in a class)
fun helper(): String = "I'm package-level"
// Package-level properties
val VERSION = "1.0.0"
1.2 Imports
// Single import
import com.example.myapp.User
// Wildcard import (all public members)
import com.example.myapp.utils.*
// Aliased import (resolve naming conflicts)
import com.example.myapp.User as AppUser
import org.external.User as ExternalUser
// Import extension functions
import com.example.extensions.formatCurrency
// Import enum entries
import com.example.Status.ACTIVE
import com.example.Status.INACTIVE
1.3 Visibility Modifiers
// public (default) - visible everywhere
class PublicClass
// internal - visible within the same module
internal class ModuleClass
// private - visible within the file (top-level) or class
private class FilePrivateClass
class Example {
public val publicProp = 1 // Visible everywhere
internal val internalProp = 2 // Same module
protected val protectedProp = 3 // Subclasses only
private val privateProp = 4 // This class only
}
1.4 Object Declarations (Singletons)
// Singleton object
object Logger {
fun log(message: String) = println(message)
}
// Usage
Logger.log("Hello")
// Companion object (static-like members)
class Factory {
companion object {
fun create(): Factory = Factory()
const val TAG = "Factory"
}
}
// Usage
val instance = Factory.create()
val tag = Factory.TAG
1.5 File Organization
// Filename: UserRepository.kt
package com.example.repository
// Multiple classes per file allowed (unlike Java)
data class User(val id: Int, val name: String)
data class UserDto(val id: Int, val displayName: String)
// Extension functions in same file
fun User.toDto() = UserDto(id, name)
// Top-level functions
fun findUserById(id: Int): User? = null
Best Practices:
- One primary class per file, named after the class
- Group related extension functions with their target type
- Use
internalfor module-private APIs in libraries
2. Null Safety
Kotlin's type system distinguishes between nullable and non-nullable types, eliminating most null pointer exceptions at compile time.
2.1 Nullable Types
// Non-nullable type (default)
var name: String = "Kotlin"
// name = null // Compilation error
// Nullable type
var nullableName: String? = "Kotlin"
nullableName = null // OK
// Function parameters
fun greet(name: String?) {
// Must handle null case
}
2.2 Safe Call Operator (?.)
val length: Int? = nullableName?.length
// Chaining safe calls
val firstChar: Char? = nullableName?.uppercase()?.firstOrNull()
// Safe calls with let
nullableName?.let { name ->
println("Name is $name")
}
// Safe call in chains
data class Person(val name: String, val address: Address?)
data class Address(val city: String?)
val city: String? = person?.address?.city
2.3 Elvis Operator (?:)
// Provide default value for null
val length: Int = nullableName?.length ?: 0
// Return early on null
fun processName(name: String?): String {
val trimmed = name?.trim() ?: return "No name provided"
return "Hello, $trimmed"
}
// Throw exception on null
val nonNullName = nullableName ?: throw IllegalArgumentException("Name required")
// Chaining with safe calls
val displayName = person?.name?.trim() ?: "Anonymous"
2.4 Not-Null Assertion (!!)
// Force unwrap - throws NPE if null
val length: Int = nullableName!!.length
// Use sparingly and only when certain
fun processSurelyNonNull(value: String?) {
// Only if you're absolutely certain
val definiteValue: String = value!!
}
// Better: Use assertion with reasoning
val config: Config = loadConfig()
?: error("Config must be initialized at startup")
Warning: Avoid !! in production code. Use proper null handling instead.
2.5 Safe Casts (as?)
// Safe cast returns null if cast fails
val stringValue: String? = value as? String
// Combined with elvis operator
val length: Int = (value as? String)?.length ?: 0
// Pattern matching alternative
when (value) {
is String -> println("String of length ${value.length}")
is Int -> println("Integer: $value")
else -> println("Unknown type")
}
2.6 Null Safety Patterns
// Pattern 1: Early return with elvis
fun processUser(user: User?): Result {
val validUser = user ?: return Result.Error("User not found")
return Result.Success(validUser)
}
// Pattern 2: Smart casts after null check
fun printLength(value: String?) {
if (value != null) {
// value is smart-casted to String
println(value.length)
}
}
// Pattern 3: require/check for preconditions
fun processPositive(value: Int?) {
requireNotNull(value) { "Value must not be null" }
require(value > 0) { "Value must be positive" }
// value is smart-casted to Int
}
// Pattern 4: Safe collection operations
val names: List<String?> = listOf("Alice", null, "Bob")
val nonNullNames: List<String> = names.filterNotNull()
val firstNonNull: String? = names.firstOrNull { it != null }
3. Data Classes
Data classes provide automatic implementation of equals(), hashCode(), toString(), and copy().
2.1 Basic Data Classes
// Simple data class
data class User(
val id: Long,
val name: String,
val email: String
)
// Automatic implementations
val user1 = User(1, "Alice", "alice@example.com")
val user2 = User(1, "Alice", "alice@example.com")
println(user1 == user2) // true (structural equality)
println(user1.toString()) // User(id=1, name=Alice, email=alice@example.com)
// Destructuring
val (id, name, email) = user1
println("User $name has ID $id")
2.2 Copy Method
data class User(
val id: Long,
val name: String,
val email: String,
val isActive: Boolean = true
)
val user = User(1, "Alice", "alice@example.com")
// Create modified copy
val updatedUser = user.copy(email = "alice@newdomain.com")
val inactiveUser = user.copy(isActive = false)
// Copy preserves other fields
println(updatedUser.name) // Alice
2.3 Data Classes with Validation
data class Email(val value: String) {
init {
require(value.contains("@")) { "Invalid email format" }
}
}
data class User(
val id: Long,
val name: String,
val email: Email
) {
init {
require(id > 0) { "ID must be positive" }
require(name.isNotBlank()) { "Name cannot be blank" }
}
}
// Usage
val user = User(1, "Alice", Email("alice@example.com"))
// val invalid = User(-1, "", Email("invalid")) // Throws IllegalArgumentException
2.4 Data Classes with Computed Properties
data class Rectangle(
val width: Double,
val height: Double
) {
// Not part of equals/hashCode/toString
val area: Double
get() = width * height
val perimeter: Double
get() = 2 * (width + height)
fun isSquare(): Boolean = width == height
}
val rect = Rectangle(4.0, 5.0)
println(rect.area) // 20.0
2.5 Data Classes in Collections
data class User(val id: Long, val name: String)
// Set automatically uses equals/hashCode
val users = setOf(
User(1, "Alice"),
User(1, "Alice"), // Duplicate, ignored
User(2, "Bob")
)
println(users.size) // 2
// Map keys
val userMap = mapOf(
User(1, "Alice") to "admin",
User(2, "Bob") to "user"
)
// Finding in collections
val alice = User(1, "Alice")
println(userMap[alice]) // admin
4. Sealed Classes and Interfaces
Sealed classes represent restricted hierarchies where all subclasses are known at compile time.
3.1 Basic Sealed Classes
// Sealed class for result types
sealed class Result<out T> {
data class Success<T>(val data: T) : Result<T>()
data class Error(val exception: Exception) : Result<Nothing>()
data object Loading : Result<Nothing>()
}
// Exhaustive when expressions
fun <T> handleResult(result: Result<T>) {
when (result) {
is Result.Success -> println("Data: ${result.data}")
is Result.Error -> println("Error: ${result.exception.message}")
Result.Loading -> println("Loading...")
}
// No else branch needed - compiler knows all cases
}
3.2 Sealed Interfaces
sealed interface NetworkState
data class Connected(val speed: Int) : NetworkState
data class Disconnected(val reason: String) : NetworkState
data object Connecting : NetworkState
fun updateUI(state: NetworkState) {
when (state) {
is Connected -> showConnected(state.speed)
is Disconnected -> showError(state.reason)
Connecting -> showLoading()
}
}
3.3 Nested Sealed Hierarchies
sealed class ApiResponse<out T> {
data class Success<T>(val body: T) : ApiResponse<T>()
sealed class Failure : ApiResponse<Nothing>() {
data class HttpError(val code: Int, val message: String) : Failure()
data class NetworkError(val exception: Exception) : Failure()
data object Unauthorized : Failure()
}
data object Loading : ApiResponse<Nothing>()
}
fun <T> processResponse(response: ApiResponse<T>) {
when (response) {
is ApiResponse.Success -> handleSuccess(response.body)
is ApiResponse.Failure.HttpError -> handleHttp(response.code)
is ApiResponse.Failure.NetworkError -> handleNetwork(response.exception)
ApiResponse.Failure.Unauthorized -> redirectToLogin()
ApiResponse.Loading -> showSpinner()
}
}
3.4 Sealed Classes for State Machines
sealed class OrderState {
data object Created : OrderState()
data class Confirmed(val confirmationId: String) : OrderState()
data class Shipped(val trackingNumber: String) : OrderState()
data class Delivered(val signature: String) : OrderState()
data class Cancelled(val reason: String) : OrderState()
}
class Order(var state: OrderState) {
fun confirm(confirmationId: String) {
state = when (state) {
is OrderState.Created -> OrderState.Confirmed(confirmationId)
else -> throw IllegalStateException("Can only confirm created orders")
}
}
fun ship(trackingNumber: String) {
state = when (state) {
is OrderState.Confirmed -> OrderState.Shipped(trackingNumber)
else -> throw IllegalStateException("Can only ship confirmed orders")
}
}
}
3.5 Pattern: Railway-Oriented Programming
sealed class Either<out L, out R> {
data class Left<L>(val value: L) : Either<L, Nothing>()
data class Right<R>(val value: R) : Either<Nothing, R>()
}
// Extension functions for functional pipeline
fun <L, R, T> Either<L, R>.map(f: (R) -> T): Either<L, T> =
when (this) {
is Either.Left -> this
is Either.Right -> Either.Right(f(value))
}
fun <L, R, T> Either<L, R>.flatMap(f: (R) -> Either<L, T>): Either<L, T> =
when (this) {
is Either.Left -> this
is Either.Right -> f(value)
}
// Usage
fun validateEmail(email: String): Either<String, String> =
if (email.contains("@")) Either.Right(email)
else Either.Left("Invalid email")
fun validateLength(email: String): Either<String, String> =
if (email.length >= 5) Either.Right(email)
else Either.Left("Email too short")
val result = validateEmail("test@example.com")
.flatMap { validateLength(it) }
.map { it.lowercase() }
5. Extension Functions
Extension functions add new functions to existing classes without modifying their source code.
4.1 Basic Extensions
// Extend String
fun String.isPalindrome(): Boolean {
val cleaned = this.lowercase().filter { it.isLetterOrDigit() }
return cleaned == cleaned.reversed()
}
println("A man a plan a canal Panama".isPalindrome()) // true
// Extend nullable types
fun String?.orDefault(default: String): String = this ?: default
val name: String? = null
println(name.orDefault("Guest")) // Guest
4.2 Extension Properties
// Read-only extension property
val String.wordCount: Int
get() = split("\\s+".toRegex()).size
println("Hello world from Kotlin".wordCount) // 4
// Extension property with backing field not allowed
// Must use get() or set()
var StringBuilder.lastChar: Char
get() = this[length - 1]
set(value) {
this[length - 1] = value
}
4.3 Generic Extensions
// Generic extension for any type
fun <T> T.print(): T {
println(this)
return this
}
"Hello".print().uppercase().print()
// Extension with type constraints
fun <T : Comparable<T>> List<T>.second(): T? =
if (size >= 2) this[1] else null
// Extension for collections
fun <T> List<T>.secondOrNull(): T? =
if (size >= 2) this[1] else null
println(listOf(1, 2, 3).secondOrNull()) // 2
4.4 Extensions on Companion Objects
class User(val name: String) {
companion object {
// Regular companion object function
}
}
// Extension on companion object
fun User.Companion.fromJson(json: String): User {
// Parse JSON and create User
return User("Parsed from JSON")
}
// Usage
val user = User.fromJson("""{"name": "Alice"}""")
4.5 Scope-Limited Extensions
// Extension only visible in this scope
class HtmlBuilder {
private fun String.escapeHtml(): String =
replace("&", "&")
.replace("<", "<")
.replace(">", ">")
fun paragraph(text: String): String =
"<p>${text.escapeHtml()}</p>"
}
// Extension not visible outside HtmlBuilder
4.6 Common Extension Patterns
// Collection extensions
fun <T> List<T>.splitAt(index: Int): Pair<List<T>, List<T>> =
take(index) to drop(index)
fun <K, V> Map<K, V>.getOrThrow(key: K): V =
this[key] ?: throw NoSuchElementException("Key $key not found")
// Validation extensions
fun String.requireNotBlank(fieldName: String): String {
require(isNotBlank()) { "$fieldName cannot be blank" }
return this
}
// Transformation extensions
fun String.toTitleCase(): String =
split(" ").joinToString(" ") { word ->
word.lowercase().replaceFirstChar { it.uppercase() }
}
// Utility extensions
inline fun <T> T.applyIf(condition: Boolean, block: T.() -> Unit): T {
if (condition) block()
return this
}
val list = mutableListOf(1, 2, 3)
.applyIf(someCondition) { add(4) }
6. Scope Functions
Scope functions execute a block of code within the context of an object.
5.1 let - Null Safety and Transformations
// Execute block if not null
val name: String? = "Alice"
name?.let {
println("Name is $it")
println("Length is ${it.length}")
}
// Transform and return result
val length = name?.let { it.length } ?: 0
// Chain operations
val result = getUserInput()
?.trim()
?.let { parseUserData(it) }
?.let { validateData(it) }
?.let { saveToDatabase(it) }
// Scope to new variable
val validatedEmail = email?.let { value ->
if (value.contains("@")) value else null
}
5.2 run - Object Configuration and Computation
// Run on nullable receiver
val config: Config? = loadConfig()
val port = config?.run {
validate()
applyDefaults()
port
} ?: 8080
// Compute with context
val hexColor = run {
val r = (color shr 16) and 0xFF
val g = (color shr 8) and 0xFF
val b = color and 0xFF
"#%02X%02X%02X".format(r, g, b)
}
// Scope local variables
val result = run {
val a = computeA()
val b = computeB()
a + b
}
5.3 with - Multiple Operations on Same Object
// Non-extension function
val person = Person()
with(person) {
name = "Alice"
age = 30
email = "alice@example.com"
validate()
}
// Return value from with
val result = with(StringBuilder()) {
append("Hello")
append(" ")
append("World")
toString()
}
// Multiple property access
val info = with(user) {
"Name: $name, Email: $email, Active: $isActive"
}
5.4 apply - Object Configuration (Returns Receiver)
// Builder-style configuration
val person = Person().apply {
name = "Alice"
age = 30
email = "alice@example.com"
}
// Chain multiple apply blocks
val request = HttpRequest().apply {
url = "https://api.example.com"
method = "POST"
}.apply {
addHeader("Content-Type", "application/json")
addHeader("Authorization", "Bearer $token")
}.apply {
body = """{"data": "value"}"""
}
// Conditional configuration
val user = User().apply {
name = "Alice"
if (isAdmin) {
role = "ADMIN"
permissions = adminPermissions
}
}
5.5 also - Side Effects (Returns Receiver)
// Logging and debugging
val result = processData(input)
.also { println("Processed result: $it") }
.also { log.debug("Result details: $it") }
// Validation before return
fun createUser(name: String): User {
return User(name).also {
require(it.isValid()) { "Invalid user" }
saveToDatabase(it)
}
}
// Multiple side effects
val file = File("output.txt")
.also { it.createNewFile() }
.also { it.setWritable(true) }
.also { it.writeText("Hello") }
5.6 Choosing the Right Scope Function
// Decision guide:
// let - Transform nullable, introduce local scope
val length: Int = nullableString?.let { it.length } ?: 0
// run - Compute value using multiple statements
val total = run {
val subtotal = items.sumOf { it.price }
val tax = subtotal * 0.1
subtotal + tax
}
// with - Multiple operations, focus on context
with(canvas) {
drawLine(0, 0, width, height)
drawCircle(width / 2, height / 2, 50)
}
// apply - Configure object, return it
val person = Person().apply {
name = "Alice"
age = 30
}
// also - Side effects, return object
val saved = user.also { repository.save(it) }
5.7 Nested Scope Functions
// Chaining different scope functions
val result = fetchUser(id)
?.let { user ->
user.apply {
lastLoginAt = Instant.now()
}
}
?.also { repository.save(it) }
?.let { user ->
UserDto(user.id, user.name)
}
// Complex transformations
val config = loadConfig()
?.run {
validate()
this
}
?.apply {
applyDefaults()
}
?.also { logger.info("Config loaded: $it") }
?: DefaultConfig()
7. Collections and Sequences
Kotlin provides rich collection APIs with eager (collections) and lazy (sequences) evaluation.
6.1 Collection Basics
// List (immutable)
val numbers = listOf(1, 2, 3, 4, 5)
val empty = emptyList<Int>()
// MutableList
val mutable = mutableListOf(1, 2, 3)
mutable.add(4)
mutable.removeAt(0)
// Set (unique elements)
val uniqueNumbers = setOf(1, 2, 2, 3, 3) // [1, 2, 3]
val mutableSet = mutableSetOf<String>()
// Map
val ages = mapOf("Alice" to 30, "Bob" to 25)
val mutableMap = mutableMapOf<String, Int>()
mutableMap["Charlie"] = 35
6.2 Collection Transformations
val numbers = listOf(1, 2, 3, 4, 5)
// map - Transform each element
val squared = numbers.map { it * it } // [1, 4, 9, 16, 25]
// filter - Keep matching elements
val evens = numbers.filter { it % 2 == 0 } // [2, 4]
// mapNotNull - Transform and filter nulls
val strings = listOf("1", "2", "abc", "3")
val parsed = strings.mapNotNull { it.toIntOrNull() } // [1, 2, 3]
// flatMap - Transform and flatten
val nested = listOf(listOf(1, 2), listOf(3, 4))
val flattened = nested.flatMap { it } // [1, 2, 3, 4]
// partition - Split into two lists
val (evens, odds) = numbers.partition { it % 2 == 0 }
// groupBy - Group into map
val byParity = numbers.groupBy { if (it % 2 == 0) "even" else "odd" }
// {odd=[1, 3, 5], even=[2, 4]}
6.3 Collection Aggregations
val numbers = listOf(1, 2, 3, 4, 5)
// sum, average, min, max
val sum = numbers.sum() // 15
val avg = numbers.average() // 3.0
val max = numbers.maxOrNull() // 5
// sumOf - Transform then sum
val lengthSum = listOf("a", "bb", "ccc").sumOf { it.length } // 6
// reduce - Combine elements
val product = numbers.reduce { acc, n -> acc * n } // 120
// fold - Reduce with initial value
val sumPlus10 = numbers.fold(10) { acc, n -> acc + n } // 25
// joinToString - Create string
val csv = numbers.joinToString(separator = ",") // "1,2,3,4,5"
val custom = numbers.joinToString(
separator = " | ",
prefix = "[",
postfix = "]",
limit = 3,
truncated = "..."
) { "Item $it" }
// [Item 1 | Item 2 | Item 3 | ...]
6.4 Collection Queries
val numbers = listOf(1, 2, 3, 4, 5)
// any, all, none
val hasEven = numbers.any { it % 2 == 0 } // true
val allPositive = numbers.all { it > 0 } // true
val noneNegative = numbers.none { it < 0 } // true
// find, first, last
val firstEven = numbers.find { it % 2 == 0 } // 2
val firstOrNull = numbers.firstOrNull { it > 10 } // null
val last = numbers.last() // 5
// take, drop, slice
val first3 = numbers.take(3) // [1, 2, 3]
val without2 = numbers.drop(2) // [3, 4, 5]
val middle = numbers.slice(1..3) // [2, 3, 4]
// distinct, distinctBy
val duplicates = listOf(1, 2, 2, 3, 3, 3)
val unique = duplicates.distinct() // [1, 2, 3]
data class Person(val name: String, val age: Int)
val people = listOf(Person("Alice", 30), Person("Bob", 30))
val byAge = people.distinctBy { it.age } // [Person("Alice", 30)]
6.5 Sequences (Lazy Evaluation)
// Sequence - lazy evaluation
val sequence = sequenceOf(1, 2, 3, 4, 5)
// Convert to sequence for lazy evaluation
val numbers = listOf(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
// Eager (collection) - all operations execute immediately
val eagerResult = numbers
.map { println("map $it"); it * 2 } // Prints 10 times
.filter { println("filter $it"); it > 10 } // Prints 10 times
.take(2) // Still processed all elements
// Lazy (sequence) - operations execute on demand
val lazyResult = numbers.asSequence()
.map { println("map $it"); it * 2 }
.filter { println("filter $it"); it > 10 }
.take(2) // Only processes until 2 elements found
.toList()
// Generate infinite sequence
val naturalNumbers = generateSequence(1) { it + 1 }
val first100Even = naturalNumbers
.filter { it % 2 == 0 }
.take(100)
.toList()
// Sequence from function
val fibonacci = sequence {
var a = 0
var b = 1
while (true) {
yield(a)
val next = a + b
a = b
b = next
}
}
val first10Fib = fibonacci.take(10).toList()
6.6 Collection Building
// buildList - Builder function
val squares = buildList {
for (i in 1..5) {
add(i * i)
}
if (someCondition) {
add(100)
}
}
// buildSet
val uniqueWords = buildSet {
add("hello")
add("world")
add("hello") // Duplicate ignored
}
// buildMap
val characterCounts = buildMap {
for (char in "hello") {
put(char, getOrDefault(char, 0) + 1)
}
}
// associate - Build map from collection
val users = listOf(User(1, "Alice"), User(2, "Bob"))
val byId = users.associateBy { it.id }
val byName = users.associateBy({ it.name }, { it.id })
6.7 Advanced Collection Patterns
// Windowed - Sliding windows
val numbers = listOf(1, 2, 3, 4, 5)
val windows = numbers.windowed(size = 3, step = 1)
// [[1, 2, 3], [2, 3, 4], [3, 4, 5]]
// Chunked - Fixed-size groups
val chunks = numbers.chunked(2) // [[1, 2], [3, 4], [5]]
// Zip - Combine two collections
val names = listOf("Alice", "Bob")
val ages = listOf(30, 25)
val pairs = names.zip(ages) // [(Alice, 30), (Bob, 25)]
val combined = names.zip(ages) { name, age -> "$name is $age" }
// Union, intersect, subtract (Sets)
val set1 = setOf(1, 2, 3)
val set2 = setOf(2, 3, 4)
val union = set1 union set2 // [1, 2, 3, 4]
val intersect = set1 intersect set2 // [2, 3]
val subtract = set1 subtract set2 // [1]
8. Higher-Order Functions and Lambdas
Functions that take functions as parameters or return functions.
7.1 Lambda Syntax
// Lambda expression
val sum: (Int, Int) -> Int = { a, b -> a + b }
println(sum(2, 3)) // 5
// Single parameter - implicit 'it'
val square: (Int) -> Int = { it * it }
println(square(5)) // 25
// No parameters
val greeting: () -> String = { "Hello" }
println(greeting()) // Hello
// Multiple statements
val process: (Int) -> Int = { value ->
val doubled = value * 2
val squared = doubled * doubled
squared // Last expression is return value
}
7.2 Function Types
// Function type notation
val operation: (Int, Int) -> Int = { a, b -> a + b }
// Nullable function type
val nullableOp: ((Int, Int) -> Int)? = null
// Function type with receiver
val stringBuilder: StringBuilder.() -> Unit = {
append("Hello")
append(" World")
}
// Higher-order function
fun calculate(a: Int, b: Int, op: (Int, Int) -> Int): Int {
return op(a, b)
}
println(calculate(5, 3, { a, b -> a + b })) // 8
println(calculate(5, 3) { a, b -> a * b }) // 15 (trailing lambda)
7.3 Trailing Lambda Syntax
// When last parameter is function, move outside parentheses
val numbers = listOf(1, 2, 3, 4, 5)
// Standard syntax
numbers.filter({ it % 2 == 0 })
// Trailing lambda
numbers.filter { it % 2 == 0 }
// Only lambda parameter - omit parentheses
numbers.forEach { println(it) }
// Multiple parameters with trailing lambda
numbers.fold(0) { acc, n -> acc + n }
7.4 Function References
// Top-level function reference
fun isEven(n: Int): Boolean = n % 2 == 0
val numbers = listOf(1, 2, 3, 4, 5)
val evens = numbers.filter(::isEven)
// Member function reference
class StringUtils {
fun isPalindrome(s: String): Boolean {
return s == s.reversed()
}
}
val utils = StringUtils()
val words = listOf("level", "hello", "radar")
val palindromes = words.filter(utils::isPalindrome)
// Constructor reference
data class Person(val name: String, val age: Int)
val names = listOf("Alice", "Bob")
val people = names.map(::Person) // Partial application not supported
7.5 Inline Functions
// Inline function - lambda code inlined at call site
inline fun <T> measureTime(block: () -> T): T {
val start = System.currentTimeMillis()
val result = block()
val end = System.currentTimeMillis()
println("Took ${end - start}ms")
return result
}
// Usage - no lambda object created
val result = measureTime {
Thread.sleep(100)
42
}
// noinline - Don't inline specific lambda
inline fun complexOperation(
inline operation: () -> Unit,
noinline callback: () -> Unit
) {
operation()
storeCallback(callback) // Can store noinline lambda
}
// crossinline - Lambda can't use non-local returns
inline fun runAsync(crossinline block: () -> Unit) {
thread {
block() // Would fail without crossinline if block has return
}
}
7.6 Returning Functions
// Function that returns function
fun makeMultiplier(factor: Int): (Int) -> Int {
return { value -> value * factor }
}
val double = makeMultiplier(2)
val triple = makeMultiplier(3)
println(double(5)) // 10
println(triple(5)) // 15
// Closure captures variables
fun makeCounter(): () -> Int {
var count = 0
return { ++count }
}
val counter = makeCounter()
println(counter()) // 1
println(counter()) // 2
println(counter()) // 3
7.7 Common Higher-Order Function Patterns
// Retry pattern
inline fun <T> retry(
times: Int = 3,
delay: Long = 1000,
block: () -> T
): T {
repeat(times - 1) {
try {
return block()
} catch (e: Exception) {
Thread.sleep(delay)
}
}
return block() // Last attempt throws if fails
}
// Resource management
inline fun <T : AutoCloseable, R> T.use(block: (T) -> R): R {
try {
return block(this)
} finally {
close()
}
}
// Memoization
fun <P, R> memoize(fn: (P) -> R): (P) -> R {
val cache = mutableMapOf<P, R>()
return { param ->
cache.getOrPut(param) { fn(param) }
}
}
val fibonacci = memoize<Int, Long> { n ->
if (n <= 1) n.toLong()
else fibonacci(n - 1) + fibonacci(n - 2)
}
9. Coroutines Basics
Kotlin coroutines provide asynchronous programming support with sequential-looking code.
8.1 Suspend Functions
import kotlinx.coroutines.*
// Suspend function - can only be called from coroutine or another suspend function
suspend fun fetchUser(id: Long): User {
delay(1000) // Non-blocking delay
return User(id, "Alice")
}
suspend fun fetchPosts(userId: Long): List<Post> {
delay(500)
return listOf(Post(1, "Hello"), Post(2, "World"))
}
// Calling suspend functions
suspend fun loadUserData(id: Long): UserData {
val user = fetchUser(id) // Sequential execution
val posts = fetchPosts(user.id)
return UserData(user, posts)
}
8.2 Coroutine Builders
import kotlinx.coroutines.*
// launch - Fire and forget
fun main() = runBlocking {
launch {
delay(1000)
println("World")
}
println("Hello")
// Output: Hello, World (after 1 second)
}
// async - Return value
fun main() = runBlocking {
val deferred = async {
delay(1000)
"Result"
}
println("Waiting...")
val result = deferred.await() // Suspend until result ready
println(result)
}
// runBlocking - Bridge to coroutine world
fun main() {
runBlocking {
launch {
delay(1000)
println("Coroutine")
}
println("Main")
}
println("Done")
}
8.3 Concurrent Execution
suspend fun loadUserData(id: Long): UserData = coroutineScope {
// Sequential (slow)
val user = fetchUser(id)
val posts = fetchPosts(id)
// Concurrent (fast)
val userDeferred = async { fetchUser(id) }
val postsDeferred = async { fetchPosts(id) }
UserData(
user = userDeferred.await(),
posts = postsDeferred.await()
)
}
// Multiple concurrent operations
suspend fun loadDashboard(): Dashboard = coroutineScope {
val user = async { fetchUser() }
val notifications = async { fetchNotifications() }
val messages = async { fetchMessages() }
val stats = async { fetchStats() }
Dashboard(
user.await(),
notifications.await(),
messages.await(),
stats.await()
)
}
8.4 Coroutine Context and Dispatchers
import kotlinx.coroutines.*
// Dispatchers.Default - CPU-intensive work
launch(Dispatchers.Default) {
val result = performHeavyComputation()
}
// Dispatchers.IO - I/O operations
launch(Dispatchers.IO) {
val data = readFromDatabase()
}
// Dispatchers.Main - UI operations (Android/Desktop)
launch(Dispatchers.Main) {
updateUI(data)
}
// Switch contexts
suspend fun fetchAndDisplay() {
val data = withContext(Dispatchers.IO) {
fetchFromNetwork()
}
withContext(Dispatchers.Main) {
displayData(data)
}
}
8.5 Structured Concurrency
// coroutineScope - Creates scope, suspends until all children complete
suspend fun processAll(items: List<Item>): List<Result> = coroutineScope {
items.map { item ->
async { processItem(item) }
}.awaitAll() // Waits for all to complete
}
// supervisorScope - Children failures don't cancel siblings
suspend fun fetchMultiple(): List<Data?> = supervisorScope {
val results = listOf(
async { fetchFromSource1() },
async { fetchFromSource2() }, // If this fails...
async { fetchFromSource3() } // ...this still runs
)
results.map {
try { it.await() }
catch (e: Exception) { null }
}
}
// Cancellation
val job = launch {
repeat(1000) { i ->
println("Job: $i")
delay(500)
}
}
delay(2000)
job.cancel() // Cancel the job
job.join() // Wait for cancellation to complete
8.6 Exception Handling in Coroutines
// try-catch in coroutines
suspend fun safeOperation(): Result<Data> {
return try {
val data = fetchData()
Result.Success(data)
} catch (e: Exception) {
Result.Error(e)
}
}
// CoroutineExceptionHandler
val handler = CoroutineExceptionHandler { _, exception ->
println("Caught $exception")
}
val scope = CoroutineScope(Dispatchers.Default + handler)
scope.launch {
throw Exception("Failed")
}
// supervisorScope with exception handling
suspend fun robustFetch(): List<Data?> = supervisorScope {
val sources = listOf(source1, source2, source3)
sources.map { source ->
async {
try {
source.fetch()
} catch (e: Exception) {
logger.error("Failed to fetch from $source", e)
null
}
}
}.awaitAll()
}
8.7 Common Coroutine Patterns
// Timeout
suspend fun fetchWithTimeout(id: Long): User? {
return try {
withTimeout(5000) { // 5 second timeout
fetchUser(id)
}
} catch (e: TimeoutCancellationException) {
null
}
}
// Retry with delay
suspend fun fetchWithRetry(maxAttempts: Int = 3): Data {
repeat(maxAttempts - 1) { attempt ->
try {
return fetchData()
} catch (e: Exception) {
delay(1000 * (attempt + 1)) // Exponential backoff
}
}
return fetchData() // Last attempt
}
// Parallel processing with limit
suspend fun processInParallel(
items: List<Item>,
concurrency: Int = 10
): List<Result> = coroutineScope {
items.chunked(concurrency).flatMap { chunk ->
chunk.map { item ->
async { processItem(item) }
}.awaitAll()
}
}
10. Kotlin Idioms
Idiomatic Kotlin patterns that make code more concise and expressive.
9.1 String Templates
val name = "Alice"
val age = 30
// Simple interpolation
val greeting = "Hello, $name!"
// Expressions in templates
val info = "Name: $name, Age: $age, Adult: ${age >= 18}"
// Multi-line strings
val json = """
{
"name": "$name",
"age": $age,
"address": {
"city": "New York"
}
}
""".trimIndent()
// Raw strings with custom margin
val sql = """
|SELECT *
|FROM users
|WHERE name = '$name'
|AND age > $age
""".trimMargin()
9.2 Destructuring
// Data classes
data class User(val id: Long, val name: String, val email: String)
val user = User(1, "Alice", "alice@example.com")
val (id, name, email) = user
// Pairs and Triples
val pair = "key" to "value"
val (key, value) = pair
// In loops
val map = mapOf("a" to 1, "b" to 2, "c" to 3)
for ((key, value) in map) {
println("$key -> $value")
}
// Component functions for custom types
class Range(val start: Int, val end: Int) {
operator fun component1() = start
operator fun component2() = end
}
val range = Range(1, 10)
val (start, end) = range
9.3 Smart Casts
// After type check, automatic cast
fun process(value: Any) {
if (value is String) {
// value is automatically String here
println(value.length)
}
}
// Smart cast with when
fun describe(obj: Any): String = when (obj) {
is String -> "String of length ${obj.length}"
is Int -> "Integer: $obj"
is List<*> -> "List of size ${obj.size}"
else -> "Unknown type"
}
// Smart cast after null check
fun printLength(str: String?) {
if (str != null) {
// str is automatically String (non-null)
println(str.length)
}
}
// Smart cast with require/check
fun process(value: Any) {
require(value is String) { "Must be String" }
// value is String here
println(value.uppercase())
}
9.4 Named Arguments and Default Parameters
// Default parameters
fun createUser(
name: String,
age: Int = 18,
email: String? = null,
isActive: Boolean = true
) = User(name, age, email, isActive)
// Call with defaults
val user1 = createUser("Alice")
val user2 = createUser("Bob", age = 25)
val user3 = createUser("Charlie", email = "charlie@example.com", age = 30)
// Named arguments improve readability
val rect = Rectangle(
width = 100.0,
height = 50.0,
color = Color.RED,
borderWidth = 2.0
)
// Reordering with named arguments
fun formatDate(
day: Int,
month: Int,
year: Int,
separator: String = "-"
): String = "$year$separator$month$separator$day"
val date = formatDate(year = 2024, month = 3, day = 15)
9.5 Single Expression Functions
// Function with expression body
fun square(x: Int): Int = x * x
// Type inference
fun double(x: Int) = x * 2
// With when expression
fun sign(x: Int) = when {
x > 0 -> "positive"
x < 0 -> "negative"
else -> "zero"
}
// With if expression
fun max(a: Int, b: Int) = if (a > b) a else b
// Chained calls
fun processName(name: String) = name
.trim()
.lowercase()
.replaceFirstChar { it.uppercase() }
9.6 Operator Overloading
// Arithmetic operators
data class Point(val x: Int, val y: Int) {
operator fun plus(other: Point) = Point(x + other.x, y + other.y)
operator fun minus(other: Point) = Point(x - other.x, y - other.y)
operator fun times(scale: Int) = Point(x * scale, y * scale)
}
val p1 = Point(1, 2)
val p2 = Point(3, 4)
val p3 = p1 + p2 // Point(4, 6)
val p4 = p1 * 2 // Point(2, 4)
// Comparison operators
data class Version(val major: Int, val minor: Int, val patch: Int) : Comparable<Version> {
override fun compareTo(other: Version): Int {
if (major != other.major) return m
…(truncated)