🚀 Kotlin Introduction
Kotlin is a modern, statically-typed programming language that runs on the Java Virtual Machine (JVM) and can be compiled to JavaScript or native code. Developed by JetBrains and officially supported by Google for Android development, Kotlin combines object-oriented and functional programming features in a concise, safe, and interoperable language.
Modern Language Design
Kotlin was designed from the ground up to address common programming pitfalls while maintaining full interoperability with Java. Its syntax is clean and expressive, reducing boilerplate code significantly compared to Java. The language includes modern features like type inference, extension functions, and smart casts that make development more productive and enjoyable.
JVM Interoperability
One of Kotlin's greatest strengths is its seamless interoperability with Java. You can call Java code from Kotlin and vice versa without any wrappers or special adapters. This allows gradual migration of Java projects to Kotlin and enables developers to leverage existing Java libraries and frameworks. The interoperability extends to the build tools, testing frameworks, and deployment processes familiar to Java developers.
Multi-Platform Capabilities
Kotlin is not limited to the JVM - it can compile to JavaScript for web development and to native code for iOS, macOS, Windows, and Linux applications through Kotlin/Native. Kotlin Multiplatform allows sharing business logic across Android, iOS, web, and desktop applications, significantly reducing development time and ensuring consistency across platforms.
Safety Features
Kotlin eliminates entire categories of errors through its design. The most notable feature is null safety, which distinguishes nullable and non-nullable types at the type system level. This prevents null pointer exceptions, one of the most common runtime errors in Java. Other safety features include checked exceptions (optional), immutable collections by default, and explicit type conversions.
Conciseness and Expressiveness
Kotlin reduces boilerplate code dramatically. Features like data classes, type inference, default parameters, and string templates make code more concise while remaining readable. A single line of Kotlin can often express what takes multiple lines in Java. This conciseness leads to fewer bugs, as there's less code to write and maintain.
Corporate Adoption
Since Google announced official support for Kotlin on Android in 2017, adoption has skyrocketed. Major companies like Google, Amazon, Netflix, Uber, Trello, and Pinterest use Kotlin in production. The language is also gaining traction in backend development with frameworks like Ktor and Spring Boot supporting Kotlin natively.
Learning Curve and Community
Kotlin has a gentle learning curve for Java developers, with most concepts being familiar but improved. The language documentation is excellent, and the community is growing rapidly with active support from JetBrains and Google. Kotlin's pragmatic design philosophy focuses on solving real-world problems effectively.
// Kotlin Introduction Example
fun main() {
println("Kotlin Advanced Track Loaded ⚡")
// Basic syntax examples
val name = "CodeOrbitPro" // Immutable variable
var counter = 0 // Mutable variable
// String template
println("Welcome to $name's Kotlin Track!")
// Function call
val result = calculateSum(10, 20)
println("Sum: $result")
// Null safety example
val nullableString: String? = null
val length = nullableString?.length ?: 0 // Safe call with Elvis operator
println("Length: $length")
// Collection operations
val numbers = listOf(1, 2, 3, 4, 5)
val squares = numbers.map { it * it }
println("Squares: $squares")
}
// Function definition
fun calculateSum(a: Int, b: Int): Int {
return a + b
}
// Data class (replaces Java POJOs)
data class Person(val name: String, val age: Int)
// Extension function
fun String.isPalindrome(): Boolean {
return this == this.reversed()
}
// Coroutine example (simplified)
suspend fun fetchData(): String {
// Simulate network call
kotlinx.coroutines.delay(1000)
return "Data loaded"
}
✨ Kotlin Basics
Kotlin Basics encompass the fundamental building blocks of the language that every developer must master. These include variable declarations, basic types, functions, control flow structures, and the revolutionary null safety system. Kotlin's approach to these basics eliminates many common programming errors while maintaining expressiveness and readability.
Type System and Inference
Kotlin is a statically-typed language with sophisticated type inference. The compiler can deduce types from context, reducing verbosity while maintaining type safety. Kotlin distinguishes between nullable and non-nullable types at compile time, preventing null pointer exceptions. The type system includes all the expected primitive types (handled as objects) and introduces special types like Unit (similar to void) and Nothing (for functions that never return).
Immutable by Default
Kotlin promotes immutability through its `val` keyword for read-only variables. This functional programming principle makes code more predictable and thread-safe. Mutable variables require explicit declaration with `var`. This design choice encourages developers to think about state changes and often leads to more robust, maintainable code with fewer side effects.
Function Declarations
Functions in Kotlin are declared with the `fun` keyword and support default parameters, named arguments, and single-expression syntax. Kotlin functions can be top-level (outside classes), member functions (inside classes), local functions (inside other functions), or extension functions (adding functionality to existing types). This flexibility allows for clean, organized code structures.
String Templates and Raw Strings
Kotlin's string interpolation is more powerful than Java's, allowing arbitrary expressions within `${}`. Triple-quoted strings support multi-line text without escape sequences, making them ideal for JSON, SQL, or regex patterns. String templates reduce concatenation clutter and improve readability significantly.
Control Flow Enhancements
Kotlin enhances traditional control flow with expression-oriented design. `if`, `when`, and `try` can all return values, eliminating the need for ternary operators. The `when` expression is a powerful replacement for switch statements, supporting arbitrary conditions, type checks, and range matching. These features make control flow more expressive and less error-prone.
Scope Functions
Kotlin provides five scope functions (`let`, `run`, `with`, `apply`, and `also`) that execute code blocks in the context of an object. These functions enable clean, fluent APIs and reduce temporary variable declarations. Each scope function has subtle differences in receiver (`this` vs `it`) and return value (context object vs lambda result) that suit different use cases.
Basic Collections
Kotlin distinguishes between mutable and immutable collections at the interface level. The standard library provides `List`, `Set`, and `Map` in both mutable and immutable variants. Collection operations are rich and functional-oriented, with methods like `filter`, `map`, `reduce`, and `fold` available out of the box.
// Kotlin Basics Examples
fun main() {
// 1. Variables and Types
val immutable = "Cannot be reassigned" // Read-only
var mutable = "Can be changed" // Mutable
mutable = "New value"
// Type inference
val inferredString = "Hello" // Compiler knows it's String
val inferredInt = 42 // Compiler knows it's Int
// Explicit types
val explicitDouble: Double = 3.14
val explicitList: List = listOf("a", "b", "c")
// 2. Basic Types
val boolean: Boolean = true
val char: Char = 'K'
val byte: Byte = 127
val short: Short = 32767
val int: Int = 2147483647
val long: Long = 9223372036854775807L
val float: Float = 3.14f
val double: Double = 3.141592653589793
// 3. Strings and Templates
val name = "Kotlin"
val greeting = "Hello, $name!" // String interpolation
val lengthMessage = "$name has ${name.length} characters"
// Multi-line string
val json = """
{
"name": "$name",
"version": "1.8",
"features": ["null-safety", "coroutines", "dsl"]
}
""".trimIndent()
// 4. Arrays
val array = arrayOf(1, 2, 3, 4, 5)
val typedArray = intArrayOf(1, 2, 3) // Primitive array
val arrayWithNulls = arrayOfNulls(5)
// 5. Type Checks and Casts
val obj: Any = "I'm a string"
// Smart cast after type check
if (obj is String) {
println(obj.uppercase()) // No explicit cast needed
}
// Safe cast returns null if not possible
val number: Int? = obj as? Int
println("Safe cast result: $number")
// 6. Basic Operations
val a = 10
val b = 3
println("a + b = ${a + b}")
println("a - b = ${a - b}")
println("a * b = ${a * b}")
println("a / b = ${a / b}") // Integer division
println("a % b = ${a % b}")
// Range operator
val range = 1..10
val exclusiveRange = 1 until 10
val descendingRange = 10 downTo 1
val stepRange = 1..10 step 2
// 7. Equality
val str1 = "Hello"
val str2 = String("Hello".toCharArray())
println("str1 == str2: ${str1 == str2}") // Structural equality
println("str1 === str2: ${str1 === str2}") // Referential equality
// 8. Basic Control Flow
val score = 85
// If as expression
val grade = if (score >= 90) {
"A"
} else if (score >= 80) {
"B"
} else if (score >= 70) {
"C"
} else {
"F"
}
println("Score: $score, Grade: $grade")
// When expression (powerful switch)
val day = 3
val dayName = when (day) {
1 -> "Monday"
2 -> "Tuesday"
3 -> "Wednesday"
4 -> "Thursday"
5 -> "Friday"
6, 7 -> "Weekend"
in 8..31 -> "Invalid day"
else -> "Unknown"
}
println("Day $day is $dayName")
// Loops
for (i in 1..5) {
print("$i ")
}
println()
for (i in 1 until 5) {
print("$i ")
}
println()
for (i in 5 downTo 1) {
print("$i ")
}
println()
// Iterating with index
val fruits = listOf("Apple", "Banana", "Cherry")
for ((index, fruit) in fruits.withIndex()) {
println("Fruit $index: $fruit")
}
// While loop
var count = 5
while (count > 0) {
print("$count ")
count--
}
println()
// Do-while loop
var input: String
do {
print("Enter 'quit' to exit: ")
input = readLine() ?: ""
} while (input != "quit")
println("Basics complete!")
}
📦 Variables & Types
Variables and Types in Kotlin represent a significant improvement over traditional Java approaches. Kotlin's type system is designed to be both expressive and safe, with features that prevent common programming errors while reducing boilerplate code. The language emphasizes immutability and provides powerful type inference capabilities.
Variable Declarations: val vs var
Kotlin distinguishes between mutable and immutable variables at the declaration level. The `val` keyword creates read-only variables that can be assigned only once (similar to Java's final). The `var` keyword creates mutable variables that can be reassigned. This explicit distinction encourages immutability by default, which leads to safer, more predictable code, especially in concurrent programming scenarios.
Type Inference System
Kotlin's compiler features sophisticated type inference that can determine variable types from context. When you assign a value to a variable, the compiler can usually infer its type, allowing you to omit explicit type declarations. However, you can still specify types explicitly when needed for clarity or when the compiler cannot infer the type. This balance reduces verbosity while maintaining type safety.
Basic Data Types
Kotlin treats all types as objects—there are no primitive types in the traditional sense (though they are optimized at runtime). The basic types include: `Byte`, `Short`, `Int`, `Long` for integers; `Float` and `Double` for floating-point numbers; `Char` for characters; `Boolean` for truth values; and `String` for text. Arrays are represented by the `Array` class, with specialized variants like `IntArray` for performance.
Nullable Types System
One of Kotlin's most important features is its null safety system. By default, types are non-nullable—they cannot hold null values. To allow nullability, you must explicitly add a question mark: `String?`. The compiler tracks nullability and ensures safe handling of nullable types through safe calls (`?.`), the Elvis operator (`?:`), and non-null assertions (`!!`). This system eliminates null pointer exceptions at compile time.
Type Aliases and Smart Casts
Kotlin allows creating aliases for existing types using the `typealias` keyword, improving code readability for complex generic types. The smart cast feature automatically casts variables after type checks, eliminating the need for explicit casting in many cases. After checking `if (obj is String)`, you can use `obj` as a String without casting within that scope.
Properties and Backing Fields
In Kotlin, variables in classes are actually properties with automatically generated getters and setters. You can customize accessors using `get()` and `set()` blocks. Backing fields (accessed via `field`) store the actual value. This unified approach eliminates the Java pattern of private fields with public getters/setters, making code more concise.
Late Initialization and Delegated Properties
For variables that cannot be initialized immediately, Kotlin provides `lateinit` for non-nullable types (with runtime checks) and `by lazy` for lazy initialization. Delegated properties allow custom logic for property access through delegation. The standard library includes delegates for observable properties (`observable`), vetoable changes (`vetoable`), and map storage (`by map`).
// Variables and Types Examples
fun main() {
println("=== Variables and Types ===")
// 1. val vs var
val immutableVal = "I cannot be reassigned"
// immutableVal = "New value" // Compilation error
var mutableVar = "I can be changed"
mutableVar = "New value allowed"
// 2. Type inference examples
val inferredInt = 42 // Int
val inferredDouble = 3.14 // Double
val inferredString = "Hello" // String
val inferredBoolean = true // Boolean
val inferredList = listOf(1, 2, 3) // List
// 3. Explicit type declarations
val explicitLong: Long = 10000000000L
val explicitFloat: Float = 3.14f
val explicitArray: Array = arrayOf(1, 2, 3)
// 4. Nullable types
var nullableString: String? = null // Can be null
var nonNullableString: String = "Hello" // Cannot be null
// nonNullableString = null // Compilation error
// Safe calls
val length = nullableString?.length // Returns null if nullableString is null
println("Length of nullable string: $length")
// Elvis operator (default value)
val safeLength = nullableString?.length ?: 0
println("Safe length: $safeLength")
// Non-null assertion (use with caution!)
nullableString = "Now I have a value"
val assertedLength = nullableString!!.length
println("Asserted length: $assertedLength")
// 5. Type checks and smart casts
fun processValue(value: Any) {
when (value) {
is String -> {
// Smart cast to String
println("String length: ${value.length}")
}
is Int -> {
// Smart cast to Int
println("Int value doubled: ${value * 2}")
}
is List<*> -> {
// Smart cast to List
println("List size: ${value.size}")
}
else -> {
println("Unknown type")
}
}
}
processValue("Kotlin")
processValue(42)
processValue(listOf(1, 2, 3))
// 6. Type aliases
typealias UserId = String
typealias UserMap = Map
val userId: UserId = "user123"
val users: UserMap = mapOf("user123" to "Alice", "user456" to "Bob")
// 7. Arrays
val intArray = intArrayOf(1, 2, 3, 4, 5)
val stringArray = arrayOf("Apple", "Banana", "Cherry")
val mixedArray = arrayOf(1, "Two", 3.0, true)
// Array operations
println("Int array size: ${intArray.size}")
println("First element: ${stringArray[0]}")
println("Last element: ${stringArray[stringArray.lastIndex]}")
// 8. Type conversions
val intValue: Int = 42
val longValue: Long = intValue.toLong() // Explicit conversion needed
val doubleValue: Double = intValue.toDouble()
val stringValue: String = intValue.toString()
// 9. Properties with custom accessors
class Rectangle(val width: Int, val height: Int) {
// Computed property
val area: Int
get() = width * height
// Property with backing field
var scale: Double = 1.0
set(value) {
if (value > 0) {
field = value
} else {
println("Scale must be positive")
}
}
// Lazy property
val diagonal: Double by lazy {
Math.sqrt((width * width + height * height).toDouble())
}
}
val rect = Rectangle(10, 20)
println("Rectangle area: ${rect.area}")
println("Rectangle diagonal: ${rect.diagonal}")
rect.scale = 2.0
println("Scale set to: ${rect.scale}")
// 10. Late initialization
class Service {
lateinit var data: String
fun initialize() {
data = "Initialized data"
}
fun process() {
// Check if initialized
if (::data.isInitialized) {
println("Processing: $data")
} else {
println("Data not initialized")
}
}
}
val service = Service()
service.process()
service.initialize()
service.process()
// 11. Delegated properties
class User {
var name: String by Delegates.observable("") { prop, old, new ->
println("$old -> $new")
}
var age: Int by Delegates.vetoable(0) { prop, old, new ->
new >= 0 // Only allow positive ages
}
}
val user = User()
user.name = "Alice"
user.name = "Bob"
user.age = 25
user.age = -5 // Won't change (vetoed)
println("User age: ${user.age}")
// 12. Any, Unit, Nothing
val anyValue: Any = "Can be anything"
val unitValue: Unit = println("Returns Unit") // Similar to void
fun neverReturns(): Nothing {
throw RuntimeException("This function never returns normally")
}
// 13. Type parameters and generics
class Box(val value: T) {
fun getValue(): T = value
}
val intBox = Box(42)
val stringBox = Box("Hello")
println("Box values: ${intBox.getValue()}, ${stringBox.getValue()}")
println("\nVariables and Types demonstration complete!")
}
🏹 Functions in Kotlin
Functions in Kotlin are first-class citizens with extensive features that go beyond traditional Java methods. They can be declared at the top level, as member functions, as local functions, or as extension functions. Kotlin's function design emphasizes expressiveness, safety, and interoperability while reducing boilerplate code significantly.
Function Declaration Syntax
Kotlin functions are declared with the `fun` keyword followed by the function name, parameters in parentheses, an optional return type, and a body. Parameters use Pascal notation (name: type) and are separated by commas. The return type is specified after the parameters with a colon. If the function returns nothing meaningful, the return type is `Unit`, which can be omitted.
Default and Named Arguments
Kotlin supports default parameter values, allowing functions to be called with fewer arguments than defined. This eliminates the need for overloaded functions in many cases. Named arguments enable calling functions with explicit parameter names, improving readability and allowing arguments to be passed in any order when combined with default values.
Single-Expression Functions
When a function consists of a single expression, Kotlin allows an abbreviated syntax where the return type can be inferred and the curly braces replaced with `=`. This makes simple functions extremely concise. For example, `fun square(x: Int) = x * x` is equivalent to a full function declaration but much cleaner.
Extension Functions
One of Kotlin's most powerful features is extension functions, which allow adding new functionality to existing classes without modifying their source code or using inheritance. Extension functions are resolved statically (they don't actually modify the class) but provide a natural, fluent API. They're particularly useful for enhancing library classes or creating DSLs.
Infix Functions
Functions marked with the `infix` keyword can be called using infix notation (without dots and parentheses for single parameters). This enables creating domain-specific languages and operators that read naturally. Infix functions must be member functions or extension functions with a single parameter.
Higher-Order Functions and Lambdas
Kotlin treats functions as values that can be passed as arguments, returned from other functions, or assigned to variables. Higher-order functions take functions as parameters or return functions. Lambda expressions provide a concise way to define anonymous functions, with several syntactic sugars like trailing lambdas and implicit `it` parameter for single-parameter lambdas.
Local Functions and Tail Recursion
Kotlin supports local functions—functions defined inside other functions. These can access variables from the enclosing scope, reducing duplication and improving encapsulation. Kotlin also supports tail-recursive functions with the `tailrec` modifier, which the compiler optimizes to iterative loops, preventing stack overflow for recursive algorithms.
// Functions in Kotlin Examples
fun main() {
println("=== Functions in Kotlin ===")
// 1. Basic function
fun greet(name: String): String {
return "Hello, $name!"
}
println(greet("Kotlin Developer"))
// 2. Single-expression function
fun square(x: Int) = x * x
fun isEven(n: Int) = n % 2 == 0
println("Square of 5: ${square(5)}")
println("Is 6 even? ${isEven(6)}")
// 3. Default arguments
fun createMessage(
text: String,
prefix: String = "Info",
suffix: String = "End of message",
times: Int = 1
): String {
return "$prefix: ${text.repeat(times)} ($suffix)"
}
println(createMessage("Hello")) // Uses all defaults
println(createMessage("Hello", "Warning")) // Custom prefix
println(createMessage("Hello", times = 3)) // Named argument
// 4. Named arguments
fun formatUser(
firstName: String,
lastName: String,
age: Int,
email: String = "",
phone: String = ""
) {
println("User: $firstName $lastName, Age: $age")
if (email.isNotEmpty()) println("Email: $email")
if (phone.isNotEmpty()) println("Phone: $phone")
}
formatUser("John", "Doe", 30)
formatUser(
firstName = "Jane",
lastName = "Smith",
age = 25,
email = "jane@example.com"
)
// Can change order with named arguments
formatUser(
age = 35,
lastName = "Brown",
firstName = "Bob",
phone = "555-1234"
)
// 5. Extension functions
fun String.addExclamation() = "$this!"
fun Int.isPositive() = this > 0
fun List.product(): Int = this.fold(1) { acc, i -> acc * i }
println("Hello".addExclamation())
println("Is 5 positive? ${5.isPositive()}")
println("Product of [1, 2, 3, 4]: ${listOf(1, 2, 3, 4).product()}")
// 6. Infix functions
infix fun Int.times(str: String) = str.repeat(this)
infix fun String.onto(other: String) = Pair(this, other)
println(3 times "Hi ") // Using infix notation
println("Kotlin" onto "Java")
class Person(val name: String) {
infix fun likes(thing: String) = "$name likes $thing"
}
val alice = Person("Alice")
println(alice likes "programming")
// 7. Operator overloading
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(factor: Int) = Point(x * factor, y * factor)
operator fun component1() = x // For destructuring
operator fun component2() = y
}
val p1 = Point(10, 20)
val p2 = Point(5, 5)
println("p1 + p2 = ${p1 + p2}")
println("p1 * 3 = ${p1 * 3}")
// Destructuring using component operators
val (x, y) = p1
println("Destructured: x=$x, y=$y")
// 8. Local functions
fun processNumbers(vararg numbers: Int) {
// Local function
fun validate(n: Int): Boolean {
return n in 1..100
}
val validNumbers = numbers.filter { validate(it) }
println("Valid numbers: $validNumbers")
}
processNumbers(10, 150, 30, -5, 75)
// 9. Tail recursive functions
tailrec fun factorial(n: Int, accumulator: Int = 1): Int {
return if (n <= 1) accumulator else factorial(n - 1, n * accumulator)
}
println("Factorial of 5: ${factorial(5)}")
// 10. Higher-order functions
fun calculate(x: Int, y: Int, operation: (Int, Int) -> Int): Int {
return operation(x, y)
}
val sum = calculate(10, 5) { a, b -> a + b }
val product = calculate(10, 5) { a, b -> a * b }
println("Sum: $sum, Product: $product")
// Function returning function
fun multiplier(factor: Int): (Int) -> Int {
return { number -> number * factor }
}
val double = multiplier(2)
val triple = multiplier(3)
println("Double 5: ${double(5)}")
println("Triple 5: ${triple(5)}")
// 11. Lambda expressions
val numbers = listOf(1, 2, 3, 4, 5)
// Traditional lambda
val squares = numbers.map { n -> n * n }
// Using it for single parameter
val evens = numbers.filter { it % 2 == 0 }
// Multi-line lambda
val processed = numbers.map {
println("Processing $it")
it * 2 + 1
}
println("Squares: $squares")
println("Evens: $evens")
println("Processed: $processed")
// 12. Function references
fun isPrime(n: Int): Boolean {
if (n <= 1) return false
for (i in 2..n/2) {
if (n % i == 0) return false
}
return true
}
val primes = (1..20).filter(::isPrime)
println("Primes 1-20: $primes")
// 13. Inline functions
inline fun measureTime(block: () -> Unit): Long {
val start = System.currentTimeMillis()
block()
return System.currentTimeMillis() - start
}
val time = measureTime {
// This code block is inlined at compile time
var sum = 0
for (i in 1..1000000) {
sum += i
}
}
println("Computation took ${time}ms")
// 14. Varargs
fun sumAll(vararg numbers: Int): Int {
return numbers.sum()
}
println("Sum: ${sumAll(1, 2, 3, 4, 5)}")
// Spread operator
val arr = intArrayOf(10, 20, 30)
println("Sum from array: ${sumAll(*arr)}")
// 15. Generic functions
fun printList(list: List) {
list.forEach { println(it) }
}
fun > maxOf(a: T, b: T): T {
return if (a > b) a else b
}
println("Max of 10 and 20: ${maxOf(10, 20)}")
println("Max of 'a' and 'b': ${maxOf("a", "b")}")
println("\nFunctions demonstration complete!")
}
🎯 Control Flow
Control Flow in Kotlin represents a significant evolution from traditional C-style languages, with expressions replacing statements in many cases. Kotlin's control flow structures are designed to be both powerful and safe, eliminating common sources of errors while providing concise syntax. The language embraces expression-oriented programming where control structures return values.
If as an Expression
In Kotlin, `if` can be used as an expression that returns a value. This eliminates the need for the ternary operator (`?:`) found in other languages. The last expression in each branch becomes the return value. When used as an expression, the `else` branch is mandatory. This design makes code more readable and reduces the chance of missing else cases that can cause null pointer exceptions.
When Expression: Supercharged Switch
The `when` expression is Kotlin's replacement for switch statements, but far more powerful. It can match against constants, ranges, types, or even arbitrary conditions. `when` can be used with or without an argument, and like `if`, it returns a value when used as an expression. The compiler checks for exhaustiveness when used with sealed classes or enums, ensuring all cases are handled.
For Loops and Iteration
Kotlin's `for` loop iterates through anything that provides an iterator, including ranges, arrays, collections, and custom types. The syntax is clean: `for (item in collection)`. Ranges are created with `..` (inclusive) or `until` (exclusive). You can iterate with indices using `withIndex()` or over maps with destructuring. The loop supports `break` and `continue` with labels for nested loops.
While and Do-While Loops
Kotlin supports traditional `while` and `do-while` loops with the same semantics as other languages. The `while` loop checks the condition before each iteration, while `do-while` checks after, guaranteeing at least one execution. These loops are statements (don't return values) and are useful when the number of iterations isn't known in advance.
Returns, Jumps, and Labels
Kotlin has three jump expressions: `return` (returns from nearest enclosing function), `break` (terminates nearest enclosing loop), and `continue` (proceeds to next iteration). All can be used with labels to control which enclosing function or loop is affected. Labels are particularly useful in nested loops or lambda expressions where you need to break from an outer scope.
Exception Handling
Kotlin's exception handling uses `try`, `catch`, and `finally` blocks, but unlike Java, exceptions are unchecked (don't need to be declared). The `try` block can be an expression, returning either the try body value or the catch body value. Kotlin also provides the `throw` expression, which has type `Nothing` (a special type indicating the function never returns normally).
Scope Functions for Control
While not traditional control flow, Kotlin's scope functions (`let`, `run`, `with`, `apply`, `also`) provide alternative ways to control execution context. They're particularly useful for null checks, initialization, and creating fluent APIs. Each has different semantics for the receiver (`this` vs `it`) and return value (context object vs lambda result), making them suitable for different control patterns.
// Control Flow Examples
fun main() {
println("=== Control Flow in Kotlin ===")
// 1. If expression (not statement)
val a = 10
val b = 20
// Traditional if-else
val max1 = if (a > b) a else b
println("Max of $a and $b is $max1")
// Multi-line if expression (last expression is returned)
val description = if (a > b) {
println("a is greater")
"a is greater"
} else if (a < b) {
println("b is greater")
"b is greater"
} else {
println("a and b are equal")
"a and b are equal"
}
println("Description: $description")
// 2. When expression (replaces switch)
val day = 3
// Basic when
val dayName = when (day) {
1 -> "Monday"
2 -> "Tuesday"
3 -> "Wednesday"
4 -> "Thursday"
5 -> "Friday"
6, 7 -> "Weekend"
else -> "Invalid day"
}
println("Day $day is $dayName")
// When with ranges
val score = 85
val grade = when (score) {
in 90..100 -> "A"
in 80..89 -> "B"
in 70..79 -> "C"
in 60..69 -> "D"
else -> "F"
}
println("Score $score gets grade $grade")
// When with type checks and smart casts
fun describe(obj: Any): String = when (obj) {
is String -> "String with length ${obj.length}"
is Int -> "Integer: $obj"
is List<*> -> "List with ${obj.size} elements"
else -> "Unknown type"
}
println(describe("Hello"))
println(describe(42))
println(describe(listOf(1, 2, 3)))
// When without argument (replaces if-else chains)
val hour = 14
val timeOfDay = when {
hour < 12 -> "Morning"
hour < 17 -> "Afternoon"
hour < 21 -> "Evening"
else -> "Night"
}
println("At $hour:00 it's $timeOfDay")
// 3. For loops
// Range iteration
println("Counting up:")
for (i in 1..5) {
print("$i ")
}
println()
println("Counting down:")
for (i in 5 downTo 1) {
print("$i ")
}
println()
println("With step:")
for (i in 1..10 step 2) {
print("$i ")
}
println()
println("Exclusive range:")
for (i in 1 until 5) {
print("$i ")
}
println()
// Collection iteration
val fruits = listOf("Apple", "Banana", "Cherry", "Date")
println("Fruits:")
for (fruit in fruits) {
print("$fruit ")
}
println()
// Iteration with index
println("Fruits with index:")
for ((index, fruit) in fruits.withIndex()) {
println(" $index: $fruit")
}
// Iteration with indices property
println("Using indices:")
for (i in fruits.indices) {
println(" $i: ${fruits[i]}")
}
// Map iteration
val map = mapOf("A" to 1, "B" to 2, "C" to 3)
println("Map entries:")
for ((key, value) in map) {
println(" $key -> $value")
}
// String iteration
val text = "Kotlin"
println("Characters in '$text':")
for (ch in text) {
print("$ch ")
}
println()
// 4. While and do-while loops
println("While loop:")
var count = 5
while (count > 0) {
print("$count ")
count--
}
println()
println("Do-while loop (always executes at least once):")
var input: String
do {
print("Enter 'exit' to quit: ")
input = readLine() ?: ""
println("You entered: $input")
} while (input != "exit")
// 5. Break and continue
println("Break example:")
for (i in 1..10) {
if (i == 5) {
println("Breaking at 5")
break
}
print("$i ")
}
println()
println("Continue example:")
for (i in 1..10) {
if (i % 2 == 0) {
continue // Skip even numbers
}
print("$i ")
}
println()
// 6. Labeled breaks and continues
println("Labeled break (outer loop):")
outer@ for (i in 1..3) {
inner@ for (j in 1..3) {
if (i == 2 && j == 2) {
break@outer // Break outer loop
}
println("i=$i, j=$j")
}
}
println("Labeled continue:")
outer@ for (i in 1..3) {
for (j in 1..3) {
if (j == 2) {
continue@outer // Continue outer loop
}
println("i=$i, j=$j")
}
}
// 7. Exception handling
println("\nException handling:")
// Try as expression
val numberStr = "123"
val parsedNumber = try {
numberStr.toInt()
} catch (e: NumberFormatException) {
println("Could not parse '$numberStr' as number")
null
}
println("Parsed number: $parsedNumber")
// Multiple catch blocks
fun riskyOperation(value: String): Int {
return try {
val num = value.toInt()
if (num < 0) throw IllegalArgumentException("Negative number")
100 / num // Could throw ArithmeticException
} catch (e: NumberFormatException) {
println("Invalid number format: ${e.message}")
0
} catch (e: ArithmeticException) {
println("Arithmetic error: ${e.message}")
-1
} catch (e: IllegalArgumentException) {
println("Illegal argument: ${e.message}")
-2
} finally {
println("Finally block always executes")
}
}
println("Result 1: ${riskyOperation("10")}")
println("Result 2: ${riskyOperation("0")}")
println("Result 3: ${riskyOperation("abc")}")
println("Result 4: ${riskyOperation("-5")}")
// Throw expression
fun requirePositive(value: Int): Int {
return if (value > 0) {
value
} else {
throw IllegalArgumentException("Value must be positive: $value")
}
}
try {
println("Positive check 1: ${requirePositive(10)}")
println("Positive check 2: ${requirePositive(-5)}") // Will throw
} catch (e: IllegalArgumentException) {
println("Caught: ${e.message}")
}
// 8. Scope functions for control flow
println("\nScope functions for control flow:")
val nullableString: String? = "Hello"
// let for null checks and transformations
nullableString?.let {
println("String is not null: $it")
println("Length: ${it.length}")
}
// run for object configuration and computing result
val result = "Hello".run {
println("Processing: $this")
length * 2
}
println("Run result: $result")
// with for calling multiple methods on same object
val list = mutableListOf()
with(list) {
add("First")
add("Second")
add("Third")
println("List has ${size} items: $this")
}
// apply for object initialization
val person = Person("").apply {
name = "Alice"
age = 30
println("Initialized: $name, $age")
}
// also for side effects
val numbersList = mutableListOf(1, 2, 3)
numbersList.also {
println("Before adding: $it")
it.add(4)
println("After adding: $it")
}
// 9. TakeIf and takeUnless
val inputValue = "42"
val parsed = inputValue.takeIf { it.all { char -> char.isDigit() } }?.toInt()
println("Parsed if all digits: $parsed")
val filtered = inputValue.takeUnless { it.isEmpty() }
println("Take unless empty: $filtered")
println("\nControl flow demonstration complete!")
}
// Helper class
class Person(var name: String) {
var age: Int = 0
}
🛡️ Null Safety
Null Safety is one of Kotlin's most celebrated features, designed to eliminate the billion-dollar mistake of null pointer exceptions. Kotlin's type system distinguishes between references that can hold null (nullable references) and those that cannot (non-nullable references). This distinction is made at compile time, allowing the compiler to detect potential null pointer exceptions before the code even runs.
Nullable vs Non-Nullable Types
In Kotlin, types are non-nullable by default. To declare a variable that can hold null, you must explicitly add a question mark after the type: `String?`. The compiler tracks these nullability annotations throughout your code. This explicit nullability forces developers to think about null cases upfront and handle them appropriately, rather than discovering them at runtime.
Safe Call Operator (?.)
The safe call operator `?.` allows you to safely access properties or call methods on nullable objects. If the object is null, the expression returns null instead of throwing a NullPointerException. Safe calls can be chained: `user?.address?.city?.uppercase()`. This eliminates the need for nested null checks and makes code more readable while maintaining safety.
Elvis Operator (?:)
The Elvis operator `?:` provides a default value when the expression on its left is null. It's named for its resemblance to Elvis Presley's hairstyle. The operator evaluates the left expression; if it's not null, that's the result. If it's null, it evaluates and returns the right expression. This is cleaner than using if-else checks for null values.
Non-Null Assertion (!!)
The non-null assertion operator `!!` converts any value to a non-null type and throws a NullPointerException if the value is null. This operator should be used sparingly, only when you're absolutely certain a value cannot be null at that point. Overuse of `!!` defeats Kotlin's null safety guarantees and can lead to runtime crashes.
Safe Casts (as?)
The safe cast operator `as?` attempts to cast a value to a specified type and returns null if the cast is not possible. This is safer than the regular cast operator `as`, which throws a ClassCastException on failure. Safe casts are particularly useful when dealing with types from Java interop or when the type is not guaranteed at compile time.
Platform Types and Java Interoperability
When calling Java code from Kotlin, the nullability of return types is unknown (platform types). Kotlin represents these as types with an exclamation mark: `String!`. You can treat them as nullable or non-nullable, but the compiler can't guarantee safety. Annotations like `@Nullable` and `@NotNull` help Kotlin understand Java code's nullability contracts.
Collection Nullability
Kotlin's collections also support null safety. You can have collections of nullable types (`List`) or nullable collections (`List?`), or both. The standard library provides functions like `filterNotNull()` to safely handle collections containing nulls. This comprehensive approach ensures null safety throughout your data structures.
// Null Safety Examples
fun main() {
println("=== Null Safety in Kotlin ===")
// 1. Nullable vs non-nullable types
var nonNullable: String = "Hello" // Cannot be null
// nonNullable = null // Compilation error
var nullable: String? = "Hello" // Can be null
nullable = null // This is allowed
// 2. Safe call operator (?.)
val length1 = nullable?.length // Returns null if nullable is null
println("Length with safe call: $length1")
// Chain safe calls
class Address(val city: String?)
class User(val name: String, val address: Address?)
val user1: User? = User("Alice", Address("New York"))
val user2: User? = User("Bob", null)
val user3: User? = null
val city1 = user1?.address?.city // "New York"
val city2 = user2?.address?.city // null
val city3 = user3?.address?.city // null
println("Cities: $city1, $city2, $city3")
// Safe call with method calls
val uppercaseCity = user1?.address?.city?.uppercase()
println("Uppercase city: $uppercaseCity")
// 3. Elvis operator (?:)
val name1: String? = "Alice"
val name2: String? = null
val displayName1 = name1 ?: "Anonymous"
val displayName2 = name2 ?: "Anonymous"
println("Display names: $displayName1, $displayName2")
// Elvis with throw
fun requireNotNull(value: String?): String {
return value ?: throw IllegalArgumentException("Value cannot be null")
}
try {
println("Required: ${requireNotNull("Valid")}")
println("Required: ${requireNotNull(null)}") // Throws
} catch (e: IllegalArgumentException) {
println("Caught: ${e.message}")
}
// Elvis with return
fun processUser(user: User?) {
val userName = user?.name ?: return // Return early if null
println("Processing user: $userName")
}
processUser(User("Charlie", null))
processUser(null)
// 4. Non-null assertion (!!) - Use with caution!
val definitelyNotNull: String? = "I'm definitely not null"
val forcedLength = definitelyNotNull!!.length // We're sure it's not null
println("Forced length: $forcedLength")
// Dangerous example (will crash at runtime)
val risky: String? = null
// val crash = risky!!.length // Throws NullPointerException
// 5. Safe casts (as?)
val obj: Any = "I'm a string"
val asString: String? = obj as? String // Safe cast, returns String
val asInt: Int? = obj as? Int // Safe cast, returns null
println("Safe cast to String: $asString")
println("Safe cast to Int: $asInt")
// 6. let, also, run, with, apply with null safety
val nullableValue: String? = "Hello"
// let executes block only if not null
nullableValue?.let {
println("Value is not null: $it")
println("Length: ${it.length}")
}
// also for side effects
nullableValue?.also {
println("Also: $it")
}
// 7. Collections with nulls
val listWithNulls: List = listOf(1, 2, null, 4, null, 6)
println("List with nulls: $listWithNulls")
// Filter out nulls
val withoutNulls: List = listWithNulls.filterNotNull()
println("Without nulls: $withoutNulls")
// Map with null handling
val mapped = listWithNulls.map { it?.times(2) }
println("Mapped (keeping nulls): $mapped")
val mappedNotNull = listWithNulls.mapNotNull { it?.times(2) }
println("Mapped (filtering nulls): $mappedNotNull")
// 8. Platform types from Java
// When calling Java code, Kotlin doesn't know nullability
// Java: public String getUserName() { ... }
// Kotlin sees this as: fun getUserName(): String!
// You can assign to nullable or non-nullable
val fromJava1: String = JavaInterop.getUserName() // Assume not null
val fromJava2: String? = JavaInterop.getUserName() // Handle possible null
println("From Java (non-null): $fromJava1")
println("From Java (nullable): $fromJava2")
// 9. Late initialization (lateinit)
class Service {
lateinit var data: String // Will be initialized later
fun initialize() {
data = "Initialized"
}
fun process() {
// Check if initialized
if (::data.isInitialized) {
println("Processing: $data")
} else {
println("Data not initialized yet")
}
}
}
val service = Service()
service.process()
service.initialize()
service.process()
// 10. Delegates for null safety
import kotlin.properties.Delegates
class UserProfile {
// Not null, must be initialized in constructor or init block
var username: String by Delegates.notNull()
// Observable property
var email: String by Delegates.observable("") { prop, old, new ->
println("Email changed: $old -> $new")
}
// Vetoable - can reject changes
var age: Int by Delegates.vetoable(0) { _, old, new ->
if (new >= 0) {
true // Accept change
} else {
println("Age cannot be negative: $new")
false // Reject change
}
}
}
val profile = UserProfile()
profile.username = "alice123" // Must be set before use
profile.email = "alice@example.com"
profile.email = "alice.new@example.com"
profile.age = 25
profile.age = -5 // Will be rejected
println("Profile age: ${profile.age}")
// 11. Type parameter nullability
fun printHashCode(t: T) { // T is non-nullable
println("Hash code: ${t.hashCode()}")
}
// printHashCode(null) // Compilation error
printHashCode("Hello")
fun printHashCodeNullable(t: T) { // T can be nullable
println("Hash code nullable: ${t?.hashCode()}")
}
printHashCodeNullable("Hello")
printHashCodeNullable(null)
// 12. Null safety in when expressions
fun describeValue(value: Any?): String {
return when (value) {
null -> "It's null"
is String -> "String of length ${value.length}"
is Int -> "Integer: $value"
else -> "Unknown type"
}
}
println(describeValue(null))
println(describeValue("Kotlin"))
println(describeValue(42))
// 13. TakeIf and takeUnless
val input = "123"
val number = input.takeIf { it.all { char -> char.isDigit() } }?.toInt()
println("Number if all digits: $number")
val notEmpty = input.takeUnless { it.isEmpty() }
println("Take unless empty: $notEmpty")
// 14. Custom null safety extensions
fun String?.orEmpty(): String = this ?: ""
fun String?.toIntOrZero(): Int = this?.toIntOrNull() ?: 0
val maybeString: String? = null
println("Or empty: '${maybeString.orEmpty()}'")
println("To int or zero: ${maybeString.toIntOrZero()}")
val maybeNumber: String? = "42"
println("To int or zero (valid): ${maybeNumber.toIntOrZero()}")
println("\nNull safety demonstration complete!")
}
// Simulated Java interop
object JavaInterop {
fun getUserName(): String {
return "JavaUser"
}
}
🏗️ OOP in Kotlin
Object-Oriented Programming (OOP) in Kotlin builds upon Java's OOP foundation while fixing its pain points and introducing modern features. Kotlin supports all four OOP pillars—encapsulation, inheritance, polymorphism, and abstraction—but implements them more cleanly and concisely. The language eliminates boilerplate code while enhancing expressiveness and safety.
Classes and Constructors
Kotlin classes are declared with the `class` keyword. The primary constructor is part of the class header and can declare properties directly. Secondary constructors use the `constructor` keyword and must delegate to the primary constructor. Properties declared in the primary constructor can be `val` (read-only) or `var` (mutable). Kotlin automatically generates getters and setters for properties.
Properties and Fields
In Kotlin, fields don't exist as a first-class concept—instead, we have properties with automatically generated backing fields when needed. You can customize property accessors with `get()` and `set()` blocks. The `field` identifier refers to the backing field in custom accessors. This unified approach eliminates the Java pattern of private fields with public getters/setters.
Inheritance and Open Classes
By default, Kotlin classes are `final`—they cannot be inherited. To allow inheritance, mark the class with the `open` keyword. Methods must also be marked `open` to be overridable. The `override` modifier is required when overriding methods (unlike Java's optional `@Override` annotation). This explicit design prevents accidental inheritance and fragile base class problems.
Interfaces with Default Implementations
Kotlin interfaces can contain method implementations (similar to Java 8 default methods) and properties. Interfaces support multiple inheritance, and classes can implement multiple interfaces. Interface properties can be abstract or provide accessor implementations. Kotlin's interfaces are more powerful than Java's, allowing rich API design without abstract classes in many cases.
Data Classes for Value Objects
Data classes (`data class`) automatically generate `equals()`, `hashCode()`, `toString()`, `copy()`, and component functions (for destructuring). They're ideal for value objects that primarily hold data. Properties declared in the primary constructor are used in the generated functions. Data classes reduce boilerplate dramatically while providing value semantics.
Sealed Classes for Restricted Hierarchies
Sealed classes restrict which classes can inherit from them. All subclasses must be declared in the same file. This makes sealed classes perfect for representing restricted class hierarchies, like algebraic data types. The compiler knows all possible subclasses, enabling exhaustive checks in `when` expressions without needing an `else` branch.
Objects and Companion Objects
Kotlin uses `object` declarations for singletons—classes that have only one instance. Companion objects are objects declared inside a class with the `companion` keyword, serving as static members in Java. They can have names, implement interfaces, and extend classes. Object expressions create anonymous objects (similar to Java anonymous classes but more powerful).
// OOP in Kotlin Examples
fun main() {
println("=== Object-Oriented Programming in Kotlin ===")
// 1. Basic class with primary constructor
class Person(val name: String, var age: Int) {
// Property with custom getter
val isAdult: Boolean
get() = age >= 18
// Method
fun greet() {
println("Hello, I'm $name and I'm $age years old")
}
}
val alice = Person("Alice", 25)
alice.greet()
println("Is Alice adult? ${alice.isAdult}")
// 2. Properties with backing fields
class Rectangle(width: Int, height: Int) {
var width = width
set(value) {
if (value > 0) {
field = value
} else {
println("Width must be positive")
}
}
var height = height
set(value) {
if (value > 0) {
field = value
}
}
// Computed property (no backing field)
val area: Int
get() = width * height
}
val rect = Rectangle(10, 20)
println("Rectangle area: ${rect.area}")
rect.width = -5 // Will print warning
println("Width after invalid set: ${rect.width}")
// 3. Inheritance
open class Animal(val name: String) { // open allows inheritance
open fun makeSound() { // open allows overriding
println("$name makes a sound")
}
fun eat() {
println("$name is eating")
}
}
class Dog(name: String, val breed: String) : Animal(name) {
override fun makeSound() {
println("$name (a $breed) barks!")
}
fun fetch() {
println("$name is fetching")
}
}
val dog = Dog("Rex", "Golden Retriever")
dog.makeSound()
dog.eat()
dog.fetch()
// 4. Abstract classes
abstract class Shape {
abstract fun area(): Double
abstract fun perimeter(): Double
fun description() {
println("Area: ${area()}, Perimeter: ${perimeter()}")
}
}
class Circle(val radius: Double) : Shape() {
override fun area() = Math.PI * radius * radius
override fun perimeter() = 2 * Math.PI * radius
}
val circle = Circle(5.0)
circle.description()
// 5. Interfaces
interface Drawable {
fun draw()
val color: String // Abstract property
fun description() { // Default implementation
println("Drawable with color $color")
}
}
interface Clickable {
fun click()
fun show() = println("Showing clickable element") // Default implementation
}
class Button(override val color: String) : Drawable, Clickable {
override fun draw() {
println("Drawing button with color $color")
}
override fun click() {
println("Button clicked!")
}
// Can override default implementation
override fun show() {
super.show() // Call super from specific interface
println("Button specific show")
}
}
val button = Button("Blue")
button.draw()
button.click()
button.description()
button.show()
// 6. Data classes
data class User(val id: Int, val username: String, val email: String)
val user1 = User(1, "alice", "alice@example.com")
val user2 = User(1, "alice", "alice@example.com")
val user3 = User(2, "bob", "bob@example.com")
println("user1: $user1")
println("user1 == user2: ${user1 == user2}") // true - structural equality
println("user1 === user2: ${user1 === user2}") // false - different references
// Copy with modification
val user1Updated = user1.copy(email = "alice.new@example.com")
println("Updated user: $user1Updated")
// Destructuring
val (id, username, email) = user1
println("Destructured: id=$id, username=$username, email=$email")
// Component functions (auto-generated)
println("Components: ${user1.component1()}, ${user1.component2()}, ${user1.component3()}")
// 7. Sealed classes (restricted hierarchies)
sealed class Result {
data class Success(val data: String) : Result()
data class Error(val message: String, val code: Int) : Result()
object Loading : Result()
}
fun handleResult(result: Result) {
// Exhaustive when (no else needed - compiler knows all cases)
when (result) {
is Result.Success -> println("Success: ${result.data}")
is Result.Error -> println("Error ${result.code}: ${result.message}")
Result.Loading -> println("Loading...")
}
}
handleResult(Result.Success("Data loaded"))
handleResult(Result.Error("Not found", 404))
handleResult(Result.Loading)
// 8. Objects (singletons)
object Logger {
private var level = "INFO"
fun log(message: String) {
println("[$level] $message")
}
fun setLevel(newLevel: String) {
level = newLevel
}
}
Logger.log("Application started")
Logger.setLevel("DEBUG")
Logger.log("Debug message")
// 9. Companion objects (static members)
class DatabaseClient private constructor(val url: String) {
companion object Factory {
private val instances = mutableMapOf()
fun getInstance(url: String): DatabaseClient {
return instances.getOrPut(url) { DatabaseClient(url) }
}
const val DEFAULT_URL = "localhost:5432"
}
fun connect() {
println("Connected to $url")
}
}
val db1 = DatabaseClient.getInstance("localhost:5432")
val db2 = DatabaseClient.getInstance("localhost:5432") // Same instance
val db3 = DatabaseClient.getInstance("remote:5432") // Different instance
println("db1 === db2: ${db1 === db2}") // true
println("db1 === db3: ${db1 === db3}") // false
db1.connect()
println("Default URL: ${DatabaseClient.DEFAULT_URL}")
// 10. Nested and inner classes
class Outer {
private val outerValue = "Outer"
// Nested class (no access to outer instance)
class Nested {
fun getValue() = "From nested"
}
// Inner class (has access to outer instance)
inner class Inner {
fun getValue() = "From inner accessing $outerValue"
}
}
val nested = Outer.Nested()
println("Nested: ${nested.getValue()}")
val outer = Outer()
val inner = outer.Inner()
println("Inner: ${inner.getValue()}")
// 11. Object expressions (anonymous objects)
interface EventListener {
fun onEvent(event: String)
}
val listener = object : EventListener {
override fun onEvent(event: String) {
println("Event received: $event")
}
// Can have additional members
val creationTime = System.currentTimeMillis()
}
listener.onEvent("click")
println("Listener created at: ${listener.creationTime}")
// 12. Visibility modifiers
class VisibilityExample {
public val publicProperty = "Everyone can see me"
internal val internalProperty = "Same module only"
protected val protectedProperty = "Subclasses can see me"
private val privateProperty = "Only this class can see me"
fun test() {
println(publicProperty)
println(internalProperty)
println(protectedProperty)
println(privateProperty)
}
}
class Subclass : VisibilityExample() {
fun testSubclass() {
println(publicProperty)
println(internalProperty)
println(protectedProperty) // OK - protected is visible
// println(privateProperty) // Error - private not visible
}
}
// 13. Delegation
interface Base {
fun print()
}
class BaseImpl(val x: Int) : Base {
override fun print() {
println("BaseImpl: $x")
}
}
// Delegation using "by" keyword
class Derived(b: Base) : Base by b {
// Can override if needed
override fun print() {
println("Derived before")
// b.print() // Called automatically by delegation
println("Derived after")
}
}
val base = BaseImpl(10)
Derived(base).print()
// 14. Property delegation
class DelegateExample {
var delegatedProperty: String by Delegate()
}
class Delegate {
private var backingField = "Default"
operator fun getValue(thisRef: Any?, property: kotlin.reflect.KProperty<*>): String {
println("Getting ${property.name}")
return backingField
}
operator fun setValue(thisRef: Any?, property: kotlin.reflect.KProperty<*>, value: String) {
println("Setting ${property.name} to $value")
backingField = value
}
}
val example = DelegateExample()
println("Initial value: ${example.delegatedProperty}")
example.delegatedProperty = "New Value"
println("Updated value: ${example.delegatedProperty}")
// 15. Enum classes
enum class Color(val rgb: Int) {
RED(0xFF0000),
GREEN(0x00FF00),
BLUE(0x0000FF),
YELLOW(0xFFFF00);
fun containsRed() = (rgb and 0xFF0000) != 0
}
println("Colors:")
for (color in Color.values()) {
println(" $color (${color.rgb.toString(16)}) contains red? ${color.containsRed()}")
}
val selected = Color.valueOf("RED")
println("Selected color: $selected, ordinal: ${selected.ordinal}")
println("\nOOP demonstration complete!")
}
🏛️ Classes & Objects
Classes and Objects in Kotlin represent the core building blocks of object-oriented programming, with significant improvements over traditional Java approaches. Kotlin's class system is designed to be concise, safe, and expressive while eliminating common sources of boilerplate code. The language provides several specialized class types (data, sealed, enum, inline) for specific use cases.
Class Declarations and Constructors
Kotlin classes are declared with the `class` keyword. The primary constructor is part of the class header and can have parameters that automatically become properties if declared with `val` or `var`. Secondary constructors use the `constructor` keyword and must delegate to the primary constructor using `this()`. Initializer blocks (`init`) run during object creation and can contain validation logic or complex initialization.
Properties and Custom Accessors
Properties in Kotlin combine fields and accessors into a single concept. By default, properties have automatically generated getters and setters. You can customize accessors using `get()` and `set()` blocks. The `field` identifier refers to the backing field in custom accessors. Computed properties have no backing field and calculate their value on each access.
Visibility Modifiers
Kotlin provides four visibility modifiers: `public` (default), `internal` (visible within the same module), `protected` (visible in subclasses), and `private` (visible within the containing scope). Unlike Java, Kotlin doesn't have package-private visibility. The modifiers can be applied to classes, functions, properties, and constructors to control encapsulation.
Companion Objects for Static Members
Kotlin doesn't have static members. Instead, it uses companion objects—objects declared inside a class with the `companion` keyword. Companion object members can be called using the class name as a qualifier. Companion objects can have names, implement interfaces, and be referenced as objects. They're more flexible than Java's static members while serving the same purpose.
Object Declarations for Singletons
Object declarations create singletons—classes with a single instance. The object is created lazily when accessed for the first time. Objects can extend classes and implement interfaces. They're useful for utility classes, factory patterns, or when you need a single instance throughout the application. Object expressions create anonymous objects on the fly.
Nested and Inner Classes
Kotlin distinguishes between nested classes (declared with `class`) and inner classes (declared with `inner class`). Nested classes don't have access to the outer class instance, similar to Java's static nested classes. Inner classes have access to the outer class instance and can reference it using `this@Outer`. This explicit distinction prevents confusion.
Inline Classes for Type Safety
Inline classes (value classes) wrap a single value while having no runtime overhead—they're represented as the underlying value at runtime. They provide type safety without performance cost. Inline classes are declared with `value class` and must have a single `val` property in the primary constructor. They're ideal for creating type-safe abstractions over primitive types.
// Classes and Objects Examples
fun main() {
println("=== Classes and Objects in Kotlin ===")
// 1. Basic class with primary constructor
class Person constructor(firstName: String) { // constructor keyword optional
val name: String
init { // Initializer block
name = firstName.capitalize()
println("Person initialized with name: $name")
}
}
val person1 = Person("alice")
println("Person name: ${person1.name}")
// 2. Concise primary constructor with properties
class User(val username: String, var age: Int) {
// Properties automatically created from constructor parameters
fun describe() {
println("User: $username, Age: $age")
}
}
val user = User("john_doe", 30)
user.describe()
user.age = 31 // Setter available because age is var
user.describe()
// 3. Secondary constructors
class Car(val make: String, val model: String) {
var year: Int = 2023
// Secondary constructor must delegate to primary
constructor(make: String, model: String, year: Int) : this(make, model) {
this.year = year
println("Car created with year: $year")
}
// Another secondary constructor
constructor(make: String) : this(make, "Unknown") {
println("Car created with unknown model")
}
fun info() {
println("$year $make $model")
}
}
val car1 = Car("Toyota", "Camry")
val car2 = Car("Honda", "Accord", 2020)
val car3 = Car("Ford")
car1.info()
car2.info()
car3.info()
// 4. Properties with custom accessors
class Rectangle(width: Int, height: Int) {
var width = width
set(value) {
if (value > 0) {
field = value // field refers to backing field
} else {
println("Width must be positive")
}
}
var height = height
get() {
println("Getting height: $field")
return field
}
set(value) {
println("Setting height from $field to $value")
field = value
}
// Computed property (no backing field)
val area: Int
get() = width * height
// Property with private setter
var scale: Double = 1.0
private set
fun doubleScale() {
scale *= 2
}
}
val rect = Rectangle(10, 20)
println("Rectangle area: ${rect.area}")
rect.width = 15
rect.height = 25
println("New area: ${rect.area}")
rect.doubleScale()
println("Scale: ${rect.scale}")
// rect.scale = 3.0 // Error - setter is private
// 5. Lazy properties
class HeavyComputation {
val expensiveResult: String by lazy {
println("Computing expensive result...")
// Simulate expensive computation
Thread.sleep(1000)
"Computed Result"
}
}
val computation = HeavyComputation()
println("Before accessing lazy property")
println("Lazy property: ${computation.expensiveResult}")
println("Lazy property (cached): ${computation.expensiveResult}")
// 6. Late initialization
class DatabaseService {
lateinit var connection: String
fun connect() {
connection = "Connected to database"
}
fun query() {
if (::connection.isInitialized) {
println("Querying using: $connection")
} else {
println("Not connected yet")
}
}
}
val service = DatabaseService()
service.query()
service.connect()
service.query()
// 7. Companion objects (static members)
class MathUtils {
companion object {
const val PI = 3.14159
fun circleArea(radius: Double): Double {
return PI * radius * radius
}
// Factory method
fun create(): MathUtils = MathUtils()
}
fun instanceMethod() {
println("I'm an instance method")
}
}
// Access companion members using class name
println("PI: ${MathUtils.PI}")
println("Circle area: ${MathUtils.circleArea(5.0)}")
val utils = MathUtils.create()
utils.instanceMethod()
// Named companion object
class Logger {
companion object Factory {
fun createLogger(): Logger = Logger()
}
}
val logger = Logger.Factory.createLogger()
// 8. Object declarations (singletons)
object ApplicationConfig {
private const val VERSION = "1.0.0"
var debugMode = false
fun printConfig() {
println("Version: $VERSION, Debug: $debugMode")
}
}
ApplicationConfig.debugMode = true
ApplicationConfig.printConfig()
// 9. Object expressions (anonymous objects)
interface ClickListener {
fun onClick()
}
val buttonListener = object : ClickListener {
override fun onClick() {
println("Button clicked!")
}
// Can have additional properties/methods
val clickCount = 0
}
buttonListener.onClick()
println("Click count: ${buttonListener.clickCount}")
// Object expression without supertype
val adHoc = object {
val x = 10
val y = 20
fun sum() = x + y
}
println("Ad hoc object sum: ${adHoc.sum()}")
// 10. Nested and inner classes
class OuterClass(val outerValue: String) {
// Nested class (no access to outer instance)
class NestedClass {
fun nestedMethod() = "From nested class"
}
// Inner class (has access to outer instance)
inner class InnerClass {
fun innerMethod() = "From inner class accessing $outerValue"
fun getOuterReference(): OuterClass = this@OuterClass
}
}
val outer = OuterClass("Outer Value")
val nested = OuterClass.NestedClass()
val inner = outer.InnerClass()
println("Nested: ${nested.nestedMethod()}")
println("Inner: ${inner.innerMethod()}")
println("Inner gets outer: ${inner.getOuterReference().outerValue}")
// 11. Data classes
data class Product(val id: Int, val name: String, val price: Double) {
// Can have additional members
fun formattedPrice() = "$${"%.2f".format(price)}"
}
val product1 = Product(1, "Laptop", 999.99)
val product2 = Product(1, "Laptop", 999.99)
val product3 = product1.copy(id = 2, price = 1099.99)
println("Product1: $product1")
println("Product1 formatted: ${product1.formattedPrice()}")
println("product1 == product2: ${product1 == product2}")
println("product1 === product2: ${product1 === product2}")
println("Product3 (copy modified): $product3")
// Destructuring
val (id, name, price) = product1
println("Destructured: id=$id, name=$name, price=$price")
// 12. Enum classes
enum class Direction(val degrees: Int) {
NORTH(0),
EAST(90),
SOUTH(180),
WEST(270);
fun opposite(): Direction {
return when (this) {
NORTH -> SOUTH
EAST -> WEST
SOUTH -> NORTH
WEST -> EAST
}
}
}
val current = Direction.EAST
println("Current direction: $current (${current.degrees}°)")
println("Opposite direction: ${current.opposite()}")
// Iterating enum values
println("All directions:")
for (direction in Direction.values()) {
println(" $direction - ${direction.degrees}°")
}
// 13. Sealed classes
sealed class NetworkResponse {
data class Success(val data: String, val code: Int) : NetworkResponse()
data class Error(val message: String, val code: Int) : NetworkResponse()
object Loading : NetworkResponse()
fun isSuccess(): Boolean = this is Success
}
fun handleResponse(response: NetworkResponse) {
when (response) {
is NetworkResponse.Success -> {
println("Success: ${response.code} - ${response.data}")
}
is NetworkResponse.Error -> {
println("Error: ${response.code} - ${response.message}")
}
NetworkResponse.Loading -> {
println("Loading...")
}
// No else needed - compiler knows all cases are covered
}
}
handleResponse(NetworkResponse.Success("Data", 200))
handleResponse(NetworkResponse.Error("Not Found", 404))
handleResponse(NetworkResponse.Loading)
// 14. Value classes (inline classes) - Kotlin 1.5+
@JvmInline
value class Password(val value: String) {
init {
require(value.length >= 8) { "Password must be at least 8 characters" }
}
fun strength(): String {
return when (value.length) {
in 8..11 -> "Weak"
in 12..15 -> "Medium"
else -> "Strong"
}
}
}
@JvmInline
value class Email(val value: String) {
init {
require(value.contains("@")) { "Invalid email format" }
}
}
fun authenticate(email: Email, password: Password) {
println("Authenticating ${email.value} with password strength: ${password.strength()}")
}
val email = Email("user@example.com")
val password = Password("securePass123")
authenticate(email, password)
// 15. Abstract classes
abstract class Shape {
abstract fun area(): Double
abstract fun perimeter(): Double
fun description() {
println("Shape: area=${area()}, perimeter=${perimeter()}")
}
}
class Square(val side: Double) : Shape() {
override fun area() = side * side
override fun perimeter() = 4 * side
}
val square = Square(5.0)
square.description()
println("\nClasses and Objects demonstration complete!")
}
🔗 Inheritance
Inheritance in Kotlin follows the object-oriented principle of creating new classes based on existing ones, but with important safety improvements over traditional languages. Kotlin classes are final by default—they cannot be inherited unless explicitly marked `open`. This "closed by default" approach prevents fragile base class problems and encourages composition over inheritance when appropriate.
Open Classes and Methods
To allow a class to be inherited, mark it with the `open` keyword. Similarly, methods must be marked `open` to be overridable. The `override` modifier is mandatory when overriding methods (unlike Java's optional `@Override`). This explicit design makes inheritance intentions clear and prevents accidental overrides or extensions that could break base class contracts.
Primary Constructor Inheritance
When a derived class has a primary constructor, the base class must be initialized in that primary constructor using parameters passed to it. If the derived class has no primary constructor, each secondary constructor must initialize the base type using the `super` keyword. This ensures proper initialization order and prevents uninitialized base class state.
Overriding Properties
Properties can be overridden similar to methods. A property declared with `open` in the base class can be overridden in a derived class. The overriding property must have a compatible type. You can override a `val` with a `var`, but not vice versa (since `val` has only a getter, while `var` has both getter and setter). Property overrides can also customize accessors.
Calling Superclass Implementations
Within overridden members, you can call the superclass implementation using the `super` keyword. For multiple inheritance from interfaces, you can qualify `super` with the interface name in angle brackets: `super.method()`. This is necessary when a class implements multiple interfaces with default method implementations having the same signature.
Abstract Classes and Members
Abstract classes cannot be instantiated and may contain abstract members (methods or properties without implementation). Abstract members must be implemented by concrete subclasses. Unlike `open` members, abstract members don't need an implementation in the abstract class. A class with abstract members must itself be declared `abstract`.
Inheritance and Initialization Order
Kotlin follows a specific initialization order: 1) base class constructor, 2) base class init blocks, 3) base class property initializers, 4) derived class constructor, 5) derived class init blocks, 6) derived class property initializers. This order ensures that base class members are initialized before derived class code accesses them.
Delegation as Alternative to Inheritance
Kotlin supports class delegation using the `by` keyword, which allows implementing interfaces by delegating to another object. This is a powerful alternative to inheritance for code reuse, especially when multiple inheritance would be needed. Delegation follows the composition-over-inheritance principle and avoids deep class hierarchies.
// Inheritance Examples
fun main() {
println("=== Inheritance in Kotlin ===")
// 1. Open classes and methods
open class Animal(val name: String) { // Class must be open to inherit
open fun makeSound() { // Method must be open to override
println("$name makes a sound")
}
// Not open - cannot be overridden
fun eat() {
println("$name is eating")
}
open val sound: String = "Generic animal sound"
}
class Dog(name: String, val breed: String) : Animal(name) {
// Must use override keyword
override fun makeSound() {
println("$name the $breed barks!")
}
// Can call superclass implementation
fun makeGenericSound() {
super.makeSound()
}
// Overriding property
override val sound: String = "Woof!"
}
val dog = Dog("Rex", "Golden Retriever")
dog.makeSound() // "Rex the Golden Retriever barks!"
dog.makeGenericSound() // "Rex makes a sound"
dog.eat() // "Rex is eating" (inherited, not overridden)
println("Dog sound: ${dog.sound}")
// 2. Primary constructor inheritance
open class Vehicle(val wheels: Int, val color: String) {
init {
println("Vehicle with $wheels wheels created")
}
open fun drive() {
println("Driving $color vehicle with $wheels wheels")
}
}
class Car(color: String) : Vehicle(4, color) {
// Car constructor passes arguments to Vehicle constructor
init {
println("Car created")
}
override fun drive() {
println("Driving $color car")
super.drive() // Call parent implementation
}
}
val car = Car("Red")
car.drive()
// 3. Secondary constructors with inheritance
open class Person(val name: String) {
var age: Int = 0
constructor(name: String, age: Int) : this(name) {
this.age = age
println("Person $name created with age $age")
}
open fun introduce() {
println("Hi, I'm $name")
}
}
class Employee : Person {
var salary: Double = 0.0
// Secondary constructor calling superclass secondary constructor
constructor(name: String, age: Int, salary: Double) : super(name, age) {
this.salary = salary
println("Employee $name with salary $$salary created")
}
override fun introduce() {
super.introduce()
println("I earn $$salary per year")
}
}
val employee = Employee("Alice", 30, 75000.0)
employee.introduce()
// 4. Property overriding
open class Shape {
open val vertexCount: Int = 0
open val name: String = "Shape"
open fun description() {
println("This is a $name with $vertexCount vertices")
}
}
class Rectangle : Shape() {
// Override val with val
override val vertexCount: Int = 4
// Override val with var (allowed)
override var name: String = "Rectangle"
set(value) {
field = value.capitalize()
}
// Custom property not in parent
val area: Double = 0.0 // Would be calculated in real implementation
}
val rect = Rectangle()
rect.name = "square" // Uses custom setter
rect.description()
// 5. Abstract classes
abstract class Graphic {
abstract val x: Int
abstract val y: Int
abstract fun draw()
// Can have non-abstract members
fun move(dx: Int, dy: Int) {
println("Moving from ($x, $y) to (${x + dx}, ${y + dy})")
}
}
class Circle(override val x: Int, override val y: Int, val radius: Int) : Graphic() {
override fun draw() {
println("Drawing circle at ($x, $y) with radius $radius")
}
}
val circle = Circle(10, 20, 5)
circle.draw()
circle.move(5, -5)
// 6. Multiple inheritance with interfaces
interface Clickable {
fun click()
fun show() = println("Showing clickable") // Default implementation
}
interface Draggable {
fun drag()
fun show() = println("Showing draggable") // Default implementation with same name
}
class UIElement : Clickable, Draggable {
override fun click() {
println("UI element clicked")
}
override fun drag() {
println("UI element dragged")
}
// Must override show() because both interfaces have default implementation
override fun show() {
super.show() // Call specific parent implementation
super.show()
println("UI element show")
}
}
val element = UIElement()
element.click()
element.drag()
element.show()
// 7. Inheritance initialization order
open class Base(val name: String) {
init {
println("Base init block: $name")
}
val baseProperty = "Base property".also {
println("Base property initialized: $it")
}
}
class Derived(name: String, val value: Int) : Base(name) {
init {
println("Derived init block: $name, value=$value")
}
val derivedProperty = "Derived property".also {
println("Derived property initialized: $it")
}
}
println("\nInitialization order demonstration:")
val derived = Derived("Test", 42)
/*
Output order:
Base init block: Test
Base property initialized: Base property
Derived init block: Test, value=42
Derived property initialized: Derived property
*/
// 8. Sealed class inheritance
sealed class Expression {
abstract fun evaluate(): Double
}
class Number(val value: Double) : Expression() {
override fun evaluate() = value
}
class Sum(val left: Expression, val right: Expression) : Expression() {
override fun evaluate() = left.evaluate() + right.evaluate()
}
class Product(val left: Expression, val right: Expression) : Expression() {
override fun evaluate() = left.evaluate() * right.evaluate()
}
fun printExpression(expr: Expression) {
when (expr) {
is Number -> println("Number: ${expr.value}")
is Sum -> println("Sum: ${expr.left.evaluate()} + ${expr.right.evaluate()}")
is Product -> println("Product: ${expr.left.evaluate()} * ${expr.right.evaluate()}")
// No else needed - compiler knows all subclasses
}
}
val expression = Sum(Number(5.0), Product(Number(2.0), Number(3.0)))
printExpression(expression)
println("Result: ${expression.evaluate()}")
// 9. Delegation instead of inheritance
interface Printer {
fun printDocument(document: String)
}
class LaserPrinter : Printer {
override fun printDocument(document: String) {
println("Laser printing: $document")
}
}
// Using delegation instead of inheritance
class OfficePrinter(private val printer: Printer) : Printer by printer {
// Can add additional functionality
fun printWithHeader(document: String) {
println("=== Office Document ===")
printDocument(document)
println("=== End of Document ===")
}
// Can override delegated methods
override fun printDocument(document: String) {
println("Logging print job")
printer.printDocument(document)
println("Print job completed")
}
}
val officePrinter = OfficePrinter(LaserPrinter())
officePrinter.printDocument("Report.pdf")
officePrinter.printWithHeader("Memo.txt")
// 10. Final modifier
open class Parent {
// This method is open but can be made final in subclass
open fun canBeOverridden() {
println("Parent implementation")
}
}
class Child : Parent() {
// Override but make it final so further subclasses can't override
final override fun canBeOverridden() {
println("Child implementation (final)")
}
}
class GrandChild : Child() {
// Can't override - method is final in Child
// override fun canBeOverridden() { } // Error
}
// 11. Visibility in inheritance
open class VisibilityBase {
public val publicProp = "Public"
protected val protectedProp = "Protected"
internal val internalProp = "Internal"
private val privateProp = "Private"
protected open fun protectedMethod() {
println("Base protected method")
}
}
class VisibilityDerived : VisibilityBase() {
// Can access protected members
fun testAccess() {
println(publicProp)
println(protectedProp) // OK - protected is accessible in subclass
println(internalProp)
// println(privateProp) // Error - private not accessible
protectedMethod() // OK
}
// Can override protected method
override fun protectedMethod() {
super.protectedMethod()
println("Derived protected method")
}
}
val derivedVis = VisibilityDerived()
derivedVis.testAccess()
// derivedVis.protectedProp // Error - protected not accessible outside
println("\nInheritance demonstration complete!")
}
📜 Interfaces
Interfaces in Kotlin are powerful constructs that define contracts for classes while supporting default implementations, properties, and inheritance. Unlike Java 7 interfaces (which could only have abstract methods), Kotlin interfaces can contain method implementations, properties with accessors, and can inherit from other interfaces. This flexibility makes interfaces central to Kotlin's design, often replacing abstract classes.
Interface Declaration and Implementation
Interfaces are declared using the `interface` keyword and can contain abstract method declarations, method implementations (default methods), abstract properties, and properties with accessor implementations. Classes implement interfaces using the `:` syntax, similar to extending classes. A class can implement multiple interfaces, supporting true multiple inheritance of type (though not state).
Properties in Interfaces
Interfaces can contain abstract property declarations (without initialization) or properties with custom accessors. Since interfaces cannot store state, properties in interfaces cannot have backing fields. Abstract properties must be implemented by implementing classes. Properties with accessors provide default implementations that can be overridden.
Default Methods and Multiple Inheritance
Kotlin interfaces can provide default implementations for methods. When a class implements multiple interfaces with conflicting default method signatures, it must provide its own implementation. The `super.method()` syntax allows calling a specific parent interface's implementation. This resolves the diamond problem that occurs with multiple inheritance.
Functional Interfaces (SAM Interfaces)
Interfaces with a single abstract method (SAM) can be implemented using lambda expressions for conciseness. The `fun` keyword modifier creates functional interfaces explicitly. When a functional interface is expected as a parameter, you can pass a lambda, and Kotlin automatically converts it to an instance of the interface. This is particularly useful for callback patterns.
Interface Inheritance
Interfaces can inherit from other interfaces, forming interface hierarchies. A derived interface can add new members or override members from parent interfaces. When overriding a member with a default implementation, the `override` keyword is required. Interface inheritance allows building complex type hierarchies without the limitations of class inheritance.
Delegated Interface Implementation
Kotlin's `by` keyword allows delegating interface implementation to another object. This is useful for implementing the decorator pattern or when you want to reuse implementation without inheritance. The delegating class can still override some methods while delegating others. This provides flexible composition patterns.
Interface vs Abstract Class
Interfaces cannot store state (no backing fields), while abstract classes can. Interfaces support multiple inheritance, while classes (including abstract) support single inheritance. Abstract classes can have constructors, while interfaces cannot. The choice depends on whether you need state storage, multiple inheritance, or initialization logic.
🗃️ Data Classes
Data Classes in Kotlin are a concise way to create classes that primarily hold data. They automatically generate several standard functions: `equals()`, `hashCode()`, `toString()`, `copy()`, and component functions for destructuring. This eliminates boilerplate code that would otherwise be manually written in Java for value objects, making Kotlin code more readable and less error-prone.
Automatic Function Generation
When you declare a data class, Kotlin automatically generates `equals()` for structural equality comparison, `hashCode()` consistent with equals, `toString()` in a readable format, and `copy()` for creating modified copies. These functions only consider properties declared in the primary constructor, ignoring properties declared in the class body. This ensures predictable behavior.
Copy Function with Named Arguments
The `copy()` function allows creating a new instance with some properties modified. It uses default parameter values equal to the current property values, so you only need to specify properties you want to change. Named arguments make `copy()` calls self-documenting: `person.copy(name = "New Name")`. This is particularly useful for immutable data structures.
Destructuring Declarations
Data classes automatically generate component functions (`component1()`, `component2()`, etc.) corresponding to properties in the primary constructor. This enables destructuring declarations: `val (name, age) = person`. The number of variables in destructuring must match the number of component functions. Destructuring works in loops, lambda parameters, and other contexts.
Requirements and Limitations
Data classes must have a primary constructor with at least one parameter. All primary constructor parameters must be marked as `val` or `var`. Data classes cannot be abstract, open, sealed, or inner. However, they can implement interfaces and extend other classes (including other data classes, though this is discouraged due to equals/hashCode issues).
Inheritance Considerations
While data classes can extend other classes, they have restrictions regarding equality. The generated `equals()` and `hashCode()` methods only consider properties declared in the primary constructor, ignoring inherited properties. This can lead to unexpected behavior if the parent class has properties that should affect equality. Generally, data classes work best as leaf classes in hierarchies.
Standard Data Class Patterns
Data classes are ideal for DTOs (Data Transfer Objects), value objects, result types, event objects, and configuration objects. They pair well with sealed classes for representing algebraic data types. Common patterns include using data classes with `copy()` for immutable updates, with destructuring for pattern matching, and with collections for complex data structures.
Customizing Generated Functions
You can customize generated functions by providing explicit implementations. If you override `toString()`, `equals()`, or `hashCode()`, the compiler won't generate these functions. The `copy()` function cannot be overridden. You can add additional properties and methods in the class body, but they won't be included in the generated functions.
// Data Classes Examples
fun main() {
println("=== Data Classes in Kotlin ===")
// 1. Basic data class
data class Person(val name: String, val age: Int)
val person1 = Person("Alice", 30)
val person2 = Person("Alice", 30)
val person3 = Person("Bob", 25)
// toString() auto-generated
println("person1: $person1") // Person(name=Alice, age=30)
// equals() auto-generated (structural equality)
println("person1 == person2: ${person1 == person2}") // true
println("person1 == person3: ${person1 == person3}") // false
// hashCode() auto-generated
println("person1.hashCode(): ${person1.hashCode()}")
println("person2.hashCode(): ${person2.hashCode()}")
println("Same hash codes: ${person1.hashCode() == person2.hashCode()}")
// 2. Copy function
val person4 = person1.copy() // Exact copy
println("person1 copied: $person4")
val person5 = person1.copy(name = "Alicia") // Copy with modification
println("person1 copied with new name: $person5")
val person6 = person1.copy(age = 31) // Copy with different age
println("person1 copied with new age: $person6")
// 3. Destructuring declarations
val (name, age) = person1 // Destructuring
println("Destructured: name=$name, age=$age")
// Component functions (auto-generated)
println("Component 1: ${person1.component1()}") // name
println("Component 2: ${person1.component1()}") // age
// Destructuring in loops
val people = listOf(person1, person3, person5)
println("\nPeople list:")
for ((personName, personAge) in people) {
println(" $personName is $personAge years old")
}
// 4. Data class with default values
data class Product(
val id: Int,
val name: String,
val price: Double = 0.0,
val inStock: Boolean = true
)
val product1 = Product(1, "Laptop", 999.99)
val product2 = Product(2, "Mouse", inStock = false)
println("\nProducts:")
println("product1: $product1")
println("product2: $product2")
// 5. Data class with additional properties/methods
data class User(
val id: Int,
val username: String,
val email: String
) {
// Additional property (not in primary constructor)
val displayName: String
get() = "@$username"
// Additional method
fun sendMessage(message: String) {
println("Sending '$message' to $email")
}
// Note: displayName and sendMessage are NOT included in
// generated equals(), hashCode(), toString(), copy(), or destructuring
}
val user1 = User(1, "alice", "alice@example.com")
val user2 = User(1, "alice", "alice@example.com")
println("\nUsers:")
println("user1: $user1")
println("user2: $user2")
println("user1 == user2: ${user1 == user2}") // true (ignores displayName)
println("user1.displayName: ${user1.displayName}")
user1.sendMessage("Hello!")
// 6. Nested destructuring
data class Name(val first: String, val last: String)
data class Employee(val id: Int, val name: Name, val department: String)
val employee = Employee(101, Name("John", "Doe"), "Engineering")
// Simple destructuring
val (empId, empName, dept) = employee
println("\nEmployee: id=$empId, name=$empName, department=$dept")
// Nested destructuring
val (id2, Name(firstName, lastName), department2) = employee
println("Nested destructuring: $firstName $lastName works in $department2")
// 7. Data classes in collections
data class Point(val x: Int, val y: Int)
val points = listOf(
Point(0, 0),
Point(1, 1),
Point(2, 4),
Point(3, 9),
Point(4, 16)
)
println("\nPoints:")
points.forEach { println(" $it") }
// Using data class properties in collection operations
val xValues = points.map { it.x }
val yValues = points.map { it.y }
val sumOfX = points.sumOf { it.x }
val maxY = points.maxOf { it.y }
println("X values: $xValues")
println("Y values: $yValues")
println("Sum of X: $sumOfX")
println("Max Y: $maxY")
// Finding duplicates (uses equals())
val duplicatePoints = listOf(Point(1, 1), Point(2, 4), Point(1, 1))
val uniquePoints = duplicatePoints.toSet() // Set uses hashCode() and equals()
println("Unique points: $uniquePoints")
// 8. Copy with nested data classes
data class Address(val street: String, val city: String, val zip: String)
data class Customer(val id: Int, val name: String, val address: Address)
val customer1 = Customer(1, "Alice", Address("123 Main St", "NYC", "10001"))
val customer2 = customer1.copy(
address = customer1.address.copy(city = "Boston", zip = "02101")
)
println("\nCustomers:")
println("Original: $customer1")
println("Modified address: $customer2")
// 9. Data class limitations and inheritance
open class Entity(val id: Int)
data class ProductEntity(
val productName: String,
val price: Double
) : Entity(1) // Extending a class
val prod1 = ProductEntity("Laptop", 999.99)
val prod2 = ProductEntity("Laptop", 999.99)
// Warning: equals() and hashCode() only consider productName and price, not id!
println("prod1 == prod2: ${prod1 == prod2}") // true (ignores inherited id)
println("prod1.id == prod2.id: ${prod1.id == prod2.id}") // true (both 1)
// 10. Data class with custom toString
data class Book(val title: String, val author: String, val year: Int) {
// Override auto-generated toString
override fun toString(): String {
return "\"$title\" by $author ($year)"
}
// Can also override equals and hashCode if needed
// override fun equals(other: Any?): Boolean { ... }
// override fun hashCode(): Int { ... }
// Note: copy() cannot be overridden
}
val book = Book("1984", "George Orwell", 1949)
println("\nBook: $book")
// 11. Data class as return type (pair with sealed class)
sealed class Result
data class Success(val data: String) : Result()
data class Error(val message: String, val code: Int) : Result()
fun processResult(result: Result) {
when (result) {
is Success -> println("Success: ${result.data}")
is Error -> println("Error ${result.code}: ${result.message}")
}
}
processResult(Success("Data loaded"))
processResult(Error("Not found", 404))
// 12. Data class with var properties (mutable)
data class MutablePerson(var name: String, var age: Int)
val mutablePerson = MutablePerson("Charlie", 40)
println("\nMutable person: $mutablePerson")
mutablePerson.name = "Charles"
mutablePerson.age = 41
println("After mutation: $mutablePerson")
// Warning: mutable data classes can have issues with collections
// if modified while in a collection that uses hashCode()
// 13. Component functions beyond destructuring
data class TripleData(val a: String, val b: String, val c: String)
val triple = TripleData("first", "second", "third")
// Component functions available
println("\nTriple components:")
println("component1(): ${triple.component1()}")
println("component2(): ${triple.component2()}")
println("component3(): ${triple.component3()}")
// Using in lambda with destructuring
val triples = listOf(triple, TripleData("x", "y", "z"))
triples.forEach { (a, b, c) ->
println(" $a, $b, $c")
}
// 14. Real-world example: API response modeling
data class ApiResponse(
val success: Boolean,
val data: T?,
val error: String? = null,
val timestamp: Long = System.currentTimeMillis()
)
data class UserResponse(
val id: Int,
val name: String,
val email: String
)
val successResponse = ApiResponse(
success = true,
data = UserResponse(1, "Alice", "alice@example.com")
)
val errorResponse = ApiResponse(
success = false,
data = null,
error = "User not found"
)
println("\nAPI Responses:")
println("Success: $successResponse")
println("Error: $errorResponse")
// Processing response
fun handleResponse(response: ApiResponse) {
if (response.success) {
val user = response.data!!
println("User loaded: ${user.name} (${user.email})")
} else {
println("Error: ${response.error}")
}
}
handleResponse(successResponse)
handleResponse(errorResponse)
println("\nData classes demonstration complete!")
}
⚡ Functional Programming
Functional Programming (FP) in Kotlin is supported as a first-class paradigm alongside object-oriented programming. Kotlin incorporates key FP concepts like higher-order functions, lambdas, immutability, and pure functions while maintaining seamless interoperability with Java. The language's design encourages functional patterns that lead to more expressive, concise, and maintainable code.
Functions as First-Class Citizens
In Kotlin, functions can be assigned to variables, passed as arguments, returned from other functions, and stored in data structures. This enables higher-order functions—functions that take other functions as parameters or return functions. The type system represents function types with arrow notation: `(Int, Int) -> Int` for a function taking two Ints and returning an Int.
Lambda Expressions and Anonymous Functions
Lambda expressions provide a concise way to define anonymous functions. The syntax `{ parameters -> body }` creates a function value. When a lambda has a single parameter, it can be omitted and referred to as `it`. Lambda expressions can capture variables from their enclosing scope. Anonymous functions (`fun(x: Int): Int { return x * x }`) are similar but allow explicit return types and return statements.
Higher-Order Functions and Function Types
Higher-order functions operate on other functions. Common examples include `map`, `filter`, and `reduce` on collections. Kotlin's standard library is rich with higher-order functions for collection processing, scope management, and resource handling. Function types can have receiver types (for extension functions), nullable returns, and generic parameters.
Immutability and Pure Functions
Kotlin encourages immutability through `val` declarations and immutable collections (`List`, `Set`, `Map`). Pure functions—functions whose output depends only on inputs and have no side effects—are easier to reason about and test. While Kotlin doesn't enforce purity, it provides the tools to write pure functions when desired.
Function Composition and Currying
Functions can be composed using the `compose` and `andThen` operations (or custom implementations). Currying transforms a multi-parameter function into a series of single-parameter functions. While Kotlin doesn't have built-in currying, it can be implemented using higher-order functions. These techniques enable powerful functional abstractions.
Recursion and Tail Recursion
Kotlin supports recursion, and when functions are marked with `tailrec`, the compiler optimizes tail-recursive calls into loops, preventing stack overflow. This allows recursive algorithms to be expressed naturally while maintaining performance. Tail recursion requires the recursive call to be the last operation in the function.
Monads and Functional Patterns
Kotlin's standard library includes types that follow monadic patterns: `Result` (for error handling), `Option`-like behavior with nullable types, and sequences for lazy evaluation. While not explicitly called monads, these types enable functional error handling, transformation pipelines, and declarative data processing similar to FP languages.
// Functional Programming Examples
fun main() {
println("=== Functional Programming in Kotlin ===")
// 1. Function types and variables
val add: (Int, Int) -> Int = { a, b -> a + b }
val multiply = { a: Int, b: Int -> a * b }
val square: (Int) -> Int = { it * it }
val greet: () -> String = { "Hello, World!" }
println("add(3, 5): ${add(3, 5)}")
println("multiply(4, 6): ${multiply(4, 6)}")
println("square(7): ${square(7)}")
println("greet(): ${greet()}")
// 2. Higher-order functions
fun calculate(x: Int, y: Int, operation: (Int, Int) -> Int): Int {
return operation(x, y)
}
val result1 = calculate(10, 5) { a, b -> a + b }
val result2 = calculate(10, 5) { a, b -> a - b }
val result3 = calculate(10, 5, multiply)
println("10 + 5 = $result1")
println("10 - 5 = $result2")
println("10 * 5 = $result3")
// 3. Functions returning functions
fun multiplier(factor: Int): (Int) -> Int {
return { number -> number * factor }
}
val double = multiplier(2)
val triple = multiplier(3)
println("double(5): ${double(5)}")
println("triple(5): ${triple(5)}")
// 4. Lambda expressions
val numbers = listOf(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
// Filter even numbers
val evens = numbers.filter { it % 2 == 0 }
println("Evens: $evens")
// Map to squares
val squares = numbers.map { it * it }
println("Squares: $squares")
// Reduce to sum
val sum = numbers.reduce { acc, num -> acc + num }
println("Sum: $sum")
// Fold with initial value
val product = numbers.fold(1) { acc, num -> acc * num }
println("Product: $product")
// 5. It parameter (single parameter lambda)
val strings = listOf("apple", "banana", "cherry", "date")
val uppercase = strings.map { it.uppercase() }
val longStrings = strings.filter { it.length > 5 }
val lengths = strings.map { it.length }
println("Uppercase: $uppercase")
println("Long strings: $longStrings")
println("Lengths: $lengths")
// 6. Anonymous functions
val isEven = fun(n: Int): Boolean {
return n % 2 == 0
}
val cube = fun(n: Int): Int {
return n * n * n
}
println("isEven(4): ${isEven(4)}")
println("cube(3): ${cube(3)}")
// Anonymous function as argument (allows explicit return)
val result = calculate(8, 2, fun(a, b): Int {
return a * a + b * b
})
println("8^2 + 2^2 = $result")
// 7. Function composition
fun
compose(f: (B) -> C, g: (A) -> B): (A) -> C {
return { x -> f(g(x)) }
}
val addTwo = { x: Int -> x + 2 }
val timesThree = { x: Int -> x * 3 }
val addThenMultiply = compose(timesThree, addTwo)
val multiplyThenAdd = { x: Int -> timesThree(addTwo(x)) } // Manual composition
println("addThenMultiply(5): ${addThenMultiply(5)}") // (5 + 2) * 3 = 21
println("multiplyThenAdd(5): ${multiplyThenAdd(5)}") // Same
// 8. Currying
fun addCurried(a: Int): (Int) -> Int {
return { b -> a + b }
}
val addFive = addCurried(5)
println("addFive(3): ${addFive(3)}") // 8
// Generic currying
fun curry(f: (A, B) -> C): (A) -> (B) -> C {
return { a -> { b -> f(a, b) } }
}
val curriedAdd = curry { a: Int, b: Int -> a + b }
val addTen = curriedAdd(10)
println("addTen(7): ${addTen(7)}") // 17
// 9. Partial application
fun power(base: Int, exponent: Int): Int {
return Math.pow(base.toDouble(), exponent.toDouble()).toInt()
}
// Partial application using lambda
val square2 = { x: Int -> power(x, 2) }
val cube2 = { x: Int -> power(x, 3) }
println("square2(4): ${square2(4)}")
println("cube2(3): ${cube2(3)}")
// 10. Immutability
val immutableList = listOf(1, 2, 3) // Read-only list
// immutableList.add(4) // Compilation error
val mutableList = mutableListOf(1, 2, 3) // Mutable list
mutableList.add(4) // OK
// Functional transformation creates new collections
val doubled = immutableList.map { it * 2 } // New list created
println("Original: $immutableList, Doubled: $doubled")
// 11. Pure functions
// Pure function: output depends only on inputs, no side effects
fun pureAdd(a: Int, b: Int): Int {
return a + b
}
// Impure function: has side effect (printing)
fun impureAdd(a: Int, b: Int): Int {
println("Adding $a and $b")
return a + b
}
// Impure function: depends on external state
var counter = 0
fun incrementAndGet(): Int {
return ++counter
}
println("Pure add: ${pureAdd(3, 4)}")
println("Impure add: ${impureAdd(3, 4)}")
println("Increment: ${incrementAndGet()}, ${incrementAndGet()}")
// 12. Recursion and tail recursion
// Regular recursion (risk of stack overflow for large n)
fun factorial(n: Int): Int {
return if (n <= 1) 1 else n * factorial(n - 1)
}
// Tail recursion (optimized to loop by compiler)
tailrec fun factorialTail(n: Int, accumulator: Int = 1): Int {
return if (n <= 1) accumulator else factorialTail(n - 1, n * accumulator)
}
println("factorial(5): ${factorial(5)}")
println("factorialTail(5): ${factorialTail(5)}")
// 13. Function references
fun isPositive(n: Int) = n > 0
fun negate(n: Int) = -n
val numbers2 = listOf(-3, -2, -1, 0, 1, 2, 3)
val positives = numbers2.filter(::isPositive)
val negated = numbers2.map(::negate)
println("Positives: $positives")
println("Negated: $negated")
// Method reference on instance
val str = "Kotlin"
val lengthFunc: (String) -> Int = String::length
println("Length of 'Kotlin': ${lengthFunc(str)}")
// 14. Function pipelines
data class Person(val name: String, val age: Int)
val people = listOf(
Person("Alice", 30),
Person("Bob", 25),
Person("Charlie", 35),
Person("Diana", 28),
Person("Eve", 40)
)
// Functional pipeline
val resultPipeline = people
.filter { it.age >= 30 } // Keep people 30+
.sortedBy { it.name } // Sort by name
.map { it.name.uppercase() } // Convert to uppercase
.joinToString(", ") // Join to string
println("People 30+ (sorted, uppercase): $resultPipeline")
// 15. Lazy evaluation with sequences
val bigList = (1..1_000_000).toList()
// Eager evaluation (processes entire collection)
val eagerResult = bigList
.filter { it % 2 == 0 }
.map { it * it }
.take(10)
.toList()
// Lazy evaluation with Sequence (processes only needed elements)
val lazyResult = bigList.asSequence()
.filter {
println("Filtering $it")
it % 2 == 0
}
.map {
println("Mapping $it")
it * it
}
.take(10)
.toList()
println("First 10 even squares (eager): $eagerResult")
println("First 10 even squares (lazy): $lazyResult")
// 16. Monadic patterns (Result type)
sealed class Result {
data class Success(val value: T) : Result()
data class Error(val message: String) : Result()
}
fun parseNumber(str: String): Result {
return try {
Result.Success(str.toInt())
} catch (e: NumberFormatException) {
Result.Error("Cannot parse '$str' as number")
}
}
fun doubleIfPositive(n: Int): Result {
return if (n > 0) {
Result.Success(n * 2)
} else {
Result.Error("Number must be positive")
}
}
// Function composition with Result
fun parseAndDouble(str: String): Result {
return when (val parsed = parseNumber(str)) {
is Result.Success -> doubleIfPositive(parsed.value)
is Result.Error -> parsed
}
}
println("parseAndDouble(\"10\"): ${parseAndDouble("10")}")
println("parseAndDouble(\"-5\"): ${parseAndDouble("-5")}")
println("parseAndDouble(\"abc\"): ${parseAndDouble("abc")}")
// 17. Functional data structures
// Persistent/immutable data structure pattern
data class FunctionalList(
val head: T,
val tail: FunctionalList? = null
) {
fun add(newHead: T): FunctionalList {
return FunctionalList(newHead, this)
}
fun size(): Int {
return 1 + (tail?.size() ?: 0)
}
fun toList(): List {
val result = mutableListOf()
var current: FunctionalList? = this
while (current != null) {
result.add(current.head)
current = current.tail
}
return result
}
}
val functionalList = FunctionalList(1)
.add(2)
.add(3)
.add(4)
println("Functional list size: ${functionalList.size()}")
println("Functional list as list: ${functionalList.toList()}")
// Original unchanged
val newList = functionalList.add(5)
println("Original: ${functionalList.toList()}")
println("New: ${newList.toList()}")
println("\nFunctional programming demonstration complete!")
}
λ Lambda Expressions
Lambda expressions in Kotlin are concise anonymous functions that can be passed as arguments, stored in variables, or returned from functions. They are a fundamental feature enabling functional programming patterns in Kotlin. Lambdas provide a more readable and expressive alternative to anonymous classes for defining behavior inline, especially when working with collections, event handlers, or callback functions.
Lambda Syntax and Structure
Lambda expressions in Kotlin follow the syntax: `{ parameters -> body }`. Parameters (with optional types) are on the left of the arrow (`->`), and the body containing expressions is on the right. If the lambda has no parameters, the arrow can be omitted. When the inferred return type is not `Unit`, the last expression in the body becomes the return value. Lambdas can span multiple lines, with the last expression determining the return value.
Implicit `it` Parameter
When a lambda has exactly one parameter and the type can be inferred, the parameter can be omitted and referred to by the implicit name `it`. This makes simple lambdas extremely concise: `list.map { it * 2 }` instead of `list.map { x -> x * 2 }`. The `it` convention is widely used in Kotlin but should be avoided when nesting lambdas or when the parameter name would provide clarity.
Trailing Lambdas
When a function's last parameter is a function type, the lambda argument can be placed outside the parentheses—this is called a trailing lambda. If the lambda is the only argument, parentheses can be omitted entirely. This syntax enables DSL-like constructs: `with(list) { add("item") }` instead of `with(list, { add("item") })`. Trailing lambdas make code more readable, especially with scope functions and builders.
Lambda with Receiver
Kotlin supports lambdas with receivers, which have access to members of a receiver object within the lambda body. The type is written as `ReceiverType.() -> ReturnType`. Inside the lambda, `this` refers to the receiver object, and members can be accessed without qualification. This feature powers DSL construction, scope functions (`apply`, `run`), and type-safe builders.
Capturing Variables
Lambdas can capture variables from their enclosing scope. Unlike Java (which requires effectively final variables), Kotlin allows lambdas to modify captured mutable variables. The captured variables are stored with the lambda, extending their lifetime. This enables powerful patterns like accumulators in higher-order functions but requires caution with concurrency.
SAM Conversions for Java Interoperability
When interfacing with Java code that expects a SAM (Single Abstract Method) interface, Kotlin can automatically convert a lambda to an instance of that interface. This works for Java interfaces with exactly one abstract method. The conversion is automatic when passing a lambda where a SAM interface is expected, eliminating the need for anonymous class syntax.
Performance Considerations
Lambdas are objects in Kotlin and incur runtime overhead. However, the `inline` keyword can eliminate this overhead by inlining the lambda's bytecode at the call site. The standard library's higher-order functions like `map`, `filter`, and `let` are inline functions. For performance-critical code with complex lambdas, consider using `inline` functions or function references.
// Lambda Expressions Examples
fun main() {
println("=== Lambda Expressions in Kotlin ===")
// 1. Basic lambda syntax
val add: (Int, Int) -> Int = { a: Int, b: Int -> a + b }
val multiply = { a: Int, b: Int -> a * b }
val greet = { name: String -> "Hello, $name!" }
val noParam = { "I have no parameters" }
println("add(3, 5): ${add(3, 5)}")
println("multiply(4, 6): ${multiply(4, 6)}")
println("greet(\"Alice\"): ${greet("Alice")}")
println("noParam(): ${noParam()}")
// 2. Type inference in lambdas
val inferredAdd = { a: Int, b: Int -> a + b } // Type inferred from parameters
val length: (String) -> Int = { it.length } // Type declared, parameter inferred
println("inferredAdd(2, 3): ${inferredAdd(2, 3)}")
println("length(\"Kotlin\"): ${length("Kotlin")}")
// 3. Lambda as function parameter
fun calculate(x: Int, y: Int, operation: (Int, Int) -> Int): Int {
return operation(x, y)
}
val sum = calculate(10, 5) { a, b -> a + b }
val difference = calculate(10, 5) { a, b -> a - b }
val product = calculate(10, 5) { a, b -> a * b }
println("10 + 5 = $sum")
println("10 - 5 = $difference")
println("10 * 5 = $product")
// 4. Implicit 'it' parameter
val numbers = listOf(1, 2, 3, 4, 5)
val doubled = numbers.map { it * 2 }
val evens = numbers.filter { it % 2 == 0 }
val sumOfSquares = numbers.fold(0) { acc, num -> acc + num * num }
println("Numbers: $numbers")
println("Doubled: $doubled")
println("Evens: $evens")
println("Sum of squares: $sumOfSquares")
// 5. Trailing lambdas
fun withPrefix(prefix: String, block: () -> String): String {
return "$prefix: ${block()}"
}
// Regular call
val result1 = withPrefix("Info", { "Hello" })
// Trailing lambda (lambda outside parentheses)
val result2 = withPrefix("Info") { "Hello" }
// When lambda is the only parameter
fun runBlock(block: () -> Unit) {
println("Running block...")
block()
}
runBlock {
println("Inside lambda")
println("Another statement")
}
println("Result1: $result1")
println("Result2: $result2")
// 6. Lambda with receiver
class StringBuilder {
private val buffer = StringBuilder()
fun append(text: String) {
buffer.append(text)
}
fun build(): String {
return buffer.toString()
}
}
fun buildString(block: StringBuilder.() -> Unit): String {
val builder = StringBuilder()
builder.block() // Call lambda with builder as receiver
return builder.build()
}
val message = buildString {
append("Hello, ")
append("World!")
}
println("Built string: $message")
// 7. Capturing variables
var counter = 0
val incrementer = {
counter++ // Lambda captures and modifies outer variable
println("Counter: $counter")
}
incrementer()
incrementer()
incrementer()
// Complex capture example
fun makeCounter(): () -> Int {
var count = 0 // Local variable captured by lambda
return { count++ } // Returns lambda that captures count
}
val counter1 = makeCounter()
val counter2 = makeCounter()
println("Counter1: ${counter1()}, ${counter1()}, ${counter1()}")
println("Counter2: ${counter2()}, ${counter2()}")
println("Counter1 again: ${counter1()}")
// 8. SAM conversion (for Java interoperability)
// Java interface: interface Runnable { void run(); }
val runnable = Runnable {
println("Running in Runnable")
}
// Using with thread (common Java pattern)
val thread = Thread {
println("Running in thread")
}
runnable.run()
thread.start()
// 9. Lambda vs function reference
fun isEven(n: Int) = n % 2 == 0
fun square(n: Int) = n * n
val numbersList = listOf(1, 2, 3, 4, 5)
// Using lambda
val evensLambda = numbersList.filter { it % 2 == 0 }
// Using function reference
val evensRef = numbersList.filter(::isEven)
// Using lambda for transformation
val squaresLambda = numbersList.map { it * it }
// Using function reference for transformation
val squaresRef = numbersList.map(::square)
println("Evens (lambda): $evensLambda")
println("Evens (ref): $evensRef")
println("Squares (lambda): $squaresLambda")
println("Squares (ref): $squaresRef")
// 10. Lambda with destructuring
data class Point(val x: Int, val y: Int)
val points = listOf(Point(1, 2), Point(3, 4), Point(5, 6))
// Destructuring in lambda parameters
val xValues = points.map { (x, y) -> x }
val sumCoordinates = points.map { (x, y) -> x + y }
println("Points: $points")
println("X values: $xValues")
println("Sum of coordinates: $sumCoordinates")
// 11. Inline lambdas (no performance overhead)
inline fun measureTime(block: () -> Unit): Long {
val start = System.currentTimeMillis()
block()
return System.currentTimeMillis() - start
}
val time = measureTime {
var sum = 0
for (i in 1..1000000) {
sum += i
}
println("Computed sum: $sum")
}
println("Computation took ${time}ms")
// 12. Returning from lambdas
fun findFirstEven(numbers: List
): Int? {
numbers.forEach {
if (it % 2 == 0) {
return it // Returns from findFirstEven, not just lambda
}
}
return null
}
// Using label to return from lambda only
fun findFirstEvenLabel(numbers: List): Int? {
numbers.forEach lambda@{
if (it % 2 == 0) {
return@lambda // Returns only from lambda, continues loop
}
}
return null
}
// Using implicit label (function name)
fun findFirstEvenImplicit(numbers: List): Int? {
numbers.forEach {
if (it % 2 == 0) {
return@forEach // Returns from lambda using function name as label
}
}
return null
}
val sampleNumbers = listOf(1, 3, 5, 2, 4, 6)
println("First even (regular return): ${findFirstEven(sampleNumbers)}")
println("First even (labeled return): ${findFirstEvenLabel(sampleNumbers)}")
println("First even (implicit label): ${findFirstEvenImplicit(sampleNumbers)}")
// 13. Lambda with vararg parameters
fun processNumbers(vararg numbers: Int, processor: (Int) -> Int): List {
return numbers.map(processor).toList()
}
val processed = processNumbers(1, 2, 3, 4, 5) { it * 10 }
println("Processed numbers: $processed")
// 14. Lambda storage in collections
val operations = mapOf Int>(
"add" to { a, b -> a + b },
"subtract" to { a, b -> a - b },
"multiply" to { a, b -> a * b },
"divide" to { a, b -> a / b }
)
val a = 20
val b = 5
println("Operations on $a and $b:")
for ((name, operation) in operations) {
println(" $name: ${operation(a, b)}")
}
// 15. Lambda with receiver in practice (DSL-like)
class HTML {
private val children = mutableListOf()
fun body(block: Body.() -> Unit) {
val body = Body().apply(block)
children.add(body)
}
override fun toString(): String {
return "\n${children.joinToString("\n")}\n"
}
}
class Body {
private val children = mutableListOf()
fun h1(text: String) {
children.add("$text
")
}
fun p(text: String) {
children.add("$text
")
}
override fun toString(): String {
return " \n ${children.joinToString("\n ")}\n "
}
}
fun html(block: HTML.() -> Unit): HTML {
return HTML().apply(block)
}
val htmlDoc = html {
body {
h1("Welcome")
p("This is a paragraph")
p("Another paragraph")
}
}
println("\nGenerated HTML:")
println(htmlDoc)
println("\nLambda expressions demonstration complete!")
}
📊 Collections
Collections in Kotlin provide a rich, type-safe API for working with groups of items. Unlike Java, Kotlin distinguishes between mutable and immutable collections at the interface level, encouraging immutability by default. The collections framework is fully interoperable with Java collections while offering extensive extension functions for functional-style operations like mapping, filtering, and reducing.
Immutable vs Mutable Collections
Kotlin's collection interfaces come in pairs: read-only (`List`, `Set`, `Map`) and mutable (`MutableList`, `MutableSet`, `MutableMap`). The read-only interfaces don't guarantee immutability (they might be backed by mutable collections), but they express intent. Factory functions like `listOf()` create read-only lists, while `mutableListOf()` creates mutable ones. This design encourages functional programming patterns.
Collection Factory Functions
Kotlin provides concise factory functions for creating collections: `listOf()`, `mutableListOf()`, `setOf()`, `mutableSetOf()`, `mapOf()`, `mutableMapOf()`. These functions accept varargs or pairs (for maps). Specialized functions like `arrayListOf()`, `hashSetOf()`, and `linkedMapOf()` create specific implementations. Empty collections are created with `emptyList()`, `emptySet()`, `emptyMap()`.
Functional Operations on Collections
The Kotlin standard library provides numerous extension functions for collection processing: `map`, `filter`, `reduce`, `fold`, `flatMap`, `groupBy`, `partition`, `zip`, etc. These higher-order functions enable declarative, functional-style code. Most are inline functions for performance. The operations are lazy for sequences (`asSequence()`) and eager for regular collections.
Sequences for Lazy Evaluation
Sequences (`Sequence`) are Kotlin's equivalent to Java streams, enabling lazy evaluation of collection operations. Unlike regular collections, sequences process elements one-by-one as needed, which can improve performance for large datasets or complex pipelines. Sequences are created with `asSequence()` or `sequenceOf()`. They support the same operations as collections but evaluate lazily.
Collection-Specific Operations
Different collection types have specialized operations: Lists support indexing (`get`, `indexOf`, `subList`), Sets support mathematical operations (`union`, `intersect`, `subtract`), and Maps support key-based access (`getOrPut`, `getOrDefault`). All collections support size checking, emptiness checks, iteration, and conversion to other collection types.
Destructuring on Collections
Lists and arrays support destructuring declarations for accessing first elements: `val (first, second) = list`. However, destructuring only works for the first N elements. For more complex destructuring, use `component1()` to `component5()` functions available on `List`. Pair and Triple are special collection-like types with destructuring support for exactly 2 and 3 elements.
Java Interoperability
Kotlin collections are fully interoperable with Java collections. Kotlin's `List` is seen as `java.util.List` in Java (read-only at compile time but mutable at runtime). When calling Java from Kotlin, collection types become platform types (`List!`). Extension functions like `toList()`, `toMutableList()` convert between Kotlin and Java collection types when needed.
// Collections Examples
fun main() {
println("=== Collections in Kotlin ===")
// 1. Creating collections
// Lists
val immutableList = listOf("Apple", "Banana", "Cherry")
val mutableList = mutableListOf("Dog", "Cat", "Bird")
// Sets
val immutableSet = setOf(1, 2, 3, 3, 2, 1) // Duplicates removed
val mutableSet = mutableSetOf("A", "B", "C")
// Maps
val immutableMap = mapOf(
1 to "One",
2 to "Two",
3 to "Three"
)
val mutableMap = mutableMapOf(
"USA" to "Washington D.C.",
"France" to "Paris",
"Japan" to "Tokyo"
)
println("Immutable List: $immutableList")
println("Mutable List: $mutableList")
println("Immutable Set: $immutableSet") // [1, 2, 3]
println("Immutable Map: $immutableMap")
// 2. Basic operations
// List operations
println("\nList Operations:")
println("First element: ${immutableList.first()}")
println("Last element: ${immutableList.last()}")
println("Element at index 1: ${immutableList[1]}")
println("Size: ${immutableList.size}")
println("Contains 'Banana': ${immutableList.contains("Banana")}")
println("Index of 'Cherry': ${immutableList.indexOf("Cherry")}")
// Set operations
println("\nSet Operations:")
println("Contains 2: ${immutableSet.contains(2)}")
println("Is empty: ${immutableSet.isEmpty()}")
// Map operations
println("\nMap Operations:")
println("Value for key 2: ${immutableMap[2]}")
println("Keys: ${immutableMap.keys}")
println("Values: ${immutableMap.values}")
println("Entries: ${immutableMap.entries}")
println("Contains key 1: ${immutableMap.containsKey(1)}")
println("Contains value 'Two': ${immutableMap.containsValue("Two")}")
// 3. Mutating mutable collections
mutableList.add("Fish")
mutableList.add(1, "Rabbit") // Insert at index
mutableList.remove("Cat")
mutableList[0] = "Puppy" // Update element
mutableSet.add("D")
mutableSet.remove("B")
mutableMap["Germany"] = "Berlin"
mutableMap.remove("France")
mutableMap["USA"] = "New York" // Update value
println("\nAfter mutation:")
println("Mutable List: $mutableList")
println("Mutable Set: $mutableSet")
println("Mutable Map: $mutableMap")
// 4. Functional operations
val numbers = listOf(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
println("\nFunctional Operations on numbers: $numbers")
// map - transform
val squares = numbers.map { it * it }
println("Squares: $squares")
// filter - select
val evens = numbers.filter { it % 2 == 0 }
println("Evens: $evens")
// reduce - combine
val sum = numbers.reduce { acc, num -> acc + num }
println("Sum: $sum")
// fold - reduce with initial value
val product = numbers.fold(1) { acc, num -> acc * num }
println("Product: $product")
// flatMap - transform and flatten
val nested = listOf(listOf(1, 2), listOf(3, 4), listOf(5, 6))
val flat = nested.flatMap { it }
println("Flat: $flat")
// groupBy - group by key
val grouped = numbers.groupBy { if (it % 2 == 0) "even" else "odd" }
println("Grouped: $grouped")
// partition - split
val (evenPart, oddPart) = numbers.partition { it % 2 == 0 }
println("Even partition: $evenPart")
println("Odd partition: $oddPart")
// zip - combine two collections
val letters = listOf("A", "B", "C", "D", "E")
val zipped = numbers.zip(letters) { num, letter -> "$letter$num" }
println("Zipped: $zipped")
// 5. Set operations
val set1 = setOf(1, 2, 3, 4, 5)
val set2 = setOf(4, 5, 6, 7, 8)
println("\nSet Operations:")
println("Set1: $set1, Set2: $set2")
println("Union: ${set1 union set2}")
println("Intersect: ${set1 intersect set2}")
println("Subtract: ${set1 subtract set2}")
// 6. Map operations
val map1 = mapOf("A" to 1, "B" to 2, "C" to 3)
val map2 = mapOf("B" to 20, "C" to 30, "D" to 40)
println("\nMap Operations:")
println("Map1: $map1, Map2: $map2")
println("getOrPut example:")
val mutableMap2 = mutableMapOf("A" to 1, "B" to 2)
val value = mutableMap2.getOrPut("C") { 3 } // Adds if key doesn't exist
println("After getOrPut: $mutableMap2, value: $value")
println("getOrDefault: ${map1.getOrDefault("X", 0)}")
// 7. Sequences (lazy evaluation)
println("\nSequences (lazy evaluation):")
val bigList = (1..1_000_000).toList()
// Eager evaluation (processes all elements)
val eagerStart = System.currentTimeMillis()
val eagerResult = bigList
.filter { it % 2 == 0 }
.map { it * it }
.take(10)
.toList()
val eagerTime = System.currentTimeMillis() - eagerStart
// Lazy evaluation with sequence (processes only needed elements)
val lazyStart = System.currentTimeMillis()
val lazyResult = bigList.asSequence()
.filter {
// print("f") // Uncomment to see lazy evaluation
it % 2 == 0
}
.map {
// print("m") // Uncomment to see lazy evaluation
it * it
}
.take(10)
.toList()
val lazyTime = System.currentTimeMillis() - lazyStart
println("Eager result: $eagerResult (${eagerTime}ms)")
println("Lazy result: $lazyResult (${lazyTime}ms)")
// Creating sequences
val naturalNumbers = generateSequence(1) { it + 1 }
val first10 = naturalNumbers.take(10).toList()
println("First 10 natural numbers: $first10")
// 8. Collection builders
val builtList = buildList {
add("First")
addAll(listOf("Second", "Third"))
if (true) {
add("Fourth")
}
}
val builtMap = buildMap {
put("A", 1)
put("B", 2)
putAll(mapOf("C" to 3, "D" to 4))
}
println("\nBuilt collections:")
println("Built list: $builtList")
println("Built map: $builtMap")
// 9. Destructuring on collections
val listForDestruct = listOf("Alice", "Bob", "Charlie", "Diana")
// Destructuring first elements
val (first, second) = listForDestruct
println("\nDestructuring:")
println("First: $first, Second: $second")
// Using component functions
println("Component1: ${listForDestruct.component1()}")
println("Component2: ${listForDestruct.component2()}")
// Pair and Triple (special collections)
val pair = "key" to "value" // Creates Pair
val (key, value2) = pair
println("Pair: $pair, key=$key, value=$value2")
val triple = Triple(1, "two", 3.0)
val (a, b, c) = triple
println("Triple: $triple, a=$a, b=$b, c=$c")
// 10. Collection transformations
println("\nCollection transformations:")
// Associate - create map from collection
val names = listOf("Alice", "Bob", "Charlie")
val nameLengthMap = names.associateWith { it.length }
println("Name lengths: $nameLengthMap")
val nameIndexMap = names.associateBy { it.first().uppercaseChar() }
println("By first letter: $nameIndexMap")
// Chunked - split into chunks
val chunked = numbers.chunked(3)
println("Chunked by 3: $chunked")
// Windowed - sliding window
val windowed = numbers.windowed(3, step = 2)
println("Windowed size 3 step 2: $windowed")
// 11. Aggregations
println("\nAggregations:")
val stats = listOf(10, 20, 30, 40, 50)
println("Numbers: $stats")
println("Min: ${stats.minOrNull()}")
println("Max: ${stats.maxOrNull()}")
println("Average: ${stats.average()}")
println("Sum: ${stats.sum()}")
// 12. Collection predicates
println("\nCollection predicates:")
println("All even? ${numbers.all { it % 2 == 0 }}")
println("Any even? ${numbers.any { it % 2 == 0 }}")
println("None negative? ${numbers.none { it < 0 }}")
println("Count even: ${numbers.count { it % 2 == 0 }}")
// 13. Finding elements
println("\nFinding elements:")
println("First even: ${numbers.first { it % 2 == 0 }}")
println("Last even: ${numbers.last { it % 2 == 0 }}")
println("First even or null: ${numbers.firstOrNull { it > 100 }}")
// 14. Sorting
val unsorted = listOf(5, 3, 8, 1, 9, 2)
println("\nSorting:")
println("Unsorted: $unsorted")
println("Sorted: ${unsorted.sorted()}")
println("Sorted descending: ${unsorted.sortedDescending()}")
println("Sorted by custom: ${unsorted.sortedBy { -it }}")
// 15. Converting between collection types
println("\nType conversions:")
val setFromList = numbers.toSet()
val listFromSet = setFromList.toList()
val arrayFromList = numbers.toTypedArray()
println("Set from list: $setFromList")
println("List from set: $listFromSet")
println("Array from list: ${arrayFromList.contentToString()}")
// 16. Java interoperability example
println("\nJava interoperability:")
// Kotlin List is seen as java.util.List in Java
// Java List is seen as (Mutable)List! in Kotlin
val kotlinList: List = listOf("Kotlin", "Java", "Scala")
// In Java, this is java.util.List
// When calling Java from Kotlin
fun receiveJavaList(list: List) { // Platform type List!
val kotlinSafeList = list.toList() // Convert to Kotlin list
println("Received from Java: $kotlinSafeList")
}
println("\nCollections demonstration complete!")
}
🌀 Coroutines
Coroutines are Kotlin's solution for asynchronous, non-blocking programming. They provide a way to write asynchronous code sequentially, making it easier to read, write, and reason about compared to callback-based or reactive approaches. Coroutines are lightweight threads—you can launch thousands of them without significant memory overhead. They enable concurrent programming while maintaining the simplicity of synchronous code.
Suspend Functions
The foundation of coroutines is suspend functions—functions that can be paused and resumed later without blocking threads. Suspend functions are marked with the `suspend` modifier and can call other suspend functions. They can only be called from coroutines or other suspend functions. Under the hood, the Kotlin compiler transforms suspend functions into state machines using continuations.
Coroutine Builders
Coroutine builders launch coroutines: `launch` starts a fire-and-forget coroutine (returns `Job`), `async` starts a coroutine that computes a result (returns `Deferred`), and `runBlocking` bridges synchronous and asynchronous code (blocks until completion). Builders accept a coroutine context that defines the dispatcher (thread pool) and other coroutine behavior.
Coroutine Context and Dispatchers
The coroutine context defines the execution environment: `Dispatchers.Default` for CPU-intensive work, `Dispatchers.IO` for I/O operations, `Dispatchers.Main` for UI updates (Android, JavaFX), and `Dispatchers.Unconfined` for no specific thread. Contexts can be combined with `+` operator and include job, exception handler, and name for debugging.
Structured Concurrency
Kotlin promotes structured concurrency—coroutines follow a parent-child hierarchy where children complete before parents. When a parent coroutine is cancelled, all its children are cancelled automatically. This prevents resource leaks and ensures proper cleanup. Coroutine scopes (`CoroutineScope`) manage this hierarchy, with `coroutineScope` and `supervisorScope` providing structured building blocks.
Channels for Communication
Channels provide a way for coroutines to communicate with each other, similar to blocking queues but non-blocking. `Channel` supports multiple producers and consumers with backpressure. Different channel types include: `RendezvousChannel` (no buffer), `LinkedListChannel` (unlimited buffer), `ArrayChannel` (fixed buffer), and `ConflatedChannel` (keeps only latest).
Flows for Reactive Streams
Flows are Kotlin's reactive streams implementation for asynchronous data streams that can emit multiple values over time. They're cold streams—data is produced on demand. Flows support operators like `map`, `filter`, `transform`, and can be transformed to/from other reactive types. They integrate with coroutines and support backpressure automatically.
Error Handling and Supervision
Coroutines provide robust error handling: exceptions propagate up the coroutine hierarchy. `CoroutineExceptionHandler` catches unhandled exceptions. `SupervisorJob` prevents child failures from cancelling siblings. `async` wraps exceptions in `Deferred` results. `try/catch` works normally within coroutines. Proper error handling is crucial for production coroutine code.
// Coroutines Examples
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.*
import kotlinx.coroutines.flow.*
import java.util.concurrent.atomic.AtomicInteger
import kotlin.system.measureTimeMillis
fun main() = runBlocking {
println("=== Coroutines in Kotlin ===")
// 1. Basic coroutine launch
println("\n1. Basic coroutine launch:")
launch {
// This runs in a coroutine
delay(1000) // Non-blocking delay
println("Hello from coroutine!")
}
println("Hello from main")
// Wait for coroutine to complete (in real example)
delay(1500)
// 2. Suspend functions
println("\n2. Suspend functions:")
suspend fun fetchUser(id: Int): String {
delay(500) // Simulate network call
return "User$id"
}
suspend fun fetchPosts(userId: Int): List {
delay(300) // Simulate database query
return listOf("Post1", "Post2", "Post3")
}
// Calling suspend functions
val user = fetchUser(1)
val posts = fetchPosts(1)
println("User: $user, Posts: $posts")
// 3. Async for concurrent operations
println("\n3. Async for concurrency:")
val time = measureTimeMillis {
val userDeferred = async { fetchUser(2) }
val postsDeferred = async { fetchPosts(2) }
// Both fetches run concurrently
val user2 = userDeferred.await()
val posts2 = postsDeferred.await()
println("Concurrent result: User: $user2, Posts: $posts2")
}
println("Time taken: ${time}ms (should be ~500ms, not 800ms)")
// 4. Coroutine contexts and dispatchers
println("\n4. Coroutine contexts and dispatchers:")
launch(Dispatchers.Default) {
println("Running on Default dispatcher - for CPU-intensive work")
}
launch(Dispatchers.IO) {
println("Running on IO dispatcher - for I/O operations")
}
launch(Dispatchers.Unconfined) {
println("Running on Unconfined - not confined to any thread")
}
// Custom context
val customContext = Dispatchers.Default + CoroutineName("MyCoroutine")
launch(customContext) {
println("Running with custom context: ${coroutineContext[CoroutineName]}")
}
delay(100) // Let coroutines complete
// 5. Structured concurrency
println("\n5. Structured concurrency:")
suspend fun fetchUserAndPosts(userId: Int): Pair> = coroutineScope {
val userDeferred = async { fetchUser(userId) }
val postsDeferred = async { fetchPosts(userId) }
userDeferred.await() to postsDeferred.await()
}
val (user3, posts3) = fetchUserAndPosts(3)
println("Structured fetch: User: $user3, Posts: $posts3")
// 6. Error handling
println("\n6. Error handling:")
val handler = CoroutineExceptionHandler { _, exception ->
println("Caught exception: $exception")
}
val job = GlobalScope.launch(handler) {
throw RuntimeException("Test exception")
}
job.join() // Wait for completion
// Try/catch in coroutines
launch {
try {
delay(100)
throw RuntimeException("Error in coroutine")
} catch (e: Exception) {
println("Caught: ${e.message}")
}
}
delay(200)
// 7. Channels for communication
println("\n7. Channels for communication:")
val channel = Channel()
launch {
// Producer
for (x in 1..5) {
channel.send(x * x)
delay(100)
}
channel.close() // Close when done
}
launch {
// Consumer
for (value in channel) {
println("Received: $value")
}
println("Channel closed")
}
delay(1000)
// 8. Flows (reactive streams)
println("\n8. Flows (reactive streams):")
fun simpleFlow(): Flow = flow {
// Flow builder
for (i in 1..3) {
delay(100) // Simulate work
emit(i) // Emit value
}
}
launch {
simpleFlow()
.map { it * it } // Transform
.filter { it % 2 != 0 } // Filter
.collect { value -> // Terminal operator
println("Flow value: $value")
}
}
delay(500)
// 9. StateFlow and SharedFlow
println("\n9. StateFlow and SharedFlow:")
val stateFlow = MutableStateFlow(0)
// Collector 1
launch {
stateFlow.collect { value ->
println("Collector 1: $value")
}
}
// Collector 2
launch {
stateFlow.collect { value ->
println("Collector 2: $value")
}
}
// Update state
stateFlow.value = 1
delay(100)
stateFlow.value = 2
delay(100)
// 10. Coroutine cancellation
println("\n10. Coroutine cancellation:")
val jobToCancel = launch {
try {
repeat(1000) { i ->
println("Job: I'm sleeping $i ...")
delay(500)
// Check for cancellation
ensureActive()
}
} catch (e: CancellationException) {
println("Job was cancelled")
throw e
} finally {
println("Job: Finally block (cleanup)")
}
}
delay(1500) // Let it run a bit
println("Main: I'm tired of waiting!")
jobToCancel.cancelAndJoin() // Cancel and wait for completion
println("Main: Now I can quit.")
// 11. Supervisor scope
println("\n11. Supervisor scope:")
supervisorScope {
val child1 = launch {
try {
delay(Long.MAX_VALUE)
} finally {
println("Child 1 cancelled")
}
}
val child2 = launch {
delay(100)
throw RuntimeException("Child 2 failed")
}
delay(200)
println("Child 1 is active: ${child1.isActive}")
println("Child 2 is active: ${child2.isActive}")
}
// 12. Select expression
println("\n12. Select expression (experimental):")
val channelA = Channel()
val channelB = Channel()
launch {
channelA.send("Message from A")
delay(200)
channelB.send("Message from B")
}
launch {
// Select the first available channel
select {
channelA.onReceive { value ->
println("Received from A: $value")
}
channelB.onReceive { value ->
println("Received from B: $value")
}
}
}
delay(500)
// 13. Flow operators
println("\n13. Flow operators:")
val numberFlow = flow {
for (i in 1..5) {
delay(50)
emit(i)
}
}
launch {
numberFlow
.transform { value ->
// Transform each value
if (value % 2 == 0) {
emit("Even: $value")
} else {
emit("Odd: $value")
}
}
.onEach { value ->
// Side effect
println("Processing: $value")
}
.collect { value ->
println("Collected: $value")
}
}
delay(400)
// 14. Flow exception handling
println("\n14. Flow exception handling:")
val errorFlow = flow {
for (i in 1..3) {
if (i == 2) {
throw RuntimeException("Error on $i")
}
emit(i)
}
}
launch {
errorFlow
.catch { e ->
// Handle exception in flow
println("Caught in flow: ${e.message}")
emit(-1) // Emit recovery value
}
.collect { value ->
println("Error flow value: $value")
}
}
delay(300)
// 15. Real-world example: parallel processing
println("\n15. Real-world example:")
suspend fun processItem(item: Int): String {
delay(100) // Simulate processing
return "Processed $item"
}
val items = (1..10).toList()
val processingTime = measureTimeMillis {
val results = items.map { item ->
async {
processItem(item)
}
}.awaitAll()
println("Processed ${results.size} items: ${results.take(3)}...")
}
println("Processing time: ${processingTime}ms (parallel)")
val sequentialTime = measureTimeMillis {
val results = mutableListOf()
for (item in items) {
results.add(processItem(item))
}
println("Sequential processing time")
}
println("Sequential time: ${sequentialTime}ms")
println("\nCoroutines demonstration complete!")
// Cleanup
coroutineContext.cancelChildren()
}
⏳ Async Programming
Asynchronous Programming in Kotlin is built around coroutines, providing a simpler alternative to callback-based or reactive approaches. Async programming allows non-blocking execution, meaning your program can perform other work while waiting for long-running operations like network requests, file I/O, or database queries. Kotlin's approach makes async code look like synchronous code, dramatically improving readability and maintainability.
From Callbacks to Coroutines
Traditional async programming uses callbacks, leading to "callback hell" with deeply nested code. Kotlin coroutines replace callbacks with sequential-looking code using `suspend` functions. The compiler transforms suspend functions into callback-based state machines, but developers write linear code. This eliminates nesting and makes error handling straightforward with regular `try/catch`.
Structured Concurrency Principles
Kotlin's async programming follows structured concurrency principles: coroutines have parent-child relationships, and children complete before their parent. When a parent is cancelled, all children are cancelled. Resources are automatically cleaned up. This structure prevents common async bugs like leaked coroutines or orphaned operations. Scopes (`CoroutineScope`) manage these relationships.
Async/Await Pattern
The `async`/`await` pattern (via `async` builder and `await()` method) allows concurrent execution of independent tasks. `async` starts a coroutine that returns a `Deferred` (a future/promise). `await()` suspends until the result is ready. Multiple `async` calls can run in parallel, and `await()` collects results. This pattern is ideal for parallelizing independent operations.
Non-Blocking vs Blocking
Coroutines provide non-blocking suspension: when a coroutine waits (with `delay()` or `await()`), it releases its thread for other work. This enables high concurrency with few threads. Contrast with blocking operations that hold a thread idle. Kotlin provides `withContext()` to switch dispatchers for blocking operations, keeping the rest of the code non-blocking.
Error Propagation in Async Code
In async programming, errors need special handling. Kotlin provides several approaches: `try/catch` around `await()`, `CoroutineExceptionHandler` for unhandled exceptions, `supervisorScope` for isolated failures, and wrapping results in `Result` or sealed classes. Proper error handling prevents silent failures and ensures robustness in production systems.
Testing Async Code
Testing async code requires special consideration. Kotlin provides `TestCoroutineDispatcher` and `runBlockingTest` for deterministic testing. `runBlockingTest` provides virtual time control, allowing fast-forwarding through delays. Test dispatchers enable controlling coroutine execution in tests. Proper testing ensures async code behaves correctly under various conditions.
Integration with Existing APIs
Kotlin provides ways to integrate coroutines with callback-based APIs: `suspendCancellableCoroutine` and `callbackFlow` bridge callbacks to coroutines. `CompletableFuture` from Java can be converted to coroutines with `asDeferred()`. RxJava observables can interoperate with flows. These integrations allow gradual migration to coroutines.
// Async Programming Examples
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.Channel
import kotlin.system.measureTimeMillis
fun main() = runBlocking {
println("=== Async Programming in Kotlin ===")
// 1. Basic async/await pattern
println("\n1. Async/Await pattern:")
suspend fun fetchDataFromSource1(): String {
delay(1000) // Simulate network call
return "Data from source 1"
}
suspend fun fetchDataFromSource2(): String {
delay(800) // Simulate network call
return "Data from source 2"
}
suspend fun fetchDataFromSource3(): String {
delay(600) // Simulate network call
return "Data from source 3"
}
val time = measureTimeMillis {
// Launch async operations concurrently
val deferred1 = async { fetchDataFromSource1() }
val deferred2 = async { fetchDataFromSource2() }
val deferred3 = async { fetchDataFromSource3() }
// Await all results (runs concurrently)
val result1 = deferred1.await()
val result2 = deferred2.await()
val result3 = deferred3.await()
println("Results: $result1, $result2, $result3")
}
println("Total time: ${time}ms (should be ~1000ms, not 2400ms)")
// 2. Converting callback-based APIs to coroutines
println("\n2. Converting callbacks to coroutines:")
// Simulating a callback-based API
class CallbackApi {
fun fetchData(callback: (String) -> Unit) {
Thread {
Thread.sleep(500) // Simulate work
callback("Data from callback")
}.start()
}
fun fetchDataWithError(callback: (String?, Throwable?) -> Unit) {
Thread {
Thread.sleep(300)
if (Math.random() > 0.5) {
callback("Success", null)
} else {
callback(null, RuntimeException("API error"))
}
}.start()
}
}
// Wrap callback API in suspend function
suspend fun CallbackApi.fetchDataSuspend(): String = suspendCancellableCoroutine { continuation ->
fetchData { result ->
continuation.resume(result)
}
// Handle cancellation
continuation.invokeOnCancellation {
println("Fetch cancelled")
}
}
suspend fun CallbackApi.fetchDataWithErrorSuspend(): String = suspendCancellableCoroutine { continuation ->
fetchDataWithError { result, error ->
if (error != null) {
continuation.resumeWithException(error)
} else {
continuation.resume(result!!)
}
}
}
val api = CallbackApi()
val data = api.fetchDataSuspend()
println("Data from callback API: $data")
try {
val dataWithError = api.fetchDataWithErrorSuspend()
println("Data with possible error: $dataWithError")
} catch (e: Exception) {
println("Caught error: ${e.message}")
}
// 3. Structured concurrency with async
println("\n3. Structured concurrency:")
suspend fun fetchUserProfile(userId: Int): Map
= coroutineScope {
val userDeferred = async {
delay(200)
mapOf("id" to userId, "name" to "User$userId")
}
val postsDeferred = async {
delay(300)
listOf("Post1", "Post2", "Post3")
}
val friendsDeferred = async {
delay(150)
listOf("Friend1", "Friend2")
}
mapOf(
"user" to userDeferred.await(),
"posts" to postsDeferred.await(),
"friends" to friendsDeferred.await()
)
}
val profile = fetchUserProfile(1)
println("User profile: $profile")
// 4. Error handling in async operations
println("\n4. Error handling:")
suspend fun riskyOperation(id: Int): String {
delay(100)
if (id == 2) {
throw RuntimeException("Failed for ID $id")
}
return "Result for $id"
}
// Approach 1: try/catch around each async
val job1 = launch {
try {
val result = async { riskyOperation(1) }.await()
println("Result 1: $result")
} catch (e: Exception) {
println("Caught error for op 1: ${e.message}")
}
}
// Approach 2: Handle errors in Deferred
val deferred2 = async {
try {
riskyOperation(2)
} catch (e: Exception) {
"Error: ${e.message}"
}
}
val deferred3 = async { riskyOperation(3) }
job1.join()
println("Result 2: ${deferred2.await()}")
println("Result 3: ${deferred3.await()}")
// 5. Timeouts and cancellation
println("\n5. Timeouts and cancellation:")
suspend fun longRunningTask(): String {
try {
delay(5000) // Long operation
return "Completed"
} catch (e: CancellationException) {
println("Task was cancelled")
throw e
}
}
// With timeout
try {
val result = withTimeout(1000) {
longRunningTask()
}
println("Result: $result")
} catch (e: TimeoutCancellationException) {
println("Task timed out: ${e.message}")
}
// With timeoutOrNull
val result = withTimeoutOrNull(1000) {
longRunningTask()
}
println("Result or null: $result")
// 6. Parallel processing with async
println("\n6. Parallel processing:")
suspend fun processItem(item: Int): String {
delay(100) // Simulate processing
return "Processed $item"
}
val items = (1..10).toList()
// Sequential processing
val sequentialTime = measureTimeMillis {
val sequentialResults = mutableListOf()
for (item in items) {
sequentialResults.add(processItem(item))
}
println("Sequential: processed ${sequentialResults.size} items")
}
// Parallel processing
val parallelTime = measureTimeMillis {
val deferredResults = items.map { item ->
async { processItem(item) }
}
val parallelResults = deferredResults.awaitAll()
println("Parallel: processed ${parallelResults.size} items")
}
println("Sequential time: ${sequentialTime}ms")
println("Parallel time: ${parallelTime}ms")
println("Speedup: ${sequentialTime.toDouble() / parallelTime}x")
// 7. Resource management with async
println("\n7. Resource management:")
class DatabaseConnection : AutoCloseable {
init { println("Database connected") }
fun query() = "Query result"
override fun close() { println("Database disconnected") }
}
suspend fun withDatabase(block: suspend (DatabaseConnection) -> String): String {
val connection = DatabaseConnection()
return try {
block(connection)
} finally {
connection.close()
}
}
val dbResult = withDatabase { connection ->
delay(100) // Simulate query
connection.query()
}
println("Database result: $dbResult")
// 8. Combining multiple async patterns
println("\n8. Combining async patterns:")
data class User(val id: Int, val name: String)
data class Post(val id: Int, val title: String, val userId: Int)
data class Comment(val id: Int, val text: String, val postId: Int)
suspend fun fetchUsers(): List {
delay(200)
return listOf(User(1, "Alice"), User(2, "Bob"))
}
suspend fun fetchPosts(userId: Int): List {
delay(150)
return listOf(Post(1, "Post 1 by $userId", userId), Post(2, "Post 2 by $userId", userId))
}
suspend fun fetchComments(postId: Int): List {
delay(100)
return listOf(Comment(1, "Great post!", postId), Comment(2, "Thanks!", postId))
}
suspend fun fetchCompleteData(): List, List>>> = coroutineScope {
val users = fetchUsers()
users.map { user ->
async {
val posts = fetchPosts(user.id)
val postComments = posts.map { post ->
async { fetchComments(post.id) }
}.awaitAll()
Triple(user, posts, postComments)
}
}.awaitAll()
}
val completeData = fetchCompleteData()
println("Complete data fetched for ${completeData.size} users")
// 9. Rate limiting and batching
println("\n9. Rate limiting and batching:")
suspend fun callApi(item: Int): String {
delay(50) // Simulate API call
return "API result for $item"
}
// Process with concurrency limit
val batchSize = 3
val batches = items.chunked(batchSize)
val batchedTime = measureTimeMillis {
val allResults = mutableListOf()
for (batch in batches) {
val batchResults = batch.map { item ->
async { callApi(item) }
}.awaitAll()
allResults.addAll(batchResults)
delay(100) // Rate limiting between batches
}
println("Batched: processed ${allResults.size} items in ${batches.size} batches")
}
println("Batched time: ${batchedTime}ms")
// 10. Async with flows
println("\n10. Async with flows:")
fun pollForUpdates(): Flow = flow {
var counter = 0
while (true) {
delay(1000) // Poll every second
emit(counter++)
}
}
val updateJob = launch {
pollForUpdates()
.take(5) // Take 5 updates
.collect { update ->
println("Update received: $update")
}
}
updateJob.join()
// 11. Real-world example: concurrent web scraping
println("\n11. Real-world example: concurrent web scraping")
suspend fun scrapePage(url: String): List {
delay((100..500).random().toLong()) // Simulate varying load times
return listOf("Data from $url")
}
val urls = listOf(
"https://example.com/page1",
"https://example.com/page2",
"https://example.com/page3",
"https://example.com/page4",
"https://example.com/page5"
)
val scrapeTime = measureTimeMillis {
val scrapedData = urls.map { url ->
async { scrapePage(url) }
}.awaitAll()
.flatten()
println("Scraped ${scrapedData.size} items from ${urls.size} URLs")
}
println("Scraping time: ${scrapeTime}ms")
// 12. Testing async code (conceptual)
println("\n12. Testing async code (conceptual):")
// In tests, you would use:
// runBlockingTest {
// // Test code with virtual time control
// val deferred = async { fetchData() }
// advanceTimeBy(1000) // Fast-forward
// assertEquals("Expected", deferred.await())
// }
println("\nAsync programming demonstration complete!")
}
🌊 Flows
Flows are Kotlin's reactive streams implementation for asynchronous data streams that can emit multiple values over time. They represent cold asynchronous data streams—data is produced on demand when collectors are present. Flows are built on coroutines and provide a declarative way to handle streams of values with backpressure support, making them ideal for live data, event streams, or any scenario where data arrives over time.
Cold vs Hot Streams
Flows are cold streams: they start producing values only when a collector starts collecting. Each collector gets its own independent stream. This differs from hot streams (like `StateFlow` or `SharedFlow`) that emit values regardless of collectors. Cold flows are suitable for one-to-one producer-consumer relationships, while hot flows are better for broadcasting to multiple consumers.
Flow Builders
Several ways create flows: `flow { ... }` builder for custom emission, `flowOf()` for fixed values, `asFlow()` extension to convert collections/channels, `channelFlow { ... }` for complex producers with await/emit, and `callbackFlow { ... }` to bridge callback APIs. Each builder serves different use cases from simple to complex stream creation.
Flow Operators
Flows support rich operators similar to collections: `map`, `filter`, `transform`, `take`, `drop`, `zip`, `combine`, etc. Operators are inline extension functions that return new flows. They can be intermediate (return flow) or terminal (start collection). Operators are sequential by default but can be made concurrent with `flowOn` or `buffer`.
Exception Handling
Flow exceptions can be caught with `catch` operator, which can emit recovery values or rethrow. The `retry` operator retries collection on failure. Terminal operators like `collect` can wrap collection in `try/catch`. For transparent error handling, flows can emit `Result` or sealed class wrappers. Proper error handling ensures robust stream processing.
Context and Threading
Flows are sequential and execute in the context of the collector by default. `flowOn` changes the context for upstream operations. `buffer` allows concurrent processing by buffering emissions. `conflate` drops intermediate values when consumer is slow. `collectLatest` cancels slow collection when new value arrives. These operators control flow execution and backpressure.
StateFlow and SharedFlow
`StateFlow` is a hot flow that represents a state holder with a single current value and replays it to new collectors. `SharedFlow` is a hot flow that broadcasts values to multiple collectors with configurable replay cache. Both are used for state management and event broadcasting in MVVM/MVI architectures, particularly in Android apps.
Integration with Other APIs
Flows integrate with other reactive APIs: they can be converted to/from RxJava Observables/Flowables with `asFlow()`/`asObservable()`. Channels can be converted to flows with `receiveAsFlow()`. Callback APIs can be adapted with `callbackFlow`. LiveData (Android) can interop with `asLiveData()`. This enables gradual adoption and interoperability.
// Flows Examples
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
import kotlin.system.measureTimeMillis
fun main() = runBlocking {
println("=== Flows in Kotlin ===")
// 1. Basic flow creation
println("\n1. Basic flow creation:")
// flow builder
fun simpleFlow(): Flow = flow {
println("Flow started")
for (i in 1..3) {
delay(100) // Simulate work
emit(i) // Emit value
}
}
// Collect the flow (terminal operator)
simpleFlow().collect { value ->
println("Collected: $value")
}
// flowOf for fixed values
val fixedFlow = flowOf(1, 2, 3, 4, 5)
// asFlow from collections
val listFlow = listOf("A", "B", "C").asFlow()
// 2. Flow operators (intermediate)
println("\n2. Flow operators:")
val numbersFlow = (1..10).asFlow()
numbersFlow
.filter { it % 2 == 0 } // Keep even numbers
.map { it * it } // Square them
.transform { value ->
// Custom transformation
if (value > 20) {
emit("Large: $value")
} else {
emit("Small: $value")
}
}
.collect { value ->
println("Transformed: $value")
}
// 3. Terminal operators
println("\n3. Terminal operators:")
val sumFlow = (1..5).asFlow()
// toList/toSet - collect to collection
val list = sumFlow.toList()
println("ToList: $list")
// single/singleOrNull - expect single value
val singleFlow = flowOf(42)
val singleValue = singleFlow.single()
println("Single: $singleValue")
// reduce/fold - combine values
val reduceFlow = (1..5).asFlow()
val sum = reduceFlow.reduce { acc, value -> acc + value }
println("Reduce sum: $sum")
val foldFlow = (1..5).asFlow()
val product = foldFlow.fold(1) { acc, value -> acc * value }
println("Fold product: $product")
// first/last - get first/last value
val firstFlow = (1..10).asFlow()
val firstEven = firstFlow.first { it % 2 == 0 }
println("First even: $firstEven")
// count - count elements
val countFlow = (1..10).asFlow()
val count = countFlow.count { it > 5 }
println("Count > 5: $count")
// 4. Flow context and threading
println("\n4. Flow context and threading:")
fun contextFlow(): Flow = flow {
for (i in 1..3) {
Thread.sleep(100) // Simulate CPU-intensive work
emit(i)
println("Emitted $i from ${Thread.currentThread().name}")
}
}
// flowOn changes context for upstream
contextFlow()
.flowOn(Dispatchers.Default) // Run emitter in background
.collect { value ->
println("Collected $value in ${Thread.currentThread().name}")
}
// 5. Buffering and concurrency
println("\n5. Buffering and concurrency:")
fun slowFlow(): Flow = flow {
for (i in 1..3) {
delay(100) // Slow producer
emit(i)
println("Emitted $i at ${System.currentTimeMillis()}")
}
}
val time = measureTimeMillis {
slowFlow()
.buffer() // Buffer emissions, don't wait
.collect { value ->
delay(300) // Slow consumer
println("Collected $value at ${System.currentTimeMillis()}")
}
}
println("Buffered collection took ${time}ms")
// conflate - skip intermediate values
val conflateTime = measureTimeMillis {
slowFlow()
.conflate() // Keep only latest
.collect { value ->
delay(300)
println("Conflated $value")
}
}
println("Conflated collection took ${conflateTime}ms")
// collectLatest - cancel slow collection on new value
val latestTime = measureTimeMillis {
slowFlow()
.collectLatest { value -> // Cancel previous collection
println("Collecting $value")
delay(300) // Will be cancelled by next emission
println("Collected $value") // May not reach here
}
}
println("collectLatest took ${latestTime}ms")
// 6. Combining flows
println("\n6. Combining flows:")
val flowA = (1..3).asFlow().onEach { delay(100) }
val flowB = flowOf("A", "B", "C").onEach { delay(150) }
// zip - combine corresponding elements
flowA.zip(flowB) { a, b -> "$a -> $b" }
.collect { println("Zipped: $it") }
// combine - combine latest values
val combined = flowA.combine(flowB) { a, b -> "$a - $b" }
println("Combined sample: ")
combined.take(5).collect { println(" $it") }
// merge - merge emissions from multiple flows
val merged = merge(flowA.map { "Number: $it" }, flowB.map { "Letter: $it" })
println("Merged sample: ")
merged.take(6).collect { println(" $it") }
// 7. Exception handling in flows
println("\n7. Exception handling:")
fun failingFlow(): Flow = flow {
for (i in 1..3) {
delay(100)
if (i == 2) {
throw RuntimeException("Error on $i")
}
emit(i)
}
}
// catch operator
failingFlow()
.catch { e ->
println("Caught: ${e.message}")
emit(-1) // Emit recovery value
}
.collect { value ->
println("Value with catch: $value")
}
// retry operator
var attempt = 0
fun retryFlow(): Flow = flow {
attempt++
println("Attempt $attempt")
if (attempt < 3) {
throw RuntimeException("Fail attempt $attempt")
}
emit(42)
}
retryFlow()
.retry(2) { cause ->
println("Retrying after: ${cause.message}")
delay(100)
true // Retry
}
.collect { value ->
println("After retry: $value")
}
// 8. StateFlow (hot flow with state)
println("\n8. StateFlow:")
val stateFlow = MutableStateFlow(0) // Initial value
// Collector 1
val job1 = launch {
stateFlow.collect { value ->
println("Collector 1: $value")
}
}
// Collector 2 (starts later)
delay(50)
val job2 = launch {
stateFlow.collect { value ->
println("Collector 2: $value")
}
}
// Update state
stateFlow.value = 1
delay(100)
stateFlow.value = 2
delay(100)
job1.cancel()
job2.cancel()
// 9. SharedFlow (hot flow for events)
println("\n9. SharedFlow:")
val sharedFlow = MutableSharedFlow(
replay = 2, // Replay last 2 values to new collectors
extraBufferCapacity = 10
)
// Emitter
launch {
for (i in 1..5) {
delay(100)
sharedFlow.emit(i)
println("Emitted: $i")
}
}
// Collector (starts after some emissions)
delay(250)
launch {
sharedFlow.collect { value ->
println("Late collector: $value")
}
}
delay(500) // Let it run
// 10. Channel flow (complex producers)
println("\n10. Channel flow:")
fun channelFlowExample(): Flow = channelFlow {
// Can use coroutine builders inside
launch {
for (i in 1..3) {
delay(100)
send(i * 2)
}
}
launch {
for (i in 1..3) {
delay(150)
send(i * 3)
}
}
}
channelFlowExample()
.take(6)
.collect { value ->
println("Channel flow: $value")
}
// 11. Callback to flow
println("\n11. Callback to flow:")
class EventEmitter {
private val listeners = mutableListOf<(String) -> Unit>()
fun emitEvent(event: String) {
listeners.forEach { it(event) }
}
fun events(): Flow = callbackFlow {
val listener = { event: String ->
trySend(event)
}
listeners.add(listener)
awaitClose {
listeners.remove(listener)
println("Listener removed")
}
}
}
val emitter = EventEmitter()
val eventJob = launch {
emitter.events()
.take(3)
.collect { event ->
println("Event: $event")
}
}
// Emit events
emitter.emitEvent("Event 1")
delay(100)
emitter.emitEvent("Event 2")
delay(100)
emitter.emitEvent("Event 3")
eventJob.join()
// 12. Real-world example: paginated API
println("\n12. Real-world example: paginated API")
data class Page(val items: List, val hasNext: Boolean)
fun fetchPage(page: Int): Page {
// Simulate API call
val items = (1..10).map { "Item ${(page - 1) * 10 + it}" }
return Page(items, page < 3) // 3 pages total
}
fun paginatedFlow(): Flow = flow {
var page = 1
var hasNext = true
while (hasNext) {
println("Fetching page $page")
val result = fetchPage(page)
result.items.forEach { item ->
emit(item)
}
hasNext = result.hasNext
page++
delay(200) // Rate limiting
}
}
paginatedFlow()
.take(15) // Take first 15 items
.collect { item ->
println(" $item")
}
// 13. Flow transformations
println("\n13. Flow transformations:")
fun tickerFlow(period: Long): Flow = flow {
while (true) {
emit(Unit)
delay(period)
}
}
// Transform ticker into sequential numbers
tickerFlow(100)
.scan(0) { acc, _ -> acc + 1 } // Like fold but emits intermediate values
.take(5)
.collect { count ->
println("Ticker count: $count")
}
// 14. Flow with timeout
println("\n14. Flow with timeout:")
fun slowEmitFlow(): Flow = flow {
for (i in 1..3) {
delay(200) // Slow emission
emit(i)
}
}
// Timeout on each emission
try {
slowEmitFlow()
.onEach { value ->
withTimeout(150) { // Timeout per element
delay(100) // Fast processing
println("Processed $value in time")
}
}
.collect()
} catch (e: TimeoutCancellationException) {
println("Timeout: ${e.message}")
}
println("\nFlows demonstration complete!")
}
📡 Channels
Channels in Kotlin coroutines provide a way for coroutines to communicate with each other by sending and receiving elements. They are conceptually similar to blocking queues but are non-blocking and suspending. Channels support multiple producers and consumers and are a fundamental building block for implementing producer-consumer patterns, work pools, and event broadcasting in concurrent applications.
Channel Basics and Types
Channels come in several types with different buffering strategies: `RendezvousChannel` (no buffer, sender suspends until receiver ready), `LinkedListChannel` (unlimited buffer), `ArrayChannel` (fixed-size buffer), and `ConflatedChannel` (buffer of 1, new value replaces old). The default `Channel()` creates a rendezvous channel. Buffering affects backpressure handling and performance characteristics.
Channel Operations
Basic channel operations are `send(element)` to send (suspends if buffer full) and `receive()` to receive (suspends if buffer empty). `offer(element)` tries to send without suspension (returns success/failure). `poll()` tries to receive without suspension. `close()` closes the channel (sending `ClosedSendChannelException` to new senders). `isClosedForSend` and `isClosedForReceive` check channel state.
Producer-Consumer Patterns
Channels naturally implement producer-consumer patterns. `produce` builder creates a coroutine that produces values to a channel. `consumeEach` extension consumes all values from a channel. Multiple producers can send to the same channel, and multiple consumers can receive, though each element is delivered to only one consumer. This enables work distribution patterns.
Fan-out and Fan-in
Fan-out: multiple coroutines receive from the same channel (competing consumers). Each element goes to only one consumer. Fan-in: multiple coroutines send to the same channel. Channels handle synchronization automatically. These patterns enable scaling consumer/producer sides independently. For broadcasting to multiple consumers, use `BroadcastChannel` or `SharedFlow`.
Channel Pipelines
Channels can be connected in pipelines: one coroutine receives from a channel, processes, and sends to another channel. This enables staged processing with backpressure propagation. Pipelines can be linear or form directed acyclic graphs. Each stage can run on different dispatchers for parallelism. Pipelines are a powerful pattern for data processing.
Select Expression with Channels
The `select` expression (experimental) allows waiting for the first completed operation among multiple channels. It can `onReceive` from channels, `onSend` to channels, `onAwait` on `Deferred`, or `onTimeout`. Select enables building reactive systems that respond to the first available event. It's useful for implementing timeouts, priority channels, or multiplexing.
Channels vs Flows
Channels are hot: they exist independently of consumers and buffer elements. Flows are cold: they produce elements on demand. Channels are for communication between coroutines. Flows are for streaming data with transformations. Use channels when you need decoupled producers/consumers with buffering. Use flows for data transformations and cold streams.
// Channels Examples
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.*
import kotlin.system.measureTimeMillis
fun main() = runBlocking {
println("=== Channels in Kotlin ===")
// 1. Basic channel operations
println("\n1. Basic channel operations:")
val channel = Channel()
launch {
// Producer
for (x in 1..5) {
delay(100)
println("Sending $x")
channel.send(x * x)
}
channel.close() // Close when done
println("Channel closed")
}
launch {
// Consumer
for (y in channel) {
println("Received $y")
delay(200) // Slow consumer
}
}
delay(1500)
// 2. Different channel types
println("\n2. Different channel types:")
// Rendezvous channel (default) - no buffer
val rendezvousChannel = Channel()
// Buffered channel - capacity 2
val bufferedChannel = Channel(capacity = 2)
// Conflated channel - keep only latest
val conflatedChannel = Channel(Channel.CONFLATED)
// Unlimited channel
val unlimitedChannel = Channel(Channel.UNLIMITED)
// Test buffered channel
launch {
for (i in 1..5) {
bufferedChannel.send("Message $i")
println("Sent $i to buffered channel")
delay(50)
}
bufferedChannel.close()
}
delay(100) // Let sender fill buffer
launch {
for (msg in bufferedChannel) {
println("Buffered received: $msg")
delay(150) // Slow receiver
}
}
delay(1000)
// 3. Producer and consumer builders
println("\n3. Producer and consumer builders:")
// produce builder
val numberChannel = produce {
for (x in 1..5) {
send(x)
delay(100)
}
}
// consumeEach extension
numberChannel.consumeEach { value ->
println("Produced: $value")
}
// 4. Multiple producers
println("\n4. Multiple producers:")
val multiChannel = Channel()
// Producer 1
launch {
for (i in 1..3) {
delay(120)
multiChannel.send("Producer 1: $i")
}
}
// Producer 2
launch {
for (i in 1..3) {
delay(80)
multiChannel.send("Producer 2: $i")
}
}
// Consumer
launch {
delay(500) // Start consuming after some production
for (msg in multiChannel) {
println("Consumed: $msg")
}
}
delay(1000)
multiChannel.close()
// 5. Multiple consumers (fan-out)
println("\n5. Multiple consumers (fan-out):")
val fanOutChannel = Channel()
// Producer
launch {
for (x in 1..10) {
fanOutChannel.send(x)
delay(50)
}
fanOutChannel.close()
}
// Consumer 1
launch {
for (value in fanOutChannel) {
println("Consumer 1: $value")
delay(100)
}
}
// Consumer 2
launch {
for (value in fanOutChannel) {
println("Consumer 2: $value")
delay(150)
}
}
delay(1500)
// 6. Pipeline pattern
println("\n6. Pipeline pattern:")
// Stage 1: Generate numbers
fun CoroutineScope.numbersFrom(start: Int) = produce {
var x = start
while (true) {
send(x++)
delay(100)
}
}
// Stage 2: Filter primes
fun CoroutineScope.filterPrimes(numbers: ReceiveChannel) = produce {
for (x in numbers) {
if (isPrime(x)) {
send(x)
}
}
}
// Simple prime check
fun isPrime(n: Int): Boolean {
if (n <= 1) return false
for (i in 2..n/2) {
if (n % i == 0) return false
}
return true
}
// Build pipeline
val numbers = numbersFrom(2)
val primes = filterPrimes(numbers)
// Consume first 10 primes
repeat(10) {
println("Prime: ${primes.receive()}")
}
coroutineContext.cancelChildren() // Cancel pipeline
// 7. Fan-in pattern
println("\n7. Fan-in pattern:")
suspend fun sendString(channel: SendChannel, text: String, time: Long) {
while (true) {
delay(time)
channel.send(text)
}
}
val fanInChannel = Channel()
launch { sendString(fanInChannel, "Foo", 200L) }
launch { sendString(fanInChannel, "BAR", 300L) }
// Receive first 6
repeat(6) {
println(fanInChannel.receive())
}
coroutineContext.cancelChildren()
// 8. Select expression with channels
println("\n8. Select expression (experimental):")
val channelA = Channel()
val channelB = Channel()
launch {
delay(100)
channelA.send("Channel A")
delay(200)
channelB.send("Channel B")
delay(100)
channelA.send("Channel A again")
}
launch {
repeat(3) {
select {
channelA.onReceive { value ->
println("Selected from A: $value")
}
channelB.onReceive { value ->
println("Selected from B: $value")
}
}
}
}
delay(500)
// 9. Channel timeouts with select
println("\n9. Channel timeouts:")
val timeoutChannel = Channel()
launch {
delay(250)
timeoutChannel.send("Data arrived")
}
select {
timeoutChannel.onReceive { value ->
println("Received: $value")
}
onTimeout(200) {
println("Timed out waiting for channel")
}
}
// 10. Broadcast channel (for multiple consumers)
println("\n10. Broadcast channel:")
val broadcastChannel = BroadcastChannel(1)
// Consumer 1
val receiver1 = broadcastChannel.openSubscription()
launch {
for (value in receiver1) {
println("Broadcast receiver 1: $value")
}
}
// Consumer 2
val receiver2 = broadcastChannel.openSubscription()
launch {
delay(100) // Start later
for (value in receiver2) {
println("Broadcast receiver 2: $value")
}
}
// Producer
launch {
for (i in 1..3) {
delay(150)
broadcastChannel.send(i)
}
broadcastChannel.close()
}
delay(1000)
// 11. Work pool pattern
println("\n11. Work pool pattern:")
data class WorkItem(val id: Int, val data: String)
val workChannel = Channel()
val resultChannel = Channel()
// Workers
repeat(3) { workerId ->
launch {
for (work in workChannel) {
delay((100..300).random().toLong()) // Simulate work
val result = "Worker $workerId processed ${work.id}: ${work.data.uppercase()}"
resultChannel.send(result)
}
}
}
// Producer
launch {
for (i in 1..10) {
workChannel.send(WorkItem(i, "item-$i"))
}
workChannel.close()
}
// Result collector
launch {
var count = 0
for (result in resultChannel) {
println("Result: $result")
if (++count == 10) {
resultChannel.close()
}
}
}
delay(2000)
// 12. Rate limiting with channels
println("\n12. Rate limiting:")
val rateLimitChannel = Channel(Channel.RENDEZVOUS)
// Rate limiter
launch {
for (item in rateLimitChannel) {
println("Processing: $item at ${System.currentTimeMillis()}")
delay(200) // Process at most 5 items per second
}
}
// Fast producer
launch {
for (i in 1..10) {
rateLimitChannel.send(i)
println("Sent: $i at ${System.currentTimeMillis()}")
}
rateLimitChannel.close()
}
delay(2500)
// 13. Channel transformations
println("\n13. Channel transformations:")
val sourceChannel = Channel()
val transformedChannel = Channel()
// Transformer coroutine
launch {
for (value in sourceChannel) {
delay(50)
transformedChannel.send("Transformed: ${value * 2}")
}
transformedChannel.close()
}
// Source producer
launch {
for (i in 1..5) {
sourceChannel.send(i)
delay(100)
}
sourceChannel.close()
}
// Consumer
launch {
for (result in transformedChannel) {
println(result)
}
}
delay(1000)
// 14. Real-world example: job queue
println("\n14. Real-world example: job queue")
data class Job(val id: Int, val priority: Int)
val jobQueue = Channel()
// Job processor
repeat(2) { processorId ->
launch {
for (job in jobQueue) {
println("Processor $processorId starting job ${job.id} (priority ${job.priority})")
delay((100..300).random().toLong()) // Process time varies
println("Processor $processorId completed job ${job.id}")
}
}
}
// Job submitter
launch {
val jobs = listOf(
Job(1, 1),
Job(2, 3),
Job(3, 2),
Job(4, 1),
Job(5, 3)
)
// Submit jobs (higher priority first)
jobs.sortedByDescending { it.priority }
.forEach { job ->
jobQueue.send(job)
delay(50) // Time between submissions
}
delay(500) // Wait for processing
jobQueue.close()
}
delay(2000)
println("\nChannels demonstration complete!")
}
🎨 DSL Design
Domain-Specific Languages (DSLs) in Kotlin leverage the language's expressive syntax to create mini-languages tailored to specific domains. Kotlin DSLs provide type-safe, readable, and maintainable ways to express domain logic through a fluent API. They're built using lambdas with receivers, infix functions, operator overloading, and extension functions, allowing you to create APIs that read like natural language while maintaining compile-time safety.
Lambda with Receiver
The foundation of Kotlin DSLs is the lambda with receiver, which has access to members of a receiver object within the lambda body. The type `T.() -> R` represents a function that can be called on a receiver of type `T`. Inside such a lambda, `this` refers to the receiver, and members can be accessed without qualification. This enables building nested, scoped structures that mirror the problem domain.
Builder Pattern Implementation
DSLs often implement the builder pattern, where a root function (like `html { }`) takes a lambda with receiver to configure a complex object. Inside the lambda, extension functions and properties provide a fluent API. Builders can have nested builders for hierarchical structures. The result is code that clearly expresses structure while hiding implementation details.
Infix Functions for Natural Syntax
Infix functions (marked with `infix`) can be called without dots or parentheses for single parameters, creating natural-looking syntax: `element attribute "value"` instead of `element.attribute("value")`. This is particularly useful in DSLs for assignments and relationships. Infix notation should be used judiciously to maintain readability while providing fluent syntax.
Operator Overloading in DSLs
Operator overloading allows defining behavior for operators like `+`, `-`, `*`, `/`, `in`, etc. In DSLs, operators can represent domain-specific operations. For example, in a CSS DSL, `padding + margin` could combine styles. Operators should be overloaded only when they match the domain's semantics to avoid confusion. Overloaded operators enable concise, expressive DSLs.
Type-Safe Builders
Kotlin DSLs are type-safe: the compiler validates the structure and content. Through clever use of generics and extension functions, DSLs can enforce correct usage patterns. For example, an HTML DSL can ensure tags are properly nested and attributes are valid for each tag. Type safety prevents runtime errors and provides IDE support like autocomplete and validation.
Scope Control and Nesting
DSLs control scope through receiver types. Different nesting levels can have different receivers, providing appropriate functions at each level. `@DslMarker` annotations prevent implicit receivers from outer scopes from being accessed accidentally, forcing explicit qualification. This prevents errors in complex nested DSLs while maintaining clean syntax.
Real-World DSL Examples
Kotlin has several production DSLs: Kotlin HTML DSL for generating HTML, Kotlinx HTML for web templates, Ktor routing DSL for HTTP servers, KotlinTest for testing, Exposed for SQL, and Gradle Kotlin DSL for build scripts. These demonstrate how DSLs make code more declarative and domain-focused. Creating custom DSLs follows the same principles used in these libraries.
// DSL Design Examples
fun main() {
println("=== DSL Design in Kotlin ===")
// 1. Basic DSL with lambda receiver
println("\n1. Basic DSL with lambda receiver:")
class Person {
var name: String = ""
var age: Int = 0
var address: String = ""
override fun toString(): String {
return "Person(name='$name', age=$age, address='$address')"
}
}
fun person(block: Person.() -> Unit): Person {
return Person().apply(block)
}
val john = person {
name = "John Doe"
age = 30
address = "123 Main St"
}
println(john)
// 2. Nested DSL builders
println("\n2. Nested DSL builders:")
class Address {
var street: String = ""
var city: String = ""
var zip: String = ""
override fun toString(): String {
return "$street, $city, $zip"
}
}
class Employee {
var name: String = ""
var position: String = ""
val address = Address()
fun address(block: Address.() -> Unit) {
address.block()
}
override fun toString(): String {
return "Employee(name='$name', position='$position', address=$address)"
}
}
fun employee(block: Employee.() -> Unit): Employee {
return Employee().apply(block)
}
val alice = employee {
name = "Alice Smith"
position = "Software Engineer"
address {
street = "456 Oak Ave"
city = "San Francisco"
zip = "94107"
}
}
println(alice)
// 3. HTML DSL example
println("\n3. HTML DSL example:")
interface Element {
fun render(builder: StringBuilder, indent: String)
}
class TextElement(val text: String) : Element {
override fun render(builder: StringBuilder, indent: String) {
builder.append("$indent$text\n")
}
}
abstract class Tag(val name: String) : Element {
val children = mutableListOf()
val attributes = mutableMapOf()
protected fun initTag(tag: T, init: T.() -> Unit): T {
tag.init()
children.add(tag)
return tag
}
override fun render(builder: StringBuilder, indent: String) {
builder.append("$indent<$name${renderAttributes()}>\n")
for (c in children) {
c.render(builder, "$indent ")
}
builder.append("$indent$name>\n")
}
private fun renderAttributes(): String {
if (attributes.isEmpty()) return ""
return attributes.entries.joinToString(" ", " ") { "${it.key}='${it.value}'" }
}
override fun toString(): String {
val builder = StringBuilder()
render(builder, "")
return builder.toString()
}
}
class HTML : Tag("html") {
fun head(init: Head.() -> Unit) = initTag(Head(), init)
fun body(init: Body.() -> Unit) = initTag(Body(), init)
}
class Head : Tag("head") {
fun title(init: Title.() -> Unit) = initTag(Title(), init)
}
class Body : Tag("body") {
fun h1(init: H1.() -> Unit) = initTag(H1(), init)
fun p(init: P.() -> Unit) = initTag(P(), init)
fun ul(init: UL.() -> Unit) = initTag(UL(), init)
}
class Title : Tag("title")
class H1 : Tag("h1")
class P : Tag("p")
class UL : Tag("ul") {
fun li(init: LI.() -> Unit) = initTag(LI(), init)
}
class LI : Tag("li")
fun html(init: HTML.() -> Unit): HTML {
return HTML().apply(init)
}
val document = html {
head {
title {
children.add(TextElement("My Page"))
}
}
body {
h1 {
children.add(TextElement("Welcome to Kotlin DSL"))
}
p {
attributes["class"] = "intro"
children.add(TextElement("This is a paragraph created with DSL"))
}
ul {
li { children.add(TextElement("First item")) }
li { children.add(TextElement("Second item")) }
li { children.add(TextElement("Third item")) }
}
}
}
println("Generated HTML:")
println(document)
// 4. Infix functions in DSL
println("\n4. Infix functions in DSL:")
class Configuration {
val settings = mutableMapOf()
infix fun String.to(value: Any) {
settings[this] = value
}
infix fun String.by(value: Any) {
settings[this] = value
}
override fun toString(): String {
return settings.entries.joinToString("\n") { " ${it.key} = ${it.value}" }
}
}
fun configure(block: Configuration.() -> Unit): Configuration {
return Configuration().apply(block)
}
val config = configure {
"server" to "localhost"
"port" to 8080
"timeout" by 30
"retries" by 3
}
println("Configuration:")
println(config)
// 5. Operator overloading in DSL
println("\n5. Operator overloading in DSL:")
data class Vector(val x: Int, val y: Int) {
operator fun plus(other: Vector): Vector {
return Vector(x + other.x, y + other.y)
}
operator fun minus(other: Vector): Vector {
return Vector(x - other.x, y - other.y)
}
operator fun times(scalar: Int): Vector {
return Vector(x * scalar, y * scalar)
}
}
class VectorBuilder {
val vectors = mutableListOf()
infix fun Int.and(y: Int) {
vectors.add(Vector(this, y))
}
infix fun Vector.plus(other: Vector) {
vectors.add(this + other)
}
infix fun Vector.minus(other: Vector) {
vectors.add(this - other)
}
}
fun vectors(block: VectorBuilder.() -> Unit): List {
return VectorBuilder().apply(block).vectors
}
val vectorList = vectors {
1 and 2
3 and 4
Vector(1, 2) plus Vector(3, 4)
Vector(5, 6) minus Vector(2, 3)
Vector(2, 3) * 3
}
println("Vectors: $vectorList")
// 6. Type-safe SQL-like DSL
println("\n6. Type-safe SQL-like DSL:")
@DslMarker
annotation class SqlDslMarker
@SqlDslMarker
class QueryBuilder {
private val columns = mutableListOf()
private var table: String = ""
private val conditions = mutableListOf()
fun select(vararg cols: String) {
columns.addAll(cols)
}
fun from(tableName: String) {
table = tableName
}
infix fun String.eq(value: Any) {
conditions.add("$this = '$value'")
}
infix fun String.gt(value: Any) {
conditions.add("$this > '$value'")
}
fun build(): String {
val cols = if (columns.isEmpty()) "*" else columns.joinToString(", ")
val where = if (conditions.isEmpty()) "" else " WHERE " + conditions.joinToString(" AND ")
return "SELECT $cols FROM $table$where"
}
}
fun query(block: QueryBuilder.() -> Unit): String {
return QueryBuilder().apply(block).build()
}
val sql = query {
select("name", "age", "email")
from("users")
"age" gt 18
"status" eq "active"
}
println("Generated SQL: $sql")
// 7. DSL for testing
println("\n7. DSL for testing:")
class TestCase {
private val assertions = mutableListOf<() -> Boolean>()
private var name: String = ""
fun test(name: String, block: TestCase.() -> Unit) {
this.name = name
block()
runTest()
}
fun expect(condition: Boolean, message: String = "") {
assertions.add {
if (!condition) println("FAIL: $message")
condition
}
}
infix fun T.shouldBe(expected: T) {
assertions.add {
val passed = this == expected
if (!passed) println("FAIL: $this should be $expected")
passed
}
}
infix fun T.shouldNotBe(expected: T) {
assertions.add {
val passed = this != expected
if (!passed) println("FAIL: $this should not be $expected")
passed
}
}
private fun runTest() {
println("\nRunning test: $name")
val results = assertions.map { it() }
val passed = results.count { it }
val total = results.size
println("Result: $passed/$total passed")
}
}
fun testCase(block: TestCase.() -> Unit) {
TestCase().apply(block)
}
testCase {
test("Basic assertions") {
2 + 2 shouldBe 4
"hello".length shouldBe 5
true shouldNotBe false
expect(1 < 2, "1 should be less than 2")
}
test("String tests") {
"Kotlin".startsWith("K") shouldBe true
"".isEmpty() shouldBe true
}
}
// 8. DSL for dependency injection
println("\n8. DSL for dependency injection:")
interface Service
class DatabaseService : Service
class ApiService : Service
class LoggerService : Service
class Container {
private val services = mutableMapOf Service>()
private val instances = mutableMapOf()
fun register(name: String, creator: () -> Service) {
services[name] = creator
}
fun resolve(name: String): T {
return instances.getOrPut(name) {
services[name]?.invoke() ?: throw IllegalArgumentException("Service $name not found")
} as T
}
}
fun container(block: Container.() -> Unit): Container {
return Container().apply(block)
}
val diContainer = container {
register("database") { DatabaseService() }
register("api") { ApiService() }
register("logger") { LoggerService() }
}
val dbService: DatabaseService = diContainer.resolve("database")
val apiService: ApiService = diContainer.resolve("api")
println("Resolved services: ${dbService::class.simpleName}, ${apiService::class.simpleName}")
// 9. DSL for configuration files
println("\n9. DSL for configuration files:")
data class ServerConfig(
var host: String = "localhost",
var port: Int = 8080,
var ssl: Boolean = false,
var timeouts: TimeoutConfig = TimeoutConfig()
)
data class TimeoutConfig(
var read: Int = 30,
var write: Int = 30,
var connect: Int = 10
)
fun serverConfig(block: ServerConfig.() -> Unit): ServerConfig {
return ServerConfig().apply(block)
}
fun ServerConfig.timeouts(block: TimeoutConfig.() -> Unit) {
timeouts = TimeoutConfig().apply(block)
}
val server = serverConfig {
host = "api.example.com"
port = 443
ssl = true
timeouts {
read = 60
write = 60
connect = 15
}
}
println("Server config: $server")
// 10. DSL for UI layout (conceptual)
println("\n10. DSL for UI layout (conceptual):")
class View {
var width: Int = 100
var height: Int = 50
var margin: Int = 0
var padding: Int = 0
val children = mutableListOf()
fun view(block: View.() -> Unit) {
children.add(View().apply(block))
}
override fun toString(): String {
return "View(w=$width, h=$height, margin=$margin, padding=$padding, children=${children.size})"
}
}
fun verticalLayout(block: View.() -> Unit): View {
return View().apply(block)
}
val layout = verticalLayout {
width = 300
height = 400
view {
width = 280
height = 40
margin = 10
}
view {
width = 280
height = 200
margin = 10
view {
width = 100
height = 30
padding = 5
}
}
}
println("UI Layout: $layout")
println("\nDSL design demonstration complete!")
}
🤖 Android Development
Android Development with Kotlin represents a paradigm shift from Java, offering more expressive, concise, and safe code. Since Google announced Kotlin as a first-class language for Android in 2017, it has become the preferred choice for new Android projects. Kotlin's null safety, extension functions, coroutines, and DSL capabilities address many pain points in Android development, leading to more robust and maintainable applications.
Kotlin Android Extensions and View Binding
Kotlin Android Extensions (now deprecated) provided synthetic view properties for accessing views without `findViewById()`. Modern Android uses View Binding or Data Binding with Kotlin for type-safe view access. Kotlin's null safety ensures view references are handled safely, preventing common `NullPointerException`s. Extension functions enable adding utility methods to Android classes without inheritance.
Coroutines for Asynchronous Tasks
Coroutines revolutionized async programming on Android, replacing callbacks, AsyncTask, and RxJava for many use cases. With `lifecycleScope` and `viewModelScope`, coroutines automatically follow lifecycle events, preventing memory leaks. `Dispatchers.Main` allows seamless UI updates. Coroutines simplify complex async flows like sequential API calls, parallel processing, and timeout handling.
Android KTX and Jetpack Libraries
Android KTX (Kotlin Extensions) provides Kotlin-friendly extensions for Android framework and Jetpack libraries. These extensions leverage Kotlin features like extension functions, lambdas, and named parameters to make Android APIs more idiomatic. Jetpack libraries like ViewModel, LiveData, Room, and WorkManager have first-class Kotlin support with coroutines and flows.
Architecture Components with Kotlin
Modern Android architecture (MVVM, MVI) pairs well with Kotlin. ViewModels use `viewModelScope` for coroutines. LiveData can be combined with Kotlin flows via `asLiveData()`. Room database uses coroutines for DAO operations. Data Binding works with Kotlin's concise syntax. Kotlin's data classes and sealed classes model state and events cleanly.
Dependency Injection with Kotlin
Dependency injection frameworks like Dagger/Hilt or Koin work seamlessly with Kotlin. Kotlin's concise syntax reduces boilerplate in DI setup. Property delegation (`by inject()`) provides lazy injection. Extension functions can simplify DI configuration. Kotlin's type system helps catch DI errors at compile time rather than runtime.
Testing Android Apps with Kotlin
Kotlin improves Android testing through more expressive test code. Coroutine testing utilities (`TestCoroutineDispatcher`, `runBlockingTest`) enable deterministic async testing. Mocking is easier with Kotlin's default arguments and interfaces. KotlinTest and Spek provide DSL-based testing frameworks. Espresso and UI tests benefit from Kotlin's conciseness.
Performance and Proguard/R8
Kotlin has minimal runtime overhead, and R8/ProGuard optimizes Kotlin code effectively. Inline functions eliminate lambda overhead. Data classes reduce boilerplate without performance cost. Coroutines are lightweight compared to threads. Understanding Kotlin's performance characteristics helps write efficient Android apps.
// Android Development with Kotlin Examples
// Note: These are conceptual examples for Android development patterns
// Real Android code would require Android framework dependencies
fun main() {
println("=== Android Development with Kotlin ===")
// 1. Android lifecycle-aware coroutines
println("\n1. Lifecycle-aware coroutines:")
// In a ViewModel:
class UserViewModel : ViewModel() {
private val _user = MutableLiveData
()
val user: LiveData = _user
fun loadUser(userId: String) {
viewModelScope.launch {
try {
val user = repository.getUser(userId)
_user.value = user
} catch (e: Exception) {
// Handle error
}
}
}
}
// In an Activity/Fragment:
class UserActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Automatic lifecycle cancellation
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.user.collect { user ->
updateUI(user)
}
}
}
}
}
// 2. Kotlin extensions for Android
println("\n2. Kotlin extensions for Android:")
// View extensions
fun View.show() {
visibility = View.VISIBLE
}
fun View.hide() {
visibility = View.GONE
}
fun View.disable() {
isEnabled = false
alpha = 0.5f
}
fun View.enable() {
isEnabled = true
alpha = 1.0f
}
// Context extensions
fun Context.toast(message: String, duration: Int = Toast.LENGTH_SHORT) {
Toast.makeText(this, message, duration).show()
}
fun Context.getColorCompat(@ColorRes colorRes: Int): Int {
return ContextCompat.getColor(this, colorRes)
}
// 3. Android KTX examples
println("\n3. Android KTX examples:")
// SharedPreferences KTX
val sharedPrefs = context.getSharedPreferences("app", Context.MODE_PRIVATE)
// Old way:
// sharedPrefs.edit().putString("key", "value").apply()
// KTX way:
sharedPrefs.edit {
putString("key", "value")
putInt("count", 42)
putBoolean("flag", true)
}
// Bundle KTX
val bundle = bundleOf(
"id" to 123,
"name" to "Kotlin",
"active" to true
)
// Fragment KTX
class MyFragment : Fragment() {
private val viewModel: UserViewModel by viewModels()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// Fragment viewLifecycleOwner
viewLifecycleOwner.lifecycleScope.launch {
viewModel.user.collect { user ->
// Update UI
}
}
}
}
// 4. Room database with Kotlin
println("\n4. Room database with Kotlin:")
@Entity
data class User(
@PrimaryKey val id: Int,
val name: String,
val age: Int,
val email: String
)
@Dao
interface UserDao {
@Query("SELECT * FROM user")
fun getAll(): Flow>
@Query("SELECT * FROM user WHERE id = :id")
suspend fun getById(id: Int): User?
@Insert
suspend fun insert(user: User)
@Update
suspend fun update(user: User)
@Delete
suspend fun delete(user: User)
@Query("DELETE FROM user")
suspend fun deleteAll()
}
@Database(entities = [User::class], version = 1)
abstract class AppDatabase : RoomDatabase() {
abstract fun userDao(): UserDao
companion object {
@Volatile
private var INSTANCE: AppDatabase? = null
fun getInstance(context: Context): AppDatabase {
return INSTANCE ?: synchronized(this) {
INSTANCE ?: buildDatabase(context).also { INSTANCE = it }
}
}
private fun buildDatabase(context: Context): AppDatabase {
return Room.databaseBuilder(
context,
AppDatabase::class.java,
"app.db"
).build()
}
}
}
// 5. Retrofit with coroutines
println("\n5. Retrofit with coroutines:")
data class ApiUser(
val id: Int,
val name: String,
val email: String
)
interface UserApiService {
@GET("users/{id}")
suspend fun getUser(@Path("id") id: Int): ApiUser
@GET("users")
suspend fun getUsers(): List
@POST("users")
suspend fun createUser(@Body user: ApiUser): ApiUser
@Multipart
@POST("upload")
suspend fun uploadImage(@Part image: MultipartBody.Part): ResponseBody
}
class UserRepository(private val api: UserApiService) {
suspend fun getUser(id: Int): User {
return try {
val apiUser = api.getUser(id)
// Convert API model to domain model
User(apiUser.id, apiUser.name, 0, apiUser.email)
} catch (e: Exception) {
throw UserNotFoundException(id)
}
}
suspend fun getUsers(): List {
return api.getUsers().map { apiUser ->
User(apiUser.id, apiUser.name, 0, apiUser.email)
}
}
}
class UserNotFoundException(id: Int) : Exception("User $id not found")
// 6. WorkManager with coroutines
println("\n6. WorkManager with coroutines:")
class UploadWorker(
context: Context,
params: WorkerParameters
) : CoroutineWorker(context, params) {
override suspend fun doWork(): Result {
return try {
// Upload data
val data = inputData.getString("data")
println("Uploading: $data")
delay(5000) // Simulate upload
Result.success()
} catch (e: Exception) {
Result.retry()
}
}
}
// Schedule work
fun scheduleUpload(context: Context, data: String) {
val workRequest = OneTimeWorkRequestBuilder()
.setInputData(workDataOf("data" to data))
.setConstraints(
Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.build()
)
.build()
WorkManager.getInstance(context).enqueue(workRequest)
}
// 7. View Binding with Kotlin
println("\n7. View Binding with Kotlin:")
// activity_main.xml -> ActivityMainBinding
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
// Type-safe view access
binding.textView.text = "Hello Kotlin!"
binding.button.setOnClickListener {
binding.textView.text = "Button clicked!"
}
// Extension for simpler binding
binding.apply {
textView.text = "Using apply"
button.isEnabled = true
progressBar.visibility = View.VISIBLE
}
}
}
// 8. Dependency Injection with Koin
println("\n8. Dependency Injection with Koin:")
// Module definitions
val appModule = module {
single { AppDatabase.getInstance(get()) }
single { get().userDao() }
single { UserRepositoryImpl(get()) }
viewModel { UserViewModel(get()) }
}
// Repository implementation
class UserRepositoryImpl(private val dao: UserDao) : UserRepository {
override suspend fun getUsers(): List = dao.getAll()
override suspend fun getUser(id: Int): User? = dao.getById(id)
}
// ViewModel with injected repository
class UserViewModel(private val repository: UserRepository) : ViewModel() {
val users: LiveData> = repository.getUsers().asLiveData()
fun loadUser(id: Int) = viewModelScope.launch {
val user = repository.getUser(id)
// Process user
}
}
// 9. Testing with Kotlin
println("\n9. Testing with Kotlin:")
@RunWith(AndroidJUnit4::class)
class UserViewModelTest {
private lateinit var viewModel: UserViewModel
private lateinit var repository: FakeUserRepository
@get:Rule
val instantTaskExecutorRule = InstantTaskExecutorRule()
@Before
fun setup() {
repository = FakeUserRepository()
viewModel = UserViewModel(repository)
}
@Test
fun `loadUsers should emit users`() = runBlockingTest {
// Given
val testUsers = listOf(
User(1, "Alice", 25, "alice@example.com"),
User(2, "Bob", 30, "bob@example.com")
)
repository.addUsers(testUsers)
// When
val result = viewModel.users.getOrAwaitValue()
// Then
assertEquals(2, result.size)
assertEquals("Alice", result[0].name)
}
@Test
fun `loadUser should return user`() = runBlockingTest {
// Given
val testUser = User(1, "Test", 25, "test@example.com")
repository.addUser(testUser)
// When
viewModel.loadUser(1)
// Then - verify repository was called
// (implementation depends on how ViewModel exposes results)
}
}
class FakeUserRepository : UserRepository {
private val users = mutableListOf()
override suspend fun getUsers(): List = users
override suspend fun getUser(id: Int): User? {
return users.find { it.id == id }
}
fun addUser(user: User) {
users.add(user)
}
fun addUsers(userList: List) {
users.addAll(userList)
}
}
// 10. Modern Android architecture with Kotlin
println("\n10. Modern Android architecture:")
// State representation
sealed class UserState {
object Loading : UserState()
data class Success(val users: List) : UserState()
data class Error(val message: String) : UserState()
}
// Events
sealed class UserEvent {
object LoadUsers : UserEvent()
data class SelectUser(val id: Int) : UserEvent()
object Refresh : UserEvent()
}
// ViewModel with StateFlow
class ModernUserViewModel(private val repository: UserRepository) : ViewModel() {
private val _state = MutableStateFlow(UserState.Loading)
val state: StateFlow = _state
private val _events = MutableSharedFlow()
val events: Flow = _events
init {
viewModelScope.launch {
_events.collect { event ->
when (event) {
is UserEvent.LoadUsers -> loadUsers()
is UserEvent.SelectUser -> selectUser(event.id)
UserEvent.Refresh -> refresh()
}
}
}
}
private suspend fun loadUsers() {
_state.value = UserState.Loading
try {
val users = repository.getUsers()
_state.value = UserState.Success(users)
} catch (e: Exception) {
_state.value = UserState.Error(e.message ?: "Unknown error")
}
}
private fun selectUser(id: Int) {
// Handle user selection
println("User $id selected")
}
private fun refresh() {
viewModelScope.launch {
loadUsers()
}
}
}
// 11. Compose with Kotlin (declarative UI)
println("\n11. Jetpack Compose with Kotlin:")
/*
@Composable
fun UserList(users: List, onUserClick: (User) -> Unit) {
LazyColumn {
items(users) { user ->
UserCard(user = user, onUserClick = onUserClick)
}
}
}
@Composable
fun UserCard(user: User, onUserClick: (User) -> Unit) {
Card(
modifier = Modifier
.fillMaxWidth()
.padding(8.dp)
.clickable { onUserClick(user) },
elevation = 4.dp
) {
Column(modifier = Modifier.padding(16.dp)) {
Text(
text = user.name,
style = MaterialTheme.typography.h6
)
Spacer(modifier = Modifier.height(4.dp))
Text(
text = user.email,
style = MaterialTheme.typography.body2
)
}
}
}
@Composable
fun UserScreen(viewModel: UserViewModel = viewModel()) {
val userState by viewModel.state.collectAsState()
when (val state = userState) {
is UserState.Loading -> {
Box(modifier = Modifier.fillMaxSize()) {
CircularProgressIndicator(modifier = Modifier.align(Alignment.Center))
}
}
is UserState.Success -> {
UserList(users = state.users) { user ->
// Handle click
}
}
is UserState.Error -> {
Text(text = "Error: ${state.message}")
}
}
}
*/
println("\nAndroid development with Kotlin demonstration complete!")
}
// Stub classes for Android examples
open class ViewModel {
val viewModelScope = MainScope()
}
class MutableLiveData {
var value: T? = null
}
typealias LiveData = MutableLiveData
class MainScope
class AppCompatActivity {
val lifecycleScope = MainScope()
}
class Bundle
class Context
class View {
var visibility: Int = 0
var isEnabled: Boolean = true
var alpha: Float = 1.0f
}
object View {
const val VISIBLE = 0
const val GONE = 1
}
class Toast {
companion object {
fun makeText(context: Context, message: String, duration: Int): Toast = Toast()
const val LENGTH_SHORT = 0
}
fun show() {}
}
object ContextCompat {
fun getColor(context: Context, colorRes: Int): Int = 0
}
class Room {
companion object {
fun databaseBuilder(context: Context, klass: Class<*>, name: String): Builder = Builder()
}
class Builder {
fun build(): Any = Any()
}
}
object RoomDatabase
annotation class Entity
annotation class PrimaryKey
annotation class Dao
annotation class Query(val value: String)
annotation class Insert
annotation class Update
annotation class Delete
annotation class Database(val entities: Array>, val version: Int)
annotation class Volatile
annotation class ColorRes
annotation class GET(val value: String)
annotation class POST(val value: String)
annotation class Path(val value: String)
annotation class Body
annotation class Multipart
annotation class Part
class MultipartBody {
class Part
}
class ResponseBody
class WorkerParameters
class CoroutineWorker(context: Context, params: WorkerParameters)
class Result {
companion object {
fun success(): Result = Result()
fun retry(): Result = Result()
}
}
class InputData {
fun getString(key: String): String? = null
}
class WorkManager {
companion object {
fun getInstance(context: Context): WorkManager = WorkManager()
}
fun enqueue(workRequest: Any) {}
}
class OneTimeWorkRequestBuilder {
fun setInputData(data: Any): OneTimeWorkRequestBuilder = this
fun setConstraints(constraints: Any): OneTimeWorkRequestBuilder = this
fun build(): Any = Any()
}
fun workDataOf(vararg pairs: Pair): Any = Any()
class Constraints {
class Builder {
fun setRequiredNetworkType(type: Any): Builder = this
fun build(): Constraints = Constraints()
}
}
object NetworkType {
const val CONNECTED = 0
}
class ActivityMainBinding {
companion object {
fun inflate(layoutInflater: Any): ActivityMainBinding = ActivityMainBinding()
}
val root: Any = Any()
val textView = TextView()
val button = Button()
val progressBar = ProgressBar()
}
class TextView {
var text: String = ""
}
class Button {
fun setOnClickListener(listener: () -> Unit) {}
var isEnabled: Boolean = true
}
class ProgressBar {
var visibility: Int = 0
}
class LayoutInflater
object module {
fun single(creator: () -> Any) {}
fun viewModel(creator: () -> Any) {}
}
inline fun viewModels(): Lazy = lazy { T::class.java.newInstance() as T }
class Fragment {
val viewLifecycleOwner = ViewLifecycleOwner()
}
class ViewLifecycleOwner {
val lifecycleScope = MainScope()
}
object Lifecycle {
enum class State {
STARTED
}
}
suspend fun repeatOnLifecycle(state: Lifecycle.State, block: suspend () -> Unit) {}
annotation class RunWith(val value: Class<*>)
object AndroidJUnit4::class
annotation class Before
annotation class Test
annotation class Rule
annotation class get:Rule
class InstantTaskExecutorRule
fun LiveData.getOrAwaitValue(): T = value as T
🌐 Kotlin Multiplatform
Kotlin Multiplatform (KMP) allows sharing code between different platforms while accessing platform-specific APIs when needed. It enables writing business logic, data models, and other non-UI code once and reusing it across Android, iOS, web, desktop, and server applications. KMP is not a write-once-run-anywhere solution but a share-what-makes-sense approach that respects platform differences while maximizing code reuse.
Shared Common Code
The core of KMP is the common module containing platform-agnostic code written in pure Kotlin. This code can define expect declarations for platform-specific APIs. Common code can use a subset of Kotlin standard library and multiplatform libraries. It's compiled to platform-specific binaries (JVM bytecode, JavaScript, or native code) for each target platform.
Expect/Actual Mechanism
The expect/actual pattern bridges platform differences: `expect` declarations in common code specify required API contracts, while `actual` implementations in platform-specific modules provide platform-specific implementations. This works for functions, properties, classes, and objects. The compiler ensures expect and actual declarations match, providing type safety across platforms.
Platform-Specific Implementations
For each target platform (iOS, Android, JS, etc.), platform-specific modules provide actual implementations of expected declarations. These modules can access full platform APIs (Android SDK, iOS frameworks, browser APIs, etc.). Platform modules depend on the common module and can also contain platform-specific code that doesn't need sharing.
Multiplatform Libraries
Kotlin Multiplatform has a growing ecosystem of libraries: Ktor for networking, Serialization for JSON, Coroutines for async, DateTime for dates, SQLDelight for databases, and more. These libraries provide common APIs with platform-specific implementations. You can also create your own multiplatform libraries to share across projects.
iOS Integration with Kotlin/Native
Kotlin/Native compiles Kotlin to native binaries for iOS (and other platforms). It provides interoperability with Objective-C and Swift through automatic framework generation. Kotlin classes are exposed as Objective-C/Swift classes, and vice versa. Memory management uses automatic reference counting (ARC) compatible with iOS's memory model.
Web Integration with Kotlin/JS
Kotlin/JS compiles Kotlin to JavaScript for web applications. It can interoperate with existing JavaScript libraries through external declarations. Kotlin/JS supports both browser and Node.js targets. For UI, you can use React bindings, Compose for Web, or interoperate with any JavaScript framework.
Build Configuration and Gradle
KMP uses Gradle with the Kotlin Multiplatform plugin. Build files define targets (ios, android, js, jvm) and source sets (commonMain, iosMain, androidMain, etc.). Dependencies can be common (shared) or platform-specific. The build produces platform-specific artifacts (Android AAR, iOS framework, JS bundle, JAR).
// Kotlin Multiplatform Examples
// Note: Multiplatform code is organized across multiple files/modules
// This is a conceptual demonstration
fun main() {
println("=== Kotlin Multiplatform (KMP) ===")
// 1. Common module structure
println("\n1. Common module structure:")
/*
// In commonMain sources:
// Shared data models
data class User(
val id: String,
val name: String,
val email: String
)
data class ApiResponse(
val success: Boolean,
val data: T?,
val error: String? = null
)
// Shared business logic
class UserValidator {
fun validateEmail(email: String): Boolean {
return email.contains("@")
}
fun validateName(name: String): Boolean {
return name.length in 2..50
}
}
// Shared interfaces
interface UserRepository {
suspend fun getUsers(): List
suspend fun getUser(id: String): User?
suspend fun saveUser(user: User): Boolean
}
// Shared use cases
class GetUserUseCase(private val repository: UserRepository) {
suspend operator fun invoke(id: String): User? {
return repository.getUser(id)
}
}
*/
// 2. Expect/Actual pattern
println("\n2. Expect/Actual pattern:")
/*
// In commonMain:
expect class Platform() {
val platform: String
fun getCurrentTime(): Long
}
// In androidMain:
actual class Platform actual constructor() {
actual val platform: String = "Android ${android.os.Build.VERSION.SDK_INT}"
actual fun getCurrentTime(): Long {
return System.currentTimeMillis()
}
}
// In iosMain:
actual class Platform actual constructor() {
actual val platform: String = "iOS"
actual fun getCurrentTime(): Long {
return platform.posix.gettimeofday().use { tv ->
tv.tv_sec * 1000L + tv.tv_usec / 1000L
}
}
}
// In jsMain:
actual class Platform actual constructor() {
actual val platform: String = "JS"
actual fun getCurrentTime(): Long {
return kotlin.js.Date.now().toLong()
}
}
// Usage in common code:
fun printPlatformInfo() {
val platform = Platform()
println("Running on: ${platform.platform}")
println("Current time: ${platform.getCurrentTime()}")
}
*/
// 3. Sharing network logic with Ktor
println("\n3. Sharing network logic:")
/*
// In commonMain:
import io.ktor.client.*
import io.ktor.client.call.*
import io.ktor.client.request.*
class ApiClient(private val client: HttpClient) {
suspend fun getUsers(): List {
return client.get("https://api.example.com/users").body()
}
suspend fun getUser(id: String): User {
return client.get("https://api.example.com/users/$id").body()
}
}
// In build.gradle.kts (common):
// kotlin {
// sourceSets {
// val commonMain by getting {
// dependencies {
// implementation("io.ktor:ktor-client-core:$ktorVersion")
// }
// }
// }
// }
// Platform-specific HTTP engine configuration:
// - Android: OkHttp
// - iOS: NSURLSession
// - JS: Fetch or XMLHttpRequest
// - JVM: Apache or CIO
*/
// 4. Sharing database logic with SQLDelight
println("\n4. Sharing database logic:")
/*
// In commonMain/sqldelight:
// Users.sq:
CREATE TABLE User (
id TEXT NOT NULL PRIMARY KEY,
name TEXT NOT NULL,
email TEXT NOT NULL
);
selectAll:
SELECT * FROM User;
insertUser:
INSERT OR REPLACE INTO User (id, name, email)
VALUES (?, ?, ?);
// Generated Kotlin code is available in commonMain
class UserDataSource(private val database: Database) {
fun getAllUsers(): List {
return database.userQueries.selectAll().executeAsList()
.map { User(it.id, it.name, it.email) }
}
fun insertUser(user: User) {
database.userQueries.insertUser(user.id, user.name, user.email)
}
}
*/
// 5. Sharing serialization with kotlinx.serialization
println("\n5. Sharing serialization:")
/*
// In commonMain:
import kotlinx.serialization.*
import kotlinx.serialization.json.*
@Serializable
data class ApiUser(
val id: String,
val name: String,
val email: String,
@SerialName("avatar_url") val avatarUrl: String?
)
object JsonParser {
private val json = Json {
ignoreUnknownKeys = true
isLenient = true
}
fun parseUser(jsonString: String): ApiUser {
return json.decodeFromString(jsonString)
}
fun toJson(user: ApiUser): String {
return json.encodeToString(user)
}
}
// Same code works on all platforms
*/
// 6. Dependency injection in KMP
println("\n6. Dependency injection in KMP:")
/*
// In commonMain:
expect class PlatformDependencies {
val httpClient: HttpClient
val database: Database
val preferences: Preferences
}
// In common code:
class AppContainer(platformDeps: PlatformDependencies) {
val userRepository: UserRepository by lazy {
UserRepositoryImpl(
apiClient = ApiClient(platformDeps.httpClient),
database = platformDeps.database
)
}
val getUserUseCase: GetUserUseCase by lazy {
GetUserUseCase(userRepository)
}
}
// Platform-specific implementations provide actual dependencies
*/
// 7. ViewModel sharing (with extra libraries)
println("\n7. ViewModel sharing:")
/*
// Using Moko-MVVM or Decompose libraries
// In commonMain:
class SharedViewModel(
private val getUserUseCase: GetUserUseCase
) : ViewModel() {
private val _state = MutableStateFlow(UserState.Loading)
val state: StateFlow = _state
fun loadUser(id: String) {
viewModelScope.launch {
_state.value = UserState.Loading
try {
val user = getUserUseCase(id)
_state.value = UserState.Success(user)
} catch (e: Exception) {
_state.value = UserState.Error(e.message ?: "Unknown error")
}
}
}
}
sealed class UserState {
object Loading : UserState()
data class Success(val user: User?) : UserState()
data class Error(val message: String) : UserState()
}
// Android: Use directly with Android ViewModel
// iOS: Bind to SwiftUI/UIKit via library bindings
*/
// 8. Testing shared code
println("\n8. Testing shared code:")
/*
// In commonTest:
class UserValidatorTest {
private val validator = UserValidator()
@Test
fun testValidEmail() {
assertTrue(validator.validateEmail("test@example.com"))
assertFalse(validator.validateEmail("invalid-email"))
}
@Test
fun testValidName() {
assertTrue(validator.validateName("John"))
assertFalse(validator.validateName("A")) // Too short
assertFalse(validator.validateName("A".repeat(51))) // Too long
}
}
// Tests run on all platforms or just JVM
*/
// 9. Building for multiple platforms
println("\n9. Build configuration example:")
/*
// build.gradle.kts:
plugins {
kotlin("multiplatform") version "1.8.0"
}
kotlin {
android {
compileSdk = 33
}
ios {
binaries {
framework {
baseName = "Shared"
}
}
}
js(IR) {
browser {
testTask {
useKarma {
useChromeHeadless()
}
}
}
}
jvm()
sourceSets {
val commonMain by getting {
dependencies {
implementation("io.ktor:ktor-client-core:2.2.4")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4")
}
}
val androidMain by getting {
dependencies {
implementation("io.ktor:ktor-client-okhttp:2.2.4")
}
}
val iosMain by getting {
dependencies {
implementation("io.ktor:ktor-client-darwin:2.2.4")
}
}
}
}
*/
// 10. Real-world sharing example
println("\n10. Real-world sharing example:")
/*
// Authentication flow that can be shared:
// In commonMain:
class AuthenticationManager(
private val apiClient: ApiClient,
private val tokenStorage: TokenStorage
) {
suspend fun login(email: String, password: String): AuthResult {
return try {
val response = apiClient.post(
"auth/login",
LoginRequest(email, password)
)
if (response.success) {
tokenStorage.saveToken(response.token)
AuthResult.Success
} else {
AuthResult.Error(response.message ?: "Login failed")
}
} catch (e: Exception) {
AuthResult.Error("Network error: ${e.message}")
}
}
suspend fun logout() {
tokenStorage.clearToken()
}
fun isAuthenticated(): Boolean {
return tokenStorage.getToken() != null
}
}
sealed class AuthResult {
object Success : AuthResult()
data class Error(val message: String) : AuthResult()
}
// Platform-specific TokenStorage implementations:
// Android: SharedPreferences
// iOS: Keychain/UserDefaults
// Web: LocalStorage
*/
// 11. Platform-specific UI with shared logic
println("\n11. Platform-specific UI with shared logic:")
/*
// Shared ViewModel (common):
class LoginViewModel(
private val authManager: AuthenticationManager
) : ViewModel() {
private val _uiState = MutableStateFlow(LoginUiState.Idle)
val uiState: StateFlow = _uiState
fun login(email: String, password: String) {
viewModelScope.launch {
_uiState.value = LoginUiState.Loading
val result = authManager.login(email, password)
_uiState.value = when (result) {
is AuthResult.Success -> LoginUiState.Success
is AuthResult.Error -> LoginUiState.Error(result.message)
}
}
}
}
sealed class LoginUiState {
object Idle : LoginUiState()
object Loading : LoginUiState()
object Success : LoginUiState()
data class Error(val message: String) : LoginUiState()
}
// Android UI (androidMain):
class LoginActivity : AppCompatActivity() {
private val viewModel: LoginViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.uiState.collect { state ->
when (state) {
is LoginUiState.Loading -> showLoading()
is LoginUiState.Success -> navigateToHome()
is LoginUiState.Error -> showError(state.message)
else -> {}
}
}
}
}
}
}
// iOS UI (Swift):
struct LoginView: View {
@StateObject private var viewModel = LoginViewModel()
var body: some View {
VStack {
TextField("Email", text: $email)
SecureField("Password", text: $password)
Button("Login") {
viewModel.login(email: email, password: password)
}
.disabled(viewModel.uiState.isLoading)
if case .error(let message) = viewModel.uiState {
Text(message).foregroundColor(.red)
}
}
}
}
*/
// 12. Code sharing metrics and benefits
println("\n12. Code sharing benefits:")
/*
Typical sharing percentages:
- Business logic: 70-90% shared
- Data models: 100% shared
- Networking: 80-90% shared
- Database: 70-80% shared
- UI: 0% shared (platform-specific)
Benefits:
1. Consistent behavior across platforms
2. Reduced development time
3. Easier maintenance (fix once)
4. Shared tests
5. Team knowledge sharing
6. Faster feature rollout
*/
println("\nKotlin Multiplatform demonstration complete!")
}
// Stub classes for examples
class HttpClient
class Database
class Preferences
class TokenStorage {
fun saveToken(token: String) {}
fun getToken(): String? = null
fun clearToken() {}
}
data class LoginRequest(val email: String, val password: String)
data class LoginResponse(val success: Boolean, val token: String?, val message: String?)
class ViewModel {
val viewModelScope = MainScope()
}
class MutableStateFlow(initialValue: T) {
var value: T = initialValue
}
typealias StateFlow = MutableStateFlow