Kotlin
Kotlin is a modern, statically typed programming language designed to improve developer productivity, code safety, and application development efficiency. Developed by JetBrains, Kotlin offers concise syntax while supporting object-oriented and functional programming approaches. It is fully interoperable with Java and is widely used for Android development, backend applications, and other software projects that benefit from a modern alternative to traditional Java-based development.
This section explores the fundamental features of Kotlin, including variables, data types, functions, classes, objects, null safety, collections, extension functions, and coroutines. It also examines Kotlin’s major strengths, limitations, interoperability with Java, and practical applications, providing a clear foundation for understanding how Kotlin is used to develop reliable and maintainable software.

Introduction To Kotlin
- Complete Kotlin Programming Learning Roadmap
- Introduction
- Detailed Setup and First Application
- 2.1 Prerequisites for Environment Setup
- 2.2 Linux Command-Line Environment
- 2.3 Linux Professional IDE Environment
- 2.4 Linux AI-Integrated Workflow
- 2.5 Windows Command-Line Environment
- 2.6 Windows Professional IDE Environment
- 2.7 Windows AI-Integrated Workflow
- 2.8 macOS Command-Line Environment
- 2.9 macOS Professional IDE Environment
- 2.10 macOS AI-Integrated Workflow
- AI Integration with Development Tools
- Kotlin Fundamentals
- Control Flow
- Functions
- Collections
- Object-Oriented Programming
- Advanced Kotlin
- Concurrency
- File Handling and Serialization
- Testing
- Backend Development
- Android Development
- Kotlin Multiplatform
- Performance Optimization
- Software Architecture
- Production Readiness
- Real-World Projects
- Career Readiness
- Documentation
- Testing
- Deployment
- Contributing
- License
- Author
- Acknowledgments
- Final Advice
Complete Kotlin Programming Learning Roadmap
Introduction
1.1 Kotlin
Kotlin is a modern, statically typed programming language that runs on the Java Virtual Machine (JVM), JavaScript, and native platforms via Kotlin/Native. JetBrains developed it as a concise and expressive alternative to Java, with a strong focus on safety while retaining complete compatibility with existing Java code and libraries. Kotlin reduces boilerplate code dramatically, eliminates null pointer exceptions through null safety, and adds functional programming features.
Key Features of Kotlin:
- Null Safety – Types are non-null by default, eliminating NullPointerException
- Concise Syntax – Less boilerplate code than Java
- Interoperability – Seamless integration with Java libraries
- Functional Programming – Lambda expressions, higher-order functions
- Coroutines – Lightweight concurrency
- Extension Functions – Add new functionality to existing classes
Code Example:
fun main() {
println("Hello, Kotlin!")
}
The fun keyword declares a function. main() is the entry point of every Kotlin program. println() outputs text to the console. Kotlin’s syntax is clean and requires no semicolons at the end of lines.
1.2 History of Kotlin
JetBrains introduced Kotlin in 2011, with its first stable 1.0 release arriving in February 2016. Google officially endorsed Kotlin for Android development in 2017. Since then, the language has continued to develop, introducing capabilities such as coroutines, multiplatform development, and other modern language features. The name Kotlin comes from Kotlin Island, located near St. Petersburg, Russia.
Major Kotlin Versions:
- 2011 – First announced by JetBrains
- 2016 – Version 1.0 released (stable)
- 2017 – Google announces official Android support
- 2018 – Kotlin 1.2 with multi-platform support
- 2019 – Kotlin 1.3 with coroutines
- 2020 – Kotlin 1.4 with improved performance
- 2022 – Kotlin 1.7 with new language features
- 2023 – Kotlin 1.9 with K2 compiler
Code Example:
// Modern Kotlin with coroutines
import kotlinx.coroutines.*
fun main() = runBlocking {
launch {
delay(1000L)
println("World!")
}
println("Hello")
}
The runBlocking function creates a coroutine scope. launch starts a new coroutine. The delay function pauses a coroutine’s execution without blocking the underlying thread, illustrating Kotlin’s approach to lightweight and efficient concurrency.
1.3 Kotlin Ecosystem
The Kotlin ecosystem includes various tools and frameworks for different platforms.
Kotlin Ecosystem Components:
- Kotlin/JVM – For backend and Android development
- Kotlin/JS – Transpiles to JavaScript for frontend
- Kotlin/Native – Compiles to native binaries (iOS, Linux, Windows)
- Kotlin Multiplatform – Shared code across platforms
- Ktor – Asynchronous web framework
- Coroutines – Lightweight concurrency
- Kotlinx.serialization – JSON and other formats
- Exposed – SQL framework
Code Example:
// Using Kotlinx.serialization
import kotlinx.serialization.*
import kotlinx.serialization.json.*
@Serializable
data class Bird(val species: String, val count: Int)
fun main() {
val sparrow = Bird("Sparrow", 10)
val json = Json.encodeToString(sparrow)
println(json) // {"species":"Sparrow","count":10}
val decoded = Json.decodeFromString<Bird>(json)
println("${decoded.species}: ${decoded.count} birds")
}
The @Serializable annotation enables automatic JSON serialization. Json.encodeToString converts an object to JSON. Json.decodeFromString converts JSON back to an object. This demonstrates Kotlin’s ecosystem capabilities.
1.4 JVM Fundamentals
Kotlin/JVM compiles to Java bytecode, which runs on the Java Virtual Machine. The JVM provides garbage collection, bytecode verification, platform independence, and a rich ecosystem of Java libraries. Kotlin code can call Java code and vice versa without any overhead.
JVM Key Concepts:
- Bytecode – Compiled Kotlin becomes Java bytecode
- Garbage Collection – Automatic memory management
- Platform Independence – “Write once, run anywhere”
- JIT Compilation – Just-in-time compilation for performance
- Rich Ecosystem – Access to thousands of Java libraries
Code Example:
// Kotlin calling Java code
fun main() {
// Using Java's ArrayList
val birds = java.util.ArrayList<String>()
birds.add("Sparrow")
birds.add("Eagle")
birds.add("Hawk")
println(birds) // [Sparrow, Eagle, Hawk]
// Using Java's HashMap
val counts = java.util.HashMap<String, Int>()
counts["Sparrow"] = 10
counts["Eagle"] = 3
println(counts) // {Sparrow=10, Eagle=3}
}
Kotlin seamlessly interoperates with Java collections. The java.util.ArrayList and java.util.HashMap are standard Java classes used directly in Kotlin. The compiler generates bytecode that works on any JVM.
1.5 Kotlin vs Java
Kotlin offers many improvements over Java while maintaining full interoperability.
Kotlin vs Java Comparison:
| Feature | Kotlin | Java |
|---|---|---|
| Null Safety | Built-in (types are non-null by default) | Optional (via Optional or annotations) |
| Concurrency | Coroutines (lightweight) | Threads (heavy) |
| Data Classes | data class (auto-generates equals, hashCode, toString) | Need boilerplate |
| Extension Functions | Yes | No |
| Smart Casts | Yes | No |
| String Templates | "Hello $name" | "Hello " + name |
| Type Inference | Yes (var, val) | Limited (var only) |
| Lambda Support | Excellent | Good (since Java 8) |
| Property Access | Direct | Getters/setters |
Code Example:
// Kotlin version
data class Bird(val species: String, val count: Int)
fun main() {
val sparrow = Bird("Sparrow", 10)
val copied = sparrow.copy(count = 15)
println(sparrow) // Bird(species=Sparrow, count=10)
println(copied) // Bird(species=Sparrow, count=15)
// Null safety
var nullableBird: Bird? = null
val name = nullableBird?.species ?: "Unknown"
println(name) // Unknown
}
Kotlin’s data class automatically provides equals(), hashCode(), toString(), and copy(). The ? operator allows null safety with safe calls (?.) and the Elvis operator (?:). This dramatically reduces boilerplate code.
1.6 Kotlin Use Cases
Kotlin is a versatile language used across many domains.
Primary Use Cases:
- Android Development – Most new Android apps use Kotlin
- Backend Microservices – Spring Boot, Ktor, Vert.x
- Serverless Functions – AWS Lambda, Azure Functions
- Desktop Applications – Compose Multiplatform, TornadoFX
- iOS Development – Via Kotlin Multiplatform
- Web Development – Kotlin/JS, React wrappers
- Data Science – Scripting, Kotlin Notebook
- Game Development – LibGDX, KorGE
Code Example:
// Android app example (Compose)
import androidx.compose.material.*
import androidx.compose.runtime.*
@Composable
fun BirdCounter() {
var count by remember { mutableStateOf(0) }
Column {
Text("Birds: $count")
Button(onClick = { count++ }) {
Text("Add Bird")
}
}
}
// Backend example (Ktor)
import io.ktor.application.*
import io.ktor.response.*
import io.ktor.routing.*
fun main() {
embeddedServer(Netty, port = 8080) {
routing {
get("/birds") {
call.respond(mapOf("sparrow" to 10, "eagle" to 3))
}
}
}.start(wait = true)
}
Kotlin works across different platforms with the same syntax. For Android, Jetpack Compose provides a modern UI framework. For backend, Ktor offers a lightweight web server. The same language can be used for full-stack development.
Detailed Setup and First Application
2.1 Prerequisites for Environment Setup
Before starting Kotlin development, ensure your system meets the requirements.
System Requirements:
- OS: Ubuntu 22.04+ / Windows 10/11 / macOS Sonoma+
- RAM: 8 GB minimum (16 GB recommended)
- Storage: 20 GB free space
- Internet: Stable connection for downloads
- Java: OpenJDK 17 or higher
- IDE: IntelliJ IDEA (Community or Ultimate)
- Build Tool: Gradle (optional)
Code Example:
// Test your setup with this program
fun main() {
println("Kotlin environment check")
println("Java version: ${System.getProperty("java.version")}")
println("Kotlin version: ${KotlinVersion.CURRENT}")
}
This program checks your environment by printing the Java version and Kotlin version. System.getProperty("java.version") retrieves the Java runtime version. KotlinVersion.CURRENT returns the Kotlin compiler version.
2.2 Linux Command-Line Environment
Setting up Kotlin on Linux using the terminal.
Step 1: Update Packages
sudo apt update
Step 2: Install Java
sudo apt install openjdk-17-jdk
Step 3: Verify Java Installation
java -version
Step 4: Install Kotlin
sudo apt install kotlin
Step 5: Verify Kotlin Installation
kotlinc -version
Step 6: Create Project Folder
mkdir KotlinProjects
cd KotlinProjects
Step 7: Create Source File
nano HelloWorld.kt
Step 8: Write Kotlin Code
fun main() {
println("Hello, Kotlin!")
}
Step 9: Compile and Create JAR
kotlinc HelloWorld.kt -include-runtime -d HelloWorld.jar
Step 10: Run the Program
java -jar HelloWorld.jar
Expected Output:
Hello, Kotlin!
Code Example:
// More advanced example
import java.util.Scanner
fun main() {
val scanner = Scanner(System.`in`)
print("Enter your name: ")
val name = scanner.nextLine()
println("Hello, $name! Welcome to Kotlin on Linux.")
}
The kotlinc command compiles Kotlin code to Java bytecode. The -include-runtime flag includes the Kotlin runtime in the JAR. The -d flag specifies the output file name. java -jar executes the compiled program.
2.3 Linux Professional IDE Environment
Setting up IntelliJ IDEA on Linux for professional Kotlin development.
Step 1: Install IntelliJ IDEA
sudo snap install intellij-idea-community --classic
Step 2: Launch IntelliJ IDEA
Open from applications menu or run:
intellij-idea-community
Step 3: Create New Project
- Click “New Project”
- Select “Kotlin” on the left
- Choose “JVM” as the project type
- Set project name and location
- Click “Create”
Step 4: Create Kotlin File
- Right-click
srcfolder - Select “New” → “Kotlin Class/File”
- Name the file
Main - Choose “File” type
Step 5: Write Code
fun main() {
println("Hello, IntelliJ IDEA!")
}
Step 6: Run the Program
- Click the green “Run” button
- Or press
Shift + F10 - Output appears in the console
Code Example:
// Using IntelliJ features
fun main() {
val birds = listOf("Sparrow", "Eagle", "Hawk", "Cardinal")
// Use code completion to explore functions
val flyingBirds = birds.filter { it.length > 5 }
.map { it.uppercase() }
.sorted()
println("Birds that can fly: $flyingBirds")
// Try refactoring with Ctrl+Alt+V
val birdCount = birds.count { it.startsWith("S") }
println("Birds starting with 'S': $birdCount")
}
IntelliJ IDEA provides code completion, refactoring, debugging, and syntax highlighting. The IDE compiles code automatically when you run it. The built-in Kotlin plugin supports all language features.
2.4 Linux AI-Integrated Workflow
Integrating AI tools like GitHub Copilot with IntelliJ on Linux.
Step 1: Install GitHub Copilot
- Open IntelliJ IDEA
- Go to File → Settings → Plugins
- Search for “GitHub Copilot”
- Click “Install”
- Restart the IDE
Step 2: Sign In
- Go to Tools → GitHub Copilot → Login to GitHub
- Follow the browser prompts
Step 3: Use Copilot
Start typing code and Copilot will suggest completions.
Code Example:
// Start typing and Copilot will suggest
fun main() {
// type: "Read a number and check if it's even"
// Copilot suggests:
print("Enter a number: ")
val number = readln().toInt()
if (number % 2 == 0) {
println("$number is even")
} else {
println("$number is odd")
}
}
AI Integration Example:
// Ask AI: "Write a function to count bird occurrences in a list"
// AI generates:
fun countBirdOccurrences(birds: List<String>): Map<String, Int> {
return birds.groupingBy { it }.eachCount()
}
fun main() {
val birdList = listOf("Sparrow", "Eagle", "Hawk", "Sparrow", "Eagle", "Sparrow")
val counts = countBirdOccurrences(birdList)
println("Bird counts: $counts")
// Output: Bird counts: {Sparrow=3, Eagle=2, Hawk=1}
}
AI tools analyze your code context and suggest completions. They can generate entire functions, write documentation, and refactor code. readln() reads user input. groupingBy groups elements by a key. eachCount() counts occurrences in each group.
2.5 Windows Command-Line Environment
Setting up Kotlin on Windows using PowerShell.
Step 1: Install Kotlin (as Administrator)
winget install JetBrains.Kotlin
Step 2: Verify Kotlin Installation
kotlinc -version
Step 3: Install Java (if not present)
winget install Microsoft.OpenJDK.17
Step 4: Verify Java
java -version
Step 5: Create Project Folder
mkdir KotlinProjects
cd KotlinProjects
Step 6: Create Source File
notepad HelloWorld.kt
Step 7: Write Kotlin Code
fun main() {
println("Hello, Kotlin on Windows!")
}
Step 8: Compile
kotlinc HelloWorld.kt -include-runtime -d HelloWorld.jar
Step 9: Run
java -jar HelloWorld.jar
Code Example:
// Windows-specific example with file operations
import java.io.File
fun main() {
val userHome = System.getProperty("user.home")
println("User home directory: $userHome")
val currentDir = File(".")
val files = currentDir.listFiles()
if (files != null) {
println("Files in current directory:")
files.forEach { println(" ${it.name}") }
}
}
winget is Windows’ package manager. kotlinc compiles Kotlin code. System.getProperty("user.home") gets the user’s home directory on Windows. File(".") represents the current directory. listFiles() returns an array of files.
2.6 Windows Professional IDE Environment
Setting up IntelliJ IDEA on Windows.
Step 1: Download IntelliJ IDEA
- Go to https://www.jetbrains.com/idea/
- Click “Download” for Community Edition
- Run the installer
Step 2: Install IntelliJ
- Follow installation wizard
- Select components (64-bit launcher, .kt file association)
- Click “Install”
Step 3: Launch IntelliJ
Open from Start Menu or desktop shortcut
Step 4: Create New Project
- Click “New Project”
- Select “Kotlin” and “JVM”
- Name: “BirdTracker”
- Click “Create”
Step 5: Create Kotlin File
Right-click src → New → Kotlin Class/File → Name: Main
Step 6: Write Code
fun main() {
println("Bird Tracker App")
val birds = mutableListOf<String>()
repeat(3) {
print("Enter bird name: ")
val name = readln()
birds.add(name)
}
println("Birds tracked: ${birds.sorted()}")
}
Step 7: Run
Click green play button or Shift + F10
Code Example:
// More complex example with data class
data class Bird(val species: String, val count: Int, val weight: Double)
class BirdTracker {
private val birds = mutableListOf<Bird>()
fun addBird(bird: Bird) {
birds.add(bird)
println("Added: ${bird.species}")
}
fun getHeaviestBird(): Bird? {
return birds.maxByOrNull { it.weight }
}
fun displayAll() {
birds.forEach { println("${it.species}: ${it.count} birds, ${it.weight}g") }
}
}
fun main() {
val tracker = BirdTracker()
tracker.addBird(Bird("Sparrow", 10, 25.5))
tracker.addBird(Bird("Eagle", 3, 4500.0))
tracker.addBird(Bird("Hawk", 5, 1200.0))
println("All birds:")
tracker.displayAll()
val heaviest = tracker.getHeaviestBird()
println("Heaviest bird: ${heaviest?.species} (${heaviest?.weight}g)")
}
IntelliJ IDEA provides full project management. The data class automatically generates useful methods. mutableListOf creates a mutable list. maxByOrNull finds the element with the maximum value. The IDE handles compilation and runtime automatically.
2.7 Windows AI-Integrated Workflow
Using AI tools in IntelliJ on Windows.
Step 1: Install GitHub Copilot
- File → Settings → Plugins
- Search “GitHub Copilot”
- Install and restart
Step 2: Sign In
Tools → GitHub Copilot → Login
Step 3: Use AI Features
Start typing and Copilot will suggest code.
Code Example:
// Demonstrating val vs var with AI assistance
fun main() {
// Type: "val is immutable, var is mutable"
val species = "Sparrow" // Cannot be reassigned
var count = 10 // Can be reassigned
count = 15 // OK: var can be reassigned
// species = "Eagle" // ERROR: val cannot be reassigned
println("Species: $species, Count: $count")
// AI suggests: "Use a data class for Bird"
data class Bird(val name: String, var population: Int)
val sparrow = Bird("Sparrow", 10)
sparrow.population = 15 // OK: var property
// sparrow.name = "Eagle" // ERROR: val property
println("${sparrow.name}: ${sparrow.population} birds")
}
AI Integration Example:
// Ask AI: "Generate a function to filter birds by weight"
// AI generates:
fun filterBirdsByWeight(
birds: List<Bird>,
minWeight: Double,
maxWeight: Double
): List<Bird> {
return birds.filter { it.weight in minWeight..maxWeight }
}
// Ask AI: "Generate a function to sort birds by count"
fun sortBirdsByCount(birds: List<Bird>): List<Bird> {
return birds.sortedByDescending { it.count }
}
data class Bird(val species: String, val count: Int, val weight: Double)
fun main() {
val birds = listOf(
Bird("Sparrow", 10, 25.5),
Bird("Eagle", 3, 4500.0),
Bird("Hawk", 5, 1200.0),
Bird("Cardinal", 8, 45.0)
)
println("Birds sorted by count:")
sortBirdsByCount(birds).forEach { println(" ${it.species}: ${it.count}") }
println("\nMedium birds (100-1000g):")
filterBirdsByWeight(birds, 100.0, 1000.0).forEach { println(" ${it.species}: ${it.weight}g") }
}
AI tools understand Kotlin idioms and suggest code accordingly. The filter function creates a new list with elements matching the predicate. sortedByDescending sorts in descending order. The in operator checks if a value is within a range.
2.8 macOS Command-Line Environment
Setting up Kotlin on macOS using Homebrew.
Step 1: Install Homebrew (if not installed)
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
Step 2: Install Kotlin
brew install kotlin
Step 3: Verify Kotlin Installation
kotlinc -version
Step 4: Install Java (OpenJDK 17)
brew install openjdk@17
Step 5: Set Up Java Path
echo 'export PATH="/opt/homebrew/opt/openjdk@17/bin:$PATH"' >> ~/.zshrc
source ~/.zshrc
Step 6: Create Project Folder
mkdir ~/KotlinProjects
cd ~/KotlinProjects
Step 7: Create Source File
echo 'fun main() { println("Hello, macOS!") }' > HelloWorld.kt
Step 8: Compile and Run
kotlinc HelloWorld.kt -include-runtime -d HelloWorld.jar
java -jar HelloWorld.jar
Code Example:
// macOS-specific example with system information
import java.io.File
fun main() {
println("System Properties:")
println("OS Name: ${System.getProperty("os.name")}")
println("OS Version: ${System.getProperty("os.version")}")
println("User Home: ${System.getProperty("user.home")}")
// Get macOS version
val versionFile = File("/System/Library/CoreServices/SystemVersion.plist")
if (versionFile.exists()) {
println("System: ${versionFile.readText().substring(0, 100)}...")
}
}
Homebrew is the package manager for macOS. brew install downloads and installs software. System.getProperty retrieves system information. File reads files from the filesystem. readText() reads the entire file content.
2.9 macOS Professional IDE Environment
Setting up IntelliJ IDEA on macOS.
Step 1: Install IntelliJ IDEA
brew install --cask intellij-idea
Step 2: Launch IntelliJ
Open from Applications folder
Step 3: Create New Project
- Click “New Project”
- Select “Kotlin” and “JVM”
- Set project name and location
- Click “Create”
Step 4: Create Kotlin File
- Right-click
src - New → Kotlin Class/File
- Name:
BirdManager
Step 5: Write Code
import kotlin.collections.groupingBy
fun main() {
println("Bird Manager Application")
val birdObservations = listOf(
"Eagle", "Sparrow", "Hawk", "Eagle",
"Sparrow", "Cardinal", "Eagle", "Hawk"
)
val birdCounts = birdObservations
.groupingBy { it }
.eachCount()
println("Bird Sightings:")
birdCounts.forEach { (species, count) ->
println(" $species: $count sightings")
}
val mostFrequent = birdCounts.maxByOrNull { it.value }
println("Most sighted bird: ${mostFrequent?.key} (${mostFrequent?.value})")
}
Step 6: Run
Click green triangle or Control + R
Code Example:
// More advanced example with classes
data class BirdSighting(
val species: String,
val location: String,
val timestamp: Long = System.currentTimeMillis()
)
class BirdWatch {
private val sightings = mutableListOf<BirdSighting>()
fun recordSighting(sighting: BirdSighting) {
sightings.add(sighting)
println("Recorded: ${sighting.species} at ${sighting.location}")
}
fun getSightingsBySpecies(species: String): List<BirdSighting> {
return sightings.filter { it.species == species }
}
fun getUniqueSpecies(): Set<String> {
return sightings.map { it.species }.toSet()
}
fun getSightingCount(): Int = sightings.size
}
fun main() {
val birdWatch = BirdWatch()
birdWatch.recordSighting(BirdSighting("Eagle", "Mountains"))
birdWatch.recordSighting(BirdSighting("Sparrow", "Park"))
birdWatch.recordSighting(BirdSighting("Eagle", "Forest"))
birdWatch.recordSighting(BirdSighting("Hawk", "Mountains"))
println("\nSightings by Eagle:")
birdWatch.getSightingsBySpecies("Eagle").forEach {
println(" ${it.species} at ${it.location}")
}
println("\nUnique species:")
birdWatch.getUniqueSpecies().forEach { println(" $it") }
println("\nTotal sightings: ${birdWatch.getSightingCount()}")
}
IntelliJ IDEA on macOS provides a native development experience. data class automatically generates equals(), hashCode(), toString(), and copy(). mutableListOf creates a mutable list. filter creates a new list with matching elements. map transforms each element. toSet() converts to a set (unique elements).
2.10 macOS AI-Integrated Workflow
Integrating AI tools in IntelliJ on macOS.
Step 1: Install GitHub Copilot
- IntelliJ IDEA → Preferences → Plugins
- Search “GitHub Copilot”
- Install and restart
Step 2: Sign In
Tools → GitHub Copilot → Login
Step 3: Use AI Features
Start typing and Copilot suggests completions.
Code Example:
// AI-assisted functional programming
// Type: "Sum of even numbers from list"
// Copilot generates:
fun sumOfEvenNumbers(numbers: List<Int>): Int {
return numbers.filter { it % 2 == 0 }.sum()
}
fun main() {
val birdCounts = listOf(10, 3, 5, 8, 12, 6)
val evenTotal = sumOfEvenNumbers(birdCounts)
println("Sum of even counts: $evenTotal")
// AI suggests functional transformations
val processed = birdCounts
.filter { it > 5 }
.map { it * 2 }
.sortedDescending()
println("Processed: $processed")
}
AI Integration Example:
// Ask AI: "Write a function to find birds with counts above threshold"
// AI generates:
fun findBirdsAboveThreshold(
birds: Map<String, Int>,
threshold: Int
): Map<String, Int> {
return birds.filter { it.value > threshold }
}
fun main() {
val birdMap = mapOf(
"Sparrow" to 10,
"Eagle" to 3,
"Hawk" to 5,
"Cardinal" to 8,
"Finch" to 12
)
println("Birds with more than 5 sightings:")
val aboveFive = findBirdsAboveThreshold(birdMap, 5)
aboveFive.forEach { (species, count) ->
println(" $species: $count")
}
}
AI tools understand functional programming patterns. filter creates a new map with entries matching the predicate. mapOf creates an immutable map. The forEach iterates over entries, destructuring the key-value pair.
AI Integration with Development Tools
3.1 ChatGPT for Kotlin
ChatGPT can generate Kotlin code, explain concepts, debug errors, suggest improvements, and create practice exercises. It is particularly useful for learning functional programming patterns, coroutines, and Kotlin-specific features.
Key Use Cases:
- Code generation from descriptions
- Bug fixing and debugging
- Code review and improvement
- Learning explanations
- Test generation
- Refactoring suggestions
Code Example:
// ChatGPT generates functional code
fun processBirds(birds: List<String>): List<String> {
return birds
.filter { it.length > 3 }
.map { it.uppercase() }
.sorted()
}
fun main() {
val birds = listOf("Eagle", "Sparrow", "Hawk", "Robin", "Finch")
val result = processBirds(birds)
println("Processed birds: $result")
// Output: [EAGLE, HAWK, ROBIN, SPARROW]
// ChatGPT can explain coroutines
// "Here's an example of using coroutines for async operations"
import kotlinx.coroutines.*
runBlocking {
val deferred = async {
delay(1000)
"Bird data loaded"
}
println("Loading...")
val result = deferred.await()
println(result)
}
}
ChatGPT can understand natural language queries and generate Kotlin code. filter { it.length > 3 } keeps elements with length greater than 3. map { it.uppercase() } converts each element to uppercase. sorted() sorts the list. The coroutine example shows async for concurrent operations.
3.2 GitHub Copilot
GitHub Copilot suggests whole lines or functions as you type. It works inside IntelliJ IDEA, Android Studio, and VS Code. Copilot understands Kotlin idioms and can generate data classes, extension functions, and coroutine code.
Key Features:
- Auto-completion of code
- Function generation
- Test generation
- Documentation suggestions
- Bug fixes
- Refactoring
Code Example:
// Start typing: "extension function to reverse string"
// Copilot generates:
fun String.reverse(): String = this.reversed()
// Start typing: "data class Bird"
// Copilot generates:
data class Bird(
val species: String,
val count: Int,
val weight: Double
)
// Start typing: "function to find heaviest bird"
// Copilot generates:
fun findHeaviestBird(birds: List<Bird>): Bird? {
return birds.maxByOrNull { it.weight }
}
fun main() {
// Copilot suggests usage
val birds = listOf(
Bird("Sparrow", 10, 25.5),
Bird("Eagle", 3, 4500.0)
)
val heaviest = findHeaviestBird(birds)
println("Heaviest: ${heaviest?.species}")
println("Kotlin".reverse()) // niltok
}
Copilot analyzes the current context and suggests relevant code. fun String.reverse(): String = this.reversed() defines an extension function on String. maxByOrNull finds the element with the maximum value or returns null if empty.
3.3 AI Debugging Workflows
When you encounter a runtime exception or compile error, copy the stack trace and code into ChatGPT. It can identify the root cause (e.g., null pointer, missing coroutine context, incorrect lambda) and suggest fixes.
Common Debugging Scenarios:
| Error Type | Cause | Fix |
|---|---|---|
| NullPointerException | Accessing null value | Use safe calls (?.) or Elvis (?:) |
| CoroutineScope | Missing coroutine context | Add runBlocking or coroutine scope |
| Type mismatch | Incorrect type inference | Add explicit type or cast |
| Lambda issues | Incorrect lambda syntax | Check lambda parameters and returns |
Code Example:
// ---- BUGGY CODE ----
val birdName: String? = null
// println(birdName.length) // ERROR: Only safe (?.) or non-null asserted (!!.) calls allowed
// ---- FIXED CODE ----
fun main() {
val birdName: String? = null
// Use safe call operator
val length = birdName?.length ?: 0
println("Length: $length") // Length: 0
// Use let for non-null operations
birdName?.let {
println("Bird name: $it")
println("Length: ${it.length}")
}
// Alternatively, use if-null check
if (birdName != null) {
println("Bird name: $birdName")
} else {
println("Bird name is null")
}
}
AI Debugging Example:
// AI can identify coroutine issues
// ---- BUGGY CODE ----
// suspend fun fetchBirds(): List<String> {
// delay(1000) // ERROR: suspend functions can only be called from coroutine scope
// return listOf("Eagle", "Sparrow")
// }
// ---- FIXED CODE ----
import kotlinx.coroutines.*
suspend fun fetchBirds(): List<String> {
delay(1000) // OK: suspend function
return listOf("Eagle", "Sparrow")
}
fun main() = runBlocking {
val birds = fetchBirds()
println("Birds: $birds")
}
AI can explain compiler errors and suggest fixes. The safe call operator ?. returns null if the receiver is null. The Elvis operator ?: provides a default value. suspend functions require a coroutine context. runBlocking provides that context for testing.
3.4 AI Code Review
Paste your Kotlin function or class into AI and ask for a review. AI can find issues like unnecessary mutability, missing kotlinx.serialization annotations, inefficient collection operations, or coroutine misuse.
Common Issues Detected:
- Using
varwhenvalis sufficient - Mutable collections when immutable would work
- Loops that could be functional chains
- Missing null safety
- Inefficient collection operations
Code Example:
// ---- ORIGINAL (Imperative) ----
fun getAdultBirdSpecies(birds: List<Bird>): List<String> {
val result = mutableListOf<String>()
for (bird in birds) {
if (bird.age >= 2) {
result.add(bird.species)
}
}
return result
}
// ---- AI REVIEW: Improved Functional Version ----
fun getAdultBirdSpecies(birds: List<Bird>): List<String> =
birds.filter { it.age >= 2 }.map { it.species }
// ---- ORIGINAL (Inefficient) ----
fun findBirdByName(birds: List<Bird>, name: String): Bird? {
for (bird in birds) {
if (bird.species == name) {
return bird
}
}
return null
}
// ---- AI REVIEW: Use find instead ----
fun findBirdByName(birds: List<Bird>, name: String): Bird? =
birds.find { it.species == name }
data class Bird(val species: String, val age: Int)
Code Review Example:
// AI review of extension functions
// ---- ORIGINAL ----
fun String.toBird(): Bird {
val parts = this.split(",")
return Bird(parts[0], parts[1].toInt())
}
// ---- AI IMPROVED ----
fun String.toBird(): Bird? {
val parts = this.split(",")
return if (parts.size == 2) {
try {
Bird(parts[0].trim(), parts[1].trim().toInt())
} catch (e: NumberFormatException) {
null
}
} else null
}
data class Bird(val species: String, val count: Int)
fun main() {
val input = "Sparrow,10"
val bird = input.toBird()
println(bird) // Bird(species=Sparrow, count=10)
val invalid = "Eagle" // Missing count
val invalidBird = invalid.toBird()
println(invalidBird) // null
}
AI code review identifies redundant loops and suggests functional alternatives. filter creates a new list with matching elements. map transforms each element. The find function searches through a collection and returns the first element that satisfies the specified predicate.The improved version adds error handling and returns null for invalid input.
3.5 AI Test Generation
AI can generate unit test cases using KotlinTest (kotest) or JUnit. Provide the function signature and a brief description, and AI will write tests covering edge cases, normal inputs, and error scenarios.
Code Example:
// ---- FUNCTION TO TEST ----
fun reverseString(input: String?): String? {
return input?.reversed()
}
// ---- AI GENERATED TESTS ----
import org.junit.jupiter.api.Assertions.*
import org.junit.jupiter.api.Test
class StringUtilsTest {
@Test
fun testReverseStringNormal() {
assertEquals("cba", reverseString("abc"))
}
@Test
fun testReverseStringEmpty() {
assertEquals("", reverseString(""))
}
@Test
fun testReverseStringNull() {
assertNull(reverseString(null))
}
@Test
fun testReverseStringSingleChar() {
assertEquals("A", reverseString("A"))
}
@Test
fun testReverseStringWithSpaces() {
assertEquals("kcor hturb", reverseString("bruth rock"))
}
}
Test Generation Example:
// ---- FUNCTION WITH MULTIPLE CASES ----
fun countBirdsBySpecies(birds: List<String>): Map<String, Int> {
return birds.groupingBy { it }.eachCount()
}
// ---- AI GENERATED TESTS ----
import org.junit.jupiter.api.Test
import kotlin.test.assertEquals
class BirdCountTest {
@Test
fun testCountBirdsBySpeciesEmptyList() {
val result = countBirdsBySpecies(emptyList())
assertEquals(emptyMap<String, Int>(), result)
}
@Test
fun testCountBirdsBySpeciesSingleSpecies() {
val birds = listOf("Sparrow", "Sparrow", "Sparrow")
val result = countBirdsBySpecies(birds)
assertEquals(mapOf("Sparrow" to 3), result)
}
@Test
fun testCountBirdsBySpeciesMultipleSpecies() {
val birds = listOf("Eagle", "Sparrow", "Eagle", "Hawk", "Sparrow")
val result = countBirdsBySpecies(birds)
assertEquals(mapOf("Eagle" to 2, "Sparrow" to 2, "Hawk" to 1), result)
}
@Test
fun testCountBirdsBySpeciesCaseSensitive() {
val birds = listOf("Sparrow", "sparrow", "Sparrow")
val result = countBirdsBySpecies(birds)
assertEquals(mapOf("Sparrow" to 2, "sparrow" to 1), result)
}
}
AI generates comprehensive tests for various scenarios. assertEquals checks expected vs actual values. emptyList() creates an empty list. groupingBy groups elements by a key. eachCount() counts occurrences in each group.
3.6 AI Refactoring
AI can refactor code to use Kotlin idioms: convert loops to functional chains, replace when with expression bodies, use scope functions (let, apply, run), and add null safety.
Common Refactoring Patterns:
- Loop → Functional chain (
filter,map,reduce) if→whenexpression- Null checks → Safe calls and Elvis operator
- Object creation →
applyoralsoscope functions - Manual builders → DSL
Code Example:
// ---- ORIGINAL ----
fun processBirds(birds: List<Bird>): List<String> {
val result = mutableListOf<String>()
for (bird in birds) {
if (bird.count > 5) {
result.add(bird.species)
}
}
return result
}
// ---- REFACTORED ----
fun processBirds(birds: List<Bird>): List<String> =
birds.filter { it.count > 5 }.map { it.species }
// ---- ORIGINAL ----
val user = getUser()
if (user != null) {
println("Hello ${user.name}")
println("Age ${user.age}")
}
// ---- REFACTORED ----
getUser()?.let {
with(it) {
println("Hello $name")
println("Age $age")
}
}
data class Bird(val species: String, val count: Int)
data class User(val name: String, val age: Int)
fun getUser(): User? = User("Alice", 25)
Refactoring Example:
// ---- ORIGINAL (Imperative) ----
fun getHeavyBirds(birds: List<Bird>, threshold: Double): List<String> {
val heavyBirds = mutableListOf<String>()
for (bird in birds) {
if (bird.weight > threshold) {
heavyBirds.add(bird.species)
}
}
return heavyBirds
}
// ---- REFACTORED (Functional) ----
fun getHeavyBirds(birds: List<Bird>, threshold: Double): List<String> =
birds.filter { it.weight > threshold }.map { it.species }
// ---- ORIGINAL (Multiple Null Checks) ----
fun formatBird(bird: Bird?): String {
if (bird == null) {
return "Unknown bird"
}
val species = if (bird.species != null) bird.species else "No species"
val count = if (bird.count != null) bird.count.toString() else "Unknown count"
return "$species: $count birds"
}
// ---- REFACTORED (Null Safety) ----
fun formatBird(bird: Bird?): String {
return bird?.let {
"${it.species ?: "No species"}: ${it.count ?: "Unknown count"} birds"
} ?: "Unknown bird"
}
data class Bird(val species: String? = null, val count: Int? = null, val weight: Double = 0.0)
AI refactoring converts imperative code to functional style. filter { it.weight > threshold } keeps birds above the weight threshold. map { it.species } extracts species names. The let scope function executes a block on a non-null object. with provides a context object for multiple operations.
Kotlin Fundamentals
4.1 Variables
Kotlin uses val (immutable, read-only reference) and var (mutable). Prefer val by default.
Detailed Explanation:
val– Read-only reference. Cannot be reassigned once initialized.var– Mutable reference. Can be reassigned multiple times.- Type inference – Kotlin infers types from initial values.
- Property access – Direct access without getters/setters.
Code Example:
fun main() {
// val - immutable (cannot be reassigned)
val birdSpecies = "Sparrow"
// birdSpecies = "Eagle" // ERROR: val cannot be reassigned
// var - mutable (can be reassigned)
var birdCount = 10
birdCount = 15 // OK: var can be reassigned
// Type inference
val inferredString = "String type inferred"
val inferredInt = 42
val inferredDouble = 3.14
// Explicit types
val explicitString: String = "String explicitly typed"
val explicitInt: Int = 100
// Multiple declarations
val (species, count) = Pair("Eagle", 3)
println("Species: $species, Count: $count")
println("Bird: $birdSpecies, Count: $birdCount")
}
val creates an immutable reference, not an immutable object. The reference cannot point to a different object, but the object itself can be mutable. Kotlin’s type inference determines the type from the assigned value. The Pair destructuring declaration assigns both values to separate variables.
4.2 Data Types
Kotlin’s built-in types: Int, Long, Double, Float, Boolean, Char, String.
Detailed Explanation:
| Type | Description | Example | Memory |
|---|---|---|---|
Int | 32-bit integer | 100 | 4 bytes |
Long | 64-bit integer | 100L | 8 bytes |
Double | 64-bit floating point | 3.14 | 8 bytes |
Float | 32-bit floating point | 3.14f | 4 bytes |
Boolean | True or false | true | 1 byte |
Char | Single character | 'A' | 2 bytes |
String | Character sequence | "Hello" | Varies |
Byte | 8-bit integer | 10 | 1 byte |
Short | 16-bit integer | 100 | 2 bytes |
Code Example:
fun main() {
// Numeric types
val byteValue: Byte = 10
val shortValue: Short = 100
val intValue = 1000 // Inferred Int
val longValue = 1000L // Explicit Long
val floatValue = 3.14f // Explicit Float
val doubleValue = 3.14159 // Inferred Double
// Other types
val charValue: Char = 'A'
val booleanValue: Boolean = true
val stringValue: String = "Sparrow"
// Type conversions (explicit)
val intFromLong = longValue.toInt()
val doubleFromInt = intValue.toDouble()
val stringFromInt = intValue.toString()
// String templates
println("Int: $intValue, Double: $doubleValue")
println("Char: $charValue, Bool: $booleanValue")
println("String: $stringValue, Length: ${stringValue.length}")
// Safe type conversions
val number: String? = "123"
val parsedInt = number?.toIntOrNull() ?: 0
println("Parsed: $parsedInt")
}
Kotlin’s numeric types are the same as Java’s primitives but with automatic boxing when needed. Types are non-nullable by default. Type conversions are explicit using methods like toInt(), toLong(), etc. toIntOrNull() returns null if parsing fails, avoiding exceptions.
4.3 Operators
Kotlin supports arithmetic, comparison, logical, and bitwise operators.
Arithmetic Operators: +, -, *, /, %
Comparison Operators: ==, !=, <, >, <=, >=
Logical Operators: && (AND), || (OR), ! (NOT)
Bitwise Operators: and, or, xor, inv, shl, shr, ushr
Range Operators: .., downTo, until, step
Code Example:
fun main() {
val eagle = 10
val hawk = 5
val sparrow = 3
// Arithmetic operators
println("Addition: ${eagle + hawk}")
println("Subtraction: ${eagle - hawk}")
println("Multiplication: ${eagle * hawk}")
println("Division: ${eagle / hawk}")
println("Modulo: ${eagle % hawk}")
// Comparison operators
println("Eagle > Hawk: ${eagle > hawk}")
println("Eagle == 10: ${eagle == 10}")
println("Sparrow <= 5: ${sparrow <= 5}")
// Logical operators
val cond1 = eagle > hawk && hawk > sparrow
val cond2 = eagle < hawk || hawk > sparrow
val cond3 = !(eagle < hawk)
println("AND: $cond1, OR: $cond2, NOT: $cond3")
// Range operators
println("5 in 1..10: ${5 in 1..10}")
println("15 in 1..10: ${15 in 1..10}")
// Bitwise operators (for Int and Long)
val a = 5 // 0101
val b = 3 // 0011
println("AND: ${a and b}") // 0001 = 1
println("OR: ${a or b}") // 0111 = 7
println("XOR: ${a xor b}") // 0110 = 6
println("SHL: ${a shl 1}") // 1010 = 10
// Increment/Decrement
var count = 5
println("Post-increment: ${count++}") // Prints 5, then 6
println("Pre-increment: ${++count}") // 7
}
Kotlin operators are similar to Java but with some differences. The in operator checks if a value is in a range. Bitwise operations use named functions instead of symbols. shl shifts bits left. The increment/decrement operators work the same as in Java.
4.4 Type Inference
Kotlin infers types from initial values, reducing boilerplate while maintaining type safety.
- Inferred types – The compiler determines the type from the initialization expression.
- Explicit types – Optional for clarity or when the compiler can’t infer.
- Smart casts – The compiler automatically casts after type checks.
- Type checks –
isoperator checks type, then smart cast follows.
Code Example:
fun main() {
// Type inference examples
val message = "Hello" // Inferred String
val count = 10 // Inferred Int
val price = 99.99 // Inferred Double
val isAvailable = true // Inferred Boolean
val char = 'A' // Inferred Char
// Explicit types (when needed)
val explicitString: String = "Hello"
val explicitInt: Int = 10
val nullableString: String? = null
// Smart casts
fun printLength(value: Any) {
if (value is String) {
// Smart cast: value is automatically String here
println("Length: ${value.length}")
} else {
println("Not a string")
}
}
printLength("Sparrow") // Length: 7
printLength(10) // Not a string
// Type inference with collections
val birds = listOf("Sparrow", "Eagle", "Hawk") // List<String>
val numbers = listOf(1, 2, 3) // List<Int>
val mixed = listOf("Bird", 10, 3.14) // List<Any>
// Type inference with var
var mutableBird = "Sparrow" // Type inferred as String
mutableBird = "Eagle" // OK: same type
// mutableBird = 10 // ERROR: type mismatch
println("Message: $message, Count: $count")
println("Price: $price, Available: $isAvailable")
}
The compiler analyzes the initialization expression and determines the most specific type. Smart casts allow using the specific type after checking with is. Collections infer the element type. Any is the root type, used when elements have different types.
4.5 Null Safety
By default, types are non-nullable. Use ? to allow null, and safe calls with ?., Elvis ?:.
- Non-nullable types – Default, cannot hold
null. - Nullable types – Marked with
?, can holdnull. - Safe call (
?.) – Calls the member if the receiver is notnull. - Elvis operator (
?:) – Provides a default value if the expression isnull. - Not-null assertion (
!!) – Converts to non-nullable (throws ifnull).
Code Example:
fun main() {
// Non-nullable type (default)
var birdName: String = "Sparrow"
// birdName = null // ERROR: Null can not be a value of a non-null type String
// Nullable type
var nullableBirdName: String? = "Sparrow"
nullableBirdName = null // OK: nullable type can hold null
// Safe call operator
val length = nullableBirdName?.length // Returns null if nullableBirdName is null
println("Length: $length") // null
// Elvis operator (provides default)
val safeLength = nullableBirdName?.length ?: 0
println("Safe length: $safeLength") // 0
// Not-null assertion (dangerous)
// val forcedLength = nullableBirdName!!.length // Throws NullPointerException
// Safe call with let
nullableBirdName = "Eagle"
nullableBirdName?.let {
println("Bird name: $it")
println("Length: ${it.length}")
}
// Safe call with chaining
data class Bird(val species: String?, val count: Int?)
val bird: Bird? = Bird("Hawk", 5)
val species = bird?.species?.uppercase() ?: "UNKNOWN"
println("Species: $species") // HAWK
// Elvis with side effects
val count = bird?.count ?: run {
println("Count is null, using default")
0
}
println("Count: $count")
}
Kotlin’s type system clearly separates types that can hold null from those that are guaranteed to contain a non-null value. The safe call ?. returns null if the receiver is null. The Elvis ?: provides an alternative value. The let scope function executes only on non-null values. The !! operator should be avoided unless absolutely necessary.
Control Flow
5.1 If Expressions
if is an expression that returns a value, not just a statement.
ifcan be used as an expression returning a value.- The last expression in each branch becomes the return value.
- Can be used in assignments and expressions.
- Eliminates the need for ternary operator (
? :).
Code Example:
fun main() {
// If as expression
val a = 10
val b = 20
val max = if (a > b) a else b
println("Max: $max") // 20
// If with multiple statements in branches
val min = if (a < b) {
println("a is smaller")
a // Last expression becomes the return value
} else {
println("b is smaller")
b
}
println("Min: $min") // 10
// If in function return
fun maxOf(a: Int, b: Int) = if (a > b) a else b
// If with string template
val result = if (a > b) "$a is greater" else "$b is greater"
println(result) // 20 is greater
// If as statement (no return value)
if (a > b) {
println("a is greater")
} else {
println("b is greater or equal")
}
// Nested if expressions
val birdCount = 15
val category = if (birdCount > 20) {
"Large flock"
} else if (birdCount > 10) {
"Medium flock"
} else if (birdCount > 5) {
"Small flock"
} else {
"Tiny flock"
}
println("Category: $category") // Medium flock
}
if expressions return the value of the chosen branch. Each branch’s last expression determines the result. The ternary operator is not needed because if serves both purposes. This makes code more concise and readable.
5.2 When Expressions
when is a powerful replacement for switch with much more flexibility.
whencan match values, types, ranges, and conditions.- Each branch ends with
->and the expression to return. elsebranch is required unless exhaustive.- Can be used as an expression or statement.
Code Example:
fun main() {
// When as expression
val value = 5
val description = when (value) {
0 -> "Zero"
1 -> "One"
2, 3, 4 -> "Small number"
in 5..10 -> "Between 5 and 10"
else -> "Large number"
}
println("Description: $description") // Between 5 and 10
// When with type check
fun describe(obj: Any): String = when (obj) {
is String -> "String of length ${obj.length}"
is Int -> "Integer value: $obj"
is Double -> "Double value: $obj"
is List<*> -> "List with ${obj.size} elements"
else -> "Unknown type"
}
println(describe("Hello")) // String of length 5
println(describe(10)) // Integer value: 10
println(describe(listOf(1,2))) // List with 2 elements
// When without argument (as statement)
val birdCount = 15
when {
birdCount > 20 -> println("Large flock")
birdCount > 10 -> println("Medium flock")
birdCount > 5 -> println("Small flock")
else -> println("Tiny flock")
}
// When with ranges
val age = 25
val category = when (age) {
in 0..12 -> "Child"
in 13..19 -> "Teenager"
in 20..64 -> "Adult"
else -> "Senior"
}
println("Category: $category") // Adult
// When with multiple conditions
val bird = "Eagle"
val type = when (bird) {
"Eagle", "Hawk", "Falcon" -> "Bird of prey"
"Sparrow", "Robin", "Finch" -> "Songbird"
"Duck", "Swan", "Goose" -> "Water bird"
else -> "Unknown type"
}
println("$bird is a $type") // Eagle is a Bird of prey
}
when evaluates the expression and finds the first matching branch. It can match constants, ranges, types, and arbitrary conditions. The else branch catches all unmatched values. Without an argument, when acts like an if-else chain.
5.3 Loops
Kotlin provides for, while, and repeat loops for iteration.
for– Iterates over ranges, collections, and arrays.while– Executes while condition is true.do-while– Executes at least once.repeat– Executes a block a fixed number of times.
Code Example:
fun main() {
// For loop with range
println("For with range:")
for (i in 1..5) {
print("$i ") // 1 2 3 4 5
}
println()
// For loop with downTo
println("For with downTo:")
for (i in 5 downTo 1) {
print("$i ") // 5 4 3 2 1
}
println()
// For loop with step
println("For with step:")
for (i in 1..10 step 2) {
print("$i ") // 1 3 5 7 9
}
println()
// For loop until (excludes upper bound)
println("For with until:")
for (i in 1 until 5) {
print("$i ") // 1 2 3 4
}
println()
// For loop with collection
val birds = listOf("Sparrow", "Eagle", "Hawk", "Robin")
println("For with collection:")
for (bird in birds) {
print("$bird ") // Sparrow Eagle Hawk Robin
}
println()
// For with index
println("For with index:")
for ((index, bird) in birds.withIndex()) {
println("$index: $bird")
}
// While loop
println("While loop:")
var i = 5
while (i > 0) {
print("$i ") // 5 4 3 2 1
i--
}
println()
// Do-while loop (executes at least once)
println("Do-while loop:")
var j = 5
do {
print("$j ") // 5 4 3 2 1
j--
} while (j > 0)
println()
// Repeat loop
println("Repeat loop:")
repeat(3) { index ->
println("Iteration $index") // 0, 1, 2
}
// Break and continue
println("Break example:")
for (i in 1..10) {
if (i == 5) break
print("$i ") // 1 2 3 4
}
println()
println("Continue example:")
for (i in 1..10) {
if (i % 2 == 0) continue
print("$i ") // 1 3 5 7 9
}
println()
}
for loops work with any iterator. Ranges are created with .., downTo, until, and step. while checks the condition before each execution. do-while checks after execution, guaranteeing at least one run. break exits the loop, continue skips to the next iteration.
5.4 Ranges
Ranges are created with .., downTo, until, step and support in and !in operators.
..– Creates a range including both ends.downTo– Creates a descending range.until– Creates a range excluding the upper bound.step– Specifies the step value.in– Checks if a value is in the range.
Code Example:
fun main() {
// Basic ranges
val numbers = 1..10
println("1..10: $numbers")
// Checking membership
println("5 in 1..10: ${5 in numbers}") // true
println("15 in 1..10: ${15 in numbers}") // false
// Descending ranges
val descending = 10 downTo 1
println("10 downTo 1: $descending")
// Ranges with step
val stepped = 1..10 step 2
println("1..10 step 2: $stepped")
// Until (excludes upper bound)
val untilRange = 1 until 5
println("1 until 5: $untilRange")
// Iterating over ranges
println("Iterating 1..5:")
for (i in 1..5) {
print("$i ") // 1 2 3 4 5
}
println()
println("Iterating 5 downTo 1:")
for (i in 5 downTo 1) {
print("$i ") // 5 4 3 2 1
}
println()
// Ranges with characters
val letters = 'A'..'Z'
println("'C' in A..Z: ${'C' in letters}") // true
println("'c' in A..Z: ${'c' in letters}") // false
// Ranges in when
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("Grade: $grade") // B
// Progressive ranges
val progressive = 1..10 step 2
for (i in progressive) {
print("$i ") // 1 3 5 7 9
}
println()
// Range with until and step
for (i in 1 until 10 step 3) {
print("$i ") // 1 4 7
}
println()
}
Ranges are implemented as IntRange or CharRange classes. The in operator checks membership. step modifies the progression. downTo creates a range in descending order. until creates a range that excludes the upper bound.
Functions
6.1 Function Declarations
Use fun keyword to declare functions. They can be top-level, inside classes, or as extensions.
funkeyword – Declares a function.- Return type – Specified after parameters.
- Parameters – Immutable, with types and optional default values.
- Single-expression functions – Use
=when a function has a single expression. - Unit – Returns
Unitwhen no meaningful return value.
Code Example:
fun main() {
// Function with parameters and return type
fun area(width: Double, height: Double): Double {
return width * height
}
println("Area: ${area(5.0, 3.0)}") // 15.0
// Single-expression function
fun perimeter(width: Double, height: Double) = 2 * (width + height)
println("Perimeter: ${perimeter(5.0, 3.0)}") // 16.0
// Function with default parameter
fun greet(name: String = "World") {
println("Hello, $name!")
}
greet() // Hello, World!
greet("Kotlin") // Hello, Kotlin!
// Function returning Unit (void)
fun printBird(species: String): Unit {
println("Bird: $species")
}
// Function with named arguments
fun createBird(species: String, count: Int, weight: Double) {
println("$species: $count birds, $weight g")
}
createBird(species = "Sparrow", count = 10, weight = 25.5)
createBird(count = 3, weight = 4500.0, species = "Eagle")
// Extension function
fun String.isBird(): Boolean = this in listOf("Eagle", "Sparrow", "Hawk")
println("Eagle.isBird(): ${"Eagle".isBird()}") // true
println("Cat.isBird(): ${"Cat".isBird()}") // false
}
Functions are declared with fun. The return type is optional for Unit functions. Single-expression functions use = and omit braces. Default parameters reduce overloading. Named arguments improve readability and allow reordering. Extension functions allow developers to add new capabilities to existing classes without modifying their original definitions.
6.2 Parameters
Parameters are immutable and can have default values. Named arguments allow flexible ordering.
- Immutable – Parameters cannot be reassigned.
- Default values – Provide fallback values when arguments are omitted.
- Named arguments – Specify parameter names when calling.
- Varargs – Variable number of arguments with
vararg. - Destructuring – Extract multiple values from a single argument.
Code Example:
fun main() {
// Default parameters
fun sendMessage(
to: String,
subject: String = "No Subject",
body: String = ""
) {
println("To: $to, Subject: $subject, Body: $body")
}
sendMessage("user@example.com")
sendMessage("user@example.com", "Hello")
sendMessage("user@example.com", "Hello", "Body text")
// Named arguments (allows reordering)
sendMessage(
body = "Important content",
to = "boss@example.com",
subject = "Report"
)
// Varargs
fun processBirds(vararg species: String) {
species.forEach { println("Bird: $it") }
}
processBirds("Eagle", "Sparrow", "Hawk", "Robin")
// Mixing varargs with other parameters
fun countBirds(threshold: Int, vararg counts: Int): Int {
return counts.filter { it > threshold }.size
}
println("Count above 5: ${countBirds(5, 3, 7, 2, 10, 6)}") // 3
// Destructuring in parameters
data class Bird(val species: String, val count: Int)
fun processBird(bird: Bird) {
val (species, count) = bird
println("$species: $count birds")
}
processBird(Bird("Sparrow", 10))
// Extension function with parameters
fun List<Int>.filterAbove(threshold: Int): List<Int> {
return this.filter { it > threshold }
}
val numbers = listOf(1, 5, 8, 3, 10)
println(numbers.filterAbove(5)) // [8, 10]
}
Parameters are read-only. Default values are automatically applied to parameters when the corresponding arguments are not supplied in a function call. Named arguments match by name, not position. vararg collects multiple arguments into an array. Destructuring declares multiple variables from a single object.
6.3 Return Types
Specify return type or infer for single-expression functions. Functions can return complex types.
- Explicit return – Specify type after
:. - Inferred return – Type inferred from the body.
- Unit – No meaningful return value.
- Nothing – Function never returns normally.
- Nullable returns – Return type can be nullable.
Code Example:
fun main() {
// Explicit return type
fun add(a: Int, b: Int): Int {
return a + b
}
// Inferred return type
fun multiply(a: Int, b: Int) = a * b
// Unit return (no explicit return)
fun displayBird(species: String): Unit {
println("Bird: $species")
}
// Unit can be omitted
fun printBird(species: String) {
println("Bird: $species")
}
// Nullable return type
fun findBird(species: String): String? {
val birds = listOf("Eagle", "Sparrow", "Hawk")
return birds.find { it == species }
}
println(findBird("Eagle")) // Eagle
println(findBird("Robin")) // null
// Multiple return types with sealed class
sealed class BirdResult
data class Found(val species: String) : BirdResult()
object NotFound : BirdResult()
fun searchBird(species: String): BirdResult {
val birds = listOf("Eagle", "Sparrow", "Hawk")
return if (species in birds) Found(species) else NotFound
}
when (val result = searchBird("Eagle")) {
is Found -> println("Found: ${result.species}")
NotFound -> println("Not found")
}
// Function returning pair
fun getBirdInfo(): Pair<String, Int> = Pair("Sparrow", 10)
val (species, count) = getBirdInfo()
println("$species: $count")
// Function returning list
fun getBirds(): List<String> = listOf("Eagle", "Sparrow", "Hawk")
println(getBirds()) // [Eagle, Sparrow, Hawk]
}
Return types can be explicit or inferred. Unit functions don’t need an explicit return. Nullable return types use ?. Sealed classes represent restricted hierarchies. Pair and Triple return multiple values. Collection types like List are common return types.
6.4 Default Parameters
Default parameters reduce overloading and provide flexibility in function calls.
- Default values – Assigned in the function signature.
- Overloading reduction – No need for multiple similar functions.
- Optional arguments – Can be omitted in calls.
- Named arguments – Can reorder and skip default values.
Code Example:
fun main() {
// Function with default parameters
fun createBird(
species: String = "Unknown",
count: Int = 0,
weight: Double = 0.0,
isMigratory: Boolean = false
) {
println("$species: $count birds, ${weight}g, Migratory: $isMigratory")
}
// Call with all defaults
createBird() // Unknown: 0 birds, 0.0g, Migratory: false
// Override some defaults
createBird("Sparrow") // Sparrow: 0 birds, 0.0g, Migratory: false
createBird("Eagle", 3) // Eagle: 3 birds, 0.0g, Migratory: false
createBird("Hawk", 5, 1200.0) // Hawk: 5 birds, 1200.0g, Migratory: false
// Using named arguments to skip defaults
createBird(
species = "Cardinal",
isMigratory = true
) // Cardinal: 0 birds, 0.0g, Migratory: true
// Different order with named arguments
createBird(
weight = 4500.0,
species = "Eagle",
count = 3,
isMigratory = false
)
// Extension function with default parameter
fun List<String>.countStartingWith(prefix: String = ""): Int {
return this.count { it.startsWith(prefix) }
}
val birds = listOf("Eagle", "Sparrow", "Hawk", "Robin")
println("Total: ${birds.countStartingWith()}") // 4
println("Starting with S: ${birds.countStartingWith("S")}") // 1
// Function with default lambda
fun processData(
data: List<Int>,
transform: (Int) -> Int = { it }
): List<Int> {
return data.map(transform)
}
val numbers = listOf(1, 2, 3, 4, 5)
println(processData(numbers)) // [1, 2, 3, 4, 5]
println(processData(numbers) { it * 2 }) // [2, 4, 6, 8, 10]
}
Default parameters are specified with =. When arguments are omitted, the default value is used. Named arguments allow specifying only the values you want to override. Using default parameter values can reduce the need to create several overloaded versions of the same function.
6.5 Named Arguments
Named arguments allow specifying argument names in function calls, improving readability and flexibility.
- Syntax –
parameterName = valuein function calls. - Readability – Makes code self-documenting.
- Flexibility – Arguments can be in any order.
- Default parameters – Can skip default values.
- Overloads – Reduces the need for overloaded functions.
Code Example:
fun main() {
// Function with many parameters
fun createBird(
species: String,
count: Int,
weight: Double,
color: String,
isMigratory: Boolean,
habitat: String
) {
println("$species: $count birds, ${weight}g, Color: $color")
println("Migratory: $isMigratory, Habitat: $habitat")
}
// Without named arguments (positional)
createBird("Sparrow", 10, 25.5, "Brown", true, "Forest")
// With named arguments (clearer)
createBird(
species = "Eagle",
count = 3,
weight = 4500.0,
color = "Brown",
isMigratory = false,
habitat = "Mountains"
)
// Reordering with named arguments
createBird(
habitat = "Forest",
color = "Red",
species = "Cardinal",
isMigratory = false,
count = 8,
weight = 45.0
)
// With default parameters, only specify needed values
fun updateBird(
species: String,
newCount: Int = 0,
newWeight: Double? = null,
newColor: String? = null
) {
println("Updating $species: Count: $newCount, Weight: $newWeight, Color: $newColor")
}
updateBird("Sparrow", newCount = 15)
updateBird("Eagle", newWeight = 4600.0)
updateBird("Hawk", newCount = 5, newColor = "Gray")
// Named arguments with varargs
fun printBirds(species: String, vararg colors: String) {
println("$species colors: ${colors.joinToString()}")
}
printBirds("Cardinal", "Red", "Brown", "Black")
printBirds(species = "Sparrow", colors = "Brown", "Gray")
// Named arguments improve readability in complex calls
data class Bird(val species: String, val count: Int)
val birdList = listOf(
Bird("Eagle", 3),
Bird("Sparrow", 10),
Bird("Hawk", 5)
)
fun findBirds(
birds: List<Bird>,
minCount: Int = 0,
maxCount: Int = Int.MAX_VALUE
): List<Bird> {
return birds.filter { it.count in minCount..maxCount }
}
val found = findBirds(
birds = birdList,
minCount = 4,
maxCount = 10
)
println(found) // [Bird(species=Sparrow, count=10), Bird(species=Hawk, count=5)]
}
Named arguments use parameterName = value syntax. They allow arguments in any order and make function calls self-documenting. This is especially useful with default parameters, where you only need to specify the values you want to change. Named arguments reduce the need for overloaded functions.
Collections
7.1 Lists
Lists are ordered collections with indexed access. Kotlin provides immutable (listOf) and mutable (mutableListOf) variants.
- Immutable lists –
listOf, cannot be modified after creation. - Mutable lists –
mutableListOf, can be modified (add, remove, update). - Indexed access – Access elements by position.
- Operations –
size,isEmpty,contains,get, etc. - Factory functions –
listOf,mutableListOf,buildList.
Code Example:
fun main() {
// Immutable list
val birds = listOf("Sparrow", "Eagle", "Hawk", "Cardinal")
println("Birds: $birds")
println("Size: ${birds.size}")
println("First: ${birds.first()}")
println("Last: ${birds.last()}")
println("Contains 'Eagle': ${birds.contains("Eagle")}")
println("Index of 'Hawk': ${birds.indexOf("Hawk")}")
// Access by index
println("First: ${birds[0]}") // Sparrow
println("Third: ${birds.get(2)}") // Hawk
// Iteration
println("Iterating:")
for (bird in birds) {
print("$bird ") // Sparrow Eagle Hawk Cardinal
}
println()
// Mutable list
val mutableBirds = mutableListOf("Sparrow", "Eagle", "Hawk")
mutableBirds.add("Cardinal")
mutableBirds.add(1, "Robin")
mutableBirds[2] = "Falcon"
mutableBirds.remove("Sparrow")
println("Mutable: $mutableBirds") // [Robin, Falcon, Hawk, Cardinal]
// List operations
val numbers = listOf(5, 2, 8, 1, 9, 3)
println("Sorted: ${numbers.sorted()}")
println("Reversed: ${numbers.reversed()}")
println("Min: ${numbers.minOrNull()}")
println("Max: ${numbers.maxOrNull()}")
println("Sum: ${numbers.sum()}")
println("Average: ${numbers.average()}")
// Filtering and mapping
val birdCounts = listOf(5, 10, 3, 8, 12, 6)
val aboveFive = birdCounts.filter { it > 5 }
val doubled = birdCounts.map { it * 2 }
println("Above 5: $aboveFive") // [10, 8, 12, 6]
println("Doubled: $doubled") // [10, 20, 6, 16, 24, 12]
// Building lists
val built = buildList {
add("Sparrow")
add("Eagle")
add("Hawk")
}
println("Built: $built")
// Empty list
val empty = emptyList<String>()
println("Empty: $empty, Size: ${empty.size}")
}
listOf creates an immutable list. mutableListOf creates a mutable list. Lists are ordered and support indexed access. Immutable lists cannot be modified but are thread-safe. Mutable lists allow adding, removing, and updating elements. Collection operations like filter and map create new lists without modifying the original.
7.2 Sets
Sets are unordered collections with unique elements (no duplicates).
- Unique elements – No duplicates allowed.
- Unordered – No guaranteed order.
- Immutable –
setOfcannot be modified. - Mutable –
mutableSetOfcan be modified. - Operations –
contains,add,remove,union,intersect.
Code Example:
fun main() {
// Immutable set (no duplicates)
val birdSet = setOf("Sparrow", "Eagle", "Hawk", "Sparrow", "Eagle")
println("Bird set: $birdSet") // [Sparrow, Eagle, Hawk] (duplicates removed)
println("Size: ${birdSet.size}") // 3
// Set operations
println("Contains 'Eagle': ${birdSet.contains("Eagle")}") // true
println("Contains 'Robin': ${birdSet.contains("Robin")}") // false
// Mutable set
val mutableBirds = mutableSetOf("Sparrow", "Eagle")
mutableBirds.add("Hawk")
mutableBirds.add("Sparrow") // Duplicate, not added
mutableBirds.remove("Eagle")
println("Mutable set: $mutableBirds") // [Sparrow, Hawk]
// Set operations with other sets
val setA = setOf(1, 2, 3, 4)
val setB = setOf(3, 4, 5, 6)
println("Union: ${setA.union(setB)}") // [1, 2, 3, 4, 5, 6]
println("Intersect: ${setA.intersect(setB)}") // [3, 4]
println("Subtract: ${setA.subtract(setB)}") // [1, 2]
// Check subset/superset
println("Is subset? ${setOf(1, 2).all { it in setA }}") // true
println("Is superset? ${setA.containsAll(setOf(1, 2))}") // true
// Converting list to set (remove duplicates)
val birds = listOf("Eagle", "Sparrow", "Eagle", "Hawk", "Sparrow")
val uniqueBirds = birds.toSet()
println("Unique birds: $uniqueBirds") // [Eagle, Sparrow, Hawk]
// Set operations on data classes
data class Bird(val species: String, val count: Int)
val birdData = setOf(
Bird("Sparrow", 10),
Bird("Eagle", 3),
Bird("Sparrow", 10) // Duplicate data class
)
println("Bird data: $birdData") // Duplicate removed (equals comparison)
// Empty set
val empty = emptySet<String>()
println("Empty set: $empty, Size: ${empty.size}")
// Build set
val built = buildSet {
add("Sparrow")
add("Eagle")
add("Sparrow")
}
println("Built set: $built") // [Sparrow, Eagle]
}
Sets automatically remove duplicates based on equals() and hashCode(). setOf creates an immutable set. mutableSetOf creates a mutable set. Set operations like union, intersect, and subtract work on collections. Data classes have built-in equality, so duplicates are removed.
7.3 Maps
Maps store key-value pairs. Keys are unique, values can be duplicated.
- Key-value pairs – Each entry associates a key with a value.
- Unique keys – Keys cannot be duplicated.
- Immutable –
mapOfcannot be modified. - Mutable –
mutableMapOfcan be modified. - Operations –
get,put,remove,containsKey,containsValue.
Code Example:
fun main() {
// Immutable map
val birdCounts = mapOf(
"Sparrow" to 10,
"Eagle" to 3,
"Hawk" to 5,
"Cardinal" to 8
)
println("Bird counts: $birdCounts")
println("Size: ${birdCounts.size}")
// Accessing values
println("Sparrow count: ${birdCounts["Sparrow"]}") // 10
println("Eagle count: ${birdCounts.get("Eagle")}") // 3
println("Robin count: ${birdCounts["Robin"]}") // null
// Checking membership
println("Contains 'Eagle': ${birdCounts.containsKey("Eagle")}") // true
println("Contains 10: ${birdCounts.containsValue(10)}") // true
// Default value for missing keys
val robinCount = birdCounts.getOrDefault("Robin", 0)
println("Robin count: $robinCount") // 0
// Mutable map
val mutableMap = mutableMapOf("Sparrow" to 10)
mutableMap["Eagle"] = 3
mutableMap.put("Hawk", 5)
mutableMap["Eagle"] = 4 // Update existing
mutableMap["Robin"] = 2
mutableMap.remove("Hawk")
println("Mutable map: $mutableMap") // {Sparrow=10, Eagle=4, Robin=2}
// Iterating over maps
println("Entries:")
for ((species, count) in birdCounts) {
println("$species: $count")
}
println("Keys: ${birdCounts.keys}") // [Sparrow, Eagle, Hawk, Cardinal]
println("Values: ${birdCounts.values}") // [10, 3, 5, 8]
// Map operations
val filtered = birdCounts.filter { it.value > 5 }
val doubled = birdCounts.mapValues { it.value * 2 }
val transformed = birdCounts.mapKeys { it.key.uppercase() }
println("Filtered (>5): $filtered") // {Sparrow=10, Cardinal=8}
println("Doubled values: $doubled") // {Sparrow=20, Eagle=6, Hawk=10, Cardinal=16}
println("Uppercase keys: $transformed") // {SPARROW=10, EAGLE=3, HAWK=5, CARDINAL=8}
// Building maps
val built = buildMap {
put("Sparrow", 10)
put("Eagle", 3)
put("Hawk", 5)
}
println("Built map: $built")
// Empty map
val empty = emptyMap<String, Int>()
println("Empty: $empty, Size: ${empty.size}")
// Grouping by
val birds = listOf("Eagle", "Sparrow", "Eagle", "Hawk", "Sparrow", "Eagle")
val grouped = birds.groupingBy { it }.eachCount()
println("Grouped: $grouped") // {Eagle=3, Sparrow=2, Hawk=1}
}
Maps associate keys with values. mapOf creates an immutable map. mutableMapOf creates a mutable map. Accessing a key that doesn’t exist returns null. groupingBy and eachCount group elements and count occurrences. Map operations like filter, mapValues, and mapKeys transform the map.
7.4 Collection Transformations
Functional operations like map, filter, flatMap, and sorted transform collections efficiently.
map– Transforms each element.filtercreates a collection containing only the elements that satisfy a specified predicate or condition.flatMap– Transforms and flattens nested collections.sorted– Sorts elements.groupBy– Groups elements by a key.associate– Transforms to a map.
Code Example:
fun main() {
val birds = listOf(
"Sparrow", "Eagle", "Hawk",
"Robin", "Cardinal", "Finch"
)
// Map - transform each element
val uppercase = birds.map { it.uppercase() }
val lengths = birds.map { it.length }
println("Uppercase: $uppercase") // [SPARROW, EAGLE, HAWK, ROBIN, CARDINAL, FINCH]
println("Lengths: $lengths") // [7, 5, 4, 5, 8, 5]
// Filter - select elements
val longBirds = birds.filter { it.length > 5 }
val birdsWithE = birds.filter { it.contains('e') }
println("Long birds (>5): $longBirds") // [Sparrow, Robin, Cardinal]
println("Birds with 'e': $birdsWithE") // [Sparrow, Eagle, Robin]
// Combine map and filter
val result = birds
.filter { it.length > 5 }
.map { it.uppercase() }
.sorted()
println("Filtered and mapped: $result") // [CARDINAL, ROBIN, SPARROW]
// FlatMap - flatten nested collections
val birdGroups = listOf(
listOf("Eagle", "Hawk"),
listOf("Sparrow", "Robin"),
listOf("Cardinal", "Finch")
)
val allBirds = birdGroups.flatMap { it }
println("FlatMap: $allBirds") // [Eagle, Hawk, Sparrow, Robin, Cardinal, Finch]
// GroupBy - group elements by a key
val groupedByLength = birds.groupBy { it.length }
println("Grouped by length: $groupedByLength")
// {7=[Sparrow], 5=[Eagle, Hawk, Robin, Finch], 8=[Cardinal]}
// Associate - transform to map
val birdMap = birds.associate { it to it.length }
println("Associate: $birdMap") // {Sparrow=7, Eagle=5, Hawk=4, Robin=5, Cardinal=8, Finch=5}
// Distinct - remove duplicates
val duplicates = listOf("Eagle", "Sparrow", "Eagle", "Hawk", "Sparrow")
val unique = duplicates.distinct()
println("Distinct: $unique") // [Eagle, Sparrow, Hawk]
// Partition - split into two lists
val (even, odd) = listOf(1, 2, 3, 4, 5, 6).partition { it % 2 == 0 }
println("Even: $even, Odd: $odd") // Even: [2, 4, 6], Odd: [1, 3, 5]
// Chaining operations
val numbers = listOf(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
val result2 = numbers
.filter { it % 2 == 0 }
.map { it * it }
.take(3)
println("Chained: $result2") // [4, 16, 36]
}
Transformation functions create new collections without modifying the original. map transforms each element. filter keeps elements matching the predicate. flatMap transforms and flattens nested structures. groupBy groups elements by a key function. associate creates a map from collection elements. These operations are lazy when used with sequences.
7.5 Functional Operations
Higher-order functions like forEach, reduce, fold, any, and all provide powerful functional patterns.
forEach– Iterates over each element.reduce– Accumulates starting from the first element.fold– Accumulates with an initial value.any– Checks if any element matches.all– Checks if all elements match.none– Checks if no elements match.find– Finds the first matching element.
Code Example:
fun main() {
val birds = listOf("Sparrow", "Eagle", "Hawk", "Robin", "Cardinal")
val numbers = listOf(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
val birdCounts = listOf(10, 3, 5, 8, 12, 6, 15, 4)
// ForEach - iterate
println("ForEach:")
birds.forEach { print("$it ") } // Sparrow Eagle Hawk Robin Cardinal
println()
// ForEach with index
birdCounts.forEachIndexed { index, count ->
println("$index: $count")
}
// Reduce - accumulate without initial value
val sum = numbers.reduce { acc, n -> acc + n }
val product = numbers.reduce { acc, n -> acc * n }
println("Sum: $sum, Product: $product") // Sum: 55, Product: 3628800
// Fold - accumulate with initial value
val sumWithInit = numbers.fold(10) { acc, n -> acc + n }
val concatenated = birds.fold("Birds: ") { acc, bird -> "$acc $bird" }
println("Sum with 10: $sumWithInit") // 65
println("Concatenated: $concatenated") // Birds: Sparrow Eagle Hawk Robin Cardinal
// Any - check if any match
println("Any > 5: ${numbers.any { it > 5 }}") // true
println("Any > 15: ${numbers.any { it > 15 }}") // false
println("Any starts with S: ${birds.any { it.startsWith('S') }}") // true
// All - check if all match
println("All > 0: ${numbers.all { it > 0 }}") // true
println("All > 5: ${numbers.all { it > 5 }}") // false
println("All length > 3: ${birds.all { it.length > 3 }}") // true
// None - check if no match
println("None > 15: ${numbers.none { it > 15 }}") // true
println("None starts with Z: ${birds.none { it.startsWith('Z') }}") // true
// Find - find first match
val firstEven = numbers.find { it % 2 == 0 }
val firstLong = birds.find { it.length > 6 }
println("First even: $firstEven") // 2
println("First long (>6): $firstLong") // Sparrow
// First - with predicate
val firstHigh = birdCounts.first { it > 10 }
println("First > 10: $firstHigh") // 12
// Last - with predicate
val lastHigh = birdCounts.last { it > 10 }
println("Last > 10: $lastHigh") // 15
// Count with predicate
val countE = birds.count { it.contains('e') }
val countLarge = birdCounts.count { it > 8 }
println("Birds with 'e': $countE") // 3
println("Counts > 8: $countLarge") // 4
// Max and Min
val maxCount = birdCounts.maxOrNull()
val minCount = birdCounts.minOrNull()
println("Max: $maxCount, Min: $minCount") // Max: 15, Min: 3
// Sum with predicate
val sumEvens = numbers.filter { it % 2 == 0 }.sum()
println("Sum of evens: $sumEvens") // 30
// Running operations
val runningSum = numbers.runningReduce { acc, n -> acc + n }
println("Running sum: $runningSum") // [1, 3, 6, 10, 15, 21, 28, 36, 45, 55]
}
Functional operations process collections without side effects. reduce combines elements using a binary operation. fold provides an initial value. any, all, and none check conditions across elements. find and first locate elements. These operations are safe and avoid manual loops.
Object-Oriented Programming
8.1 Classes
Classes are blueprints for objects. Kotlin classes can have properties, constructors, and methods.
- A primary constructor is declared directly in the class header and is used to define the parameters needed when creating an instance of the class.
- Properties – Declared in the constructor or class body.
- Methods – Functions defined inside the class.
- Visibility –
public(default),private,protected,internal. - Inheritance – Classes are final by default; use
opento allow inheritance.
Code Example:
fun main() {
// Class with primary constructor
class Bird(val species: String, var count: Int) {
// Property with custom getter
val speciesUpper: String
get() = species.uppercase()
// Method
fun display() {
println("Species: $species, Count: $count")
}
// Method with logic
fun addBirds(number: Int) {
count += number
println("Added $number $species. Total: $count")
}
}
// Creating objects
val sparrow = Bird("Sparrow", 10)
val eagle = Bird("Eagle", 3)
// Accessing properties
println("Sparrow: ${sparrow.species}, ${sparrow.count}")
println("Eagle uppercase: ${eagle.speciesUpper}")
// Calling methods
sparrow.display()
sparrow.addBirds(5)
// Class with custom constructor
class BirdWithWeight(val species: String, val count: Int) {
val weight: Double
// Secondary constructor
constructor(species: String, count: Int, weight: Double) : this(species, count) {
this.weight = weight
}
init {
require(count >= 0) { "Count must be non-negative" }
}
fun info() = "$species: $count birds"
}
val hawk = BirdWithWeight("Hawk", 5, 1200.0)
println(hawk.info())
// Class with private properties
class BirdCounter {
private var total = 0
private val speciesMap = mutableMapOf<String, Int>()
fun addBird(species: String, count: Int) {
speciesMap[species] = (speciesMap[species] ?: 0) + count
total += count
}
fun getTotal(): Int = total
fun getCount(species: String): Int = speciesMap[species] ?: 0
fun getSpecies(): Set<String> = speciesMap.keys
}
val counter = BirdCounter()
counter.addBird("Sparrow", 10)
counter.addBird("Eagle", 3)
counter.addBird("Sparrow", 5)
println("Total: ${counter.getTotal()}") // 18
println("Sparrow: ${counter.getCount("Sparrow")}") // 15
println("Species: ${counter.getSpecies()}") // [Sparrow, Eagle]
}
Classes are declared with class. The primary constructor is in the class header. Properties can be val (read-only) or var (mutable). init blocks run during object creation. Secondary constructors provide alternative ways to create objects. Methods define behavior. Private properties encapsulate data.
8.2 Objects
object declares a singleton – a class with exactly one instance.
- Singleton – Only one instance exists.
- Declaration – Use
objectinstead ofclass. - Lazy initialization – Created when first accessed.
- Access – Directly using the object name.
- Use cases – Singletons, companion objects, factory functions.
Code Example:
fun main() {
// Singleton object
object AppConfig {
val appName = "BirdTracker"
val version = "1.0"
val maxBirds = 100
fun display() {
println("$appName v$version")
println("Max birds: $maxBirds")
}
}
// Accessing singleton
println("App: ${AppConfig.appName}")
println("Version: ${AppConfig.version}")
AppConfig.display()
// Object with state
object BirdCounter {
private var count = 0
private val species = mutableMapOf<String, Int>()
fun add(species: String) {
this.species[species] = (this.species[species] ?: 0) + 1
count++
println("Added $species. Total: $count")
}
fun getCount(species: String): Int = this.species[species] ?: 0
fun getTotal(): Int = count
fun getSpecies(): Set<String> = species.keys
}
BirdCounter.add("Sparrow")
BirdCounter.add("Eagle")
BirdCounter.add("Sparrow")
println("Sparrow count: ${BirdCounter.getCount("Sparrow")}") // 2
println("Total: ${BirdCounter.getTotal()}") // 3
// Companion object (inside a class)
class Bird {
companion object {
val kingdom = "Animalia"
val phylum = "Chordata"
fun create(species: String): Bird = Bird()
}
fun chirp() = println("Chirp!")
}
println("Kingdom: ${Bird.kingdom}")
val bird = Bird.create("Sparrow")
bird.chirp()
// Object expressions (anonymous object)
val birdPrinter = object {
fun printBird(species: String, count: Int) {
println("$species: $count birds")
}
fun printHeader() {
println("=== Bird Report ===")
}
}
birdPrinter.printHeader()
birdPrinter.printBird("Eagle", 3)
// Using object as factory
object BirdFactory {
fun createEagle(): Bird = Bird()
fun createSparrow(): Bird = Bird()
fun createHawk(): Bird = Bird()
}
val eagle = BirdFactory.createEagle()
val sparrow = BirdFactory.createSparrow()
println("Eagle: ${eagle.hashCode()}, Sparrow: ${sparrow.hashCode()}")
}
object creates a singleton instance. It’s initialized lazily when first accessed. Companion objects are singletons inside a class, similar to static members in Java. Object expressions create anonymous objects without a named class. Objects are useful for utility functions, factories, and configuration.
8.3 Constructors
Constructors initialize objects. Kotlin has primary and secondary constructors with init blocks.
- Primary constructor — Declared as part of the class header and provides the main way to initialize an object.
- Secondary constructor — Declared inside the class body and offers an alternative way to create and initialize objects.
- Init blocks – Run during object initialization.
- Property initialization – Can be done in the constructor or in init blocks.
- Validation – Use
require()andcheck()for validation.
Code Example:
fun main() {
// Primary constructor with validation
class Bird(val species: String, var count: Int) {
init {
require(species.isNotBlank()) { "Species cannot be blank" }
require(count >= 0) { "Count cannot be negative" }
println("Bird $species created with $count birds")
}
// Secondary constructor
constructor(species: String) : this(species, 0) {
println("Secondary constructor: $species with default count")
}
fun display() = println("$species: $count birds")
}
val sparrow = Bird("Sparrow", 10)
sparrow.display()
val eagle = Bird("Eagle") // Secondary constructor
eagle.display()
// Class with multiple init blocks
class BirdWithData(species: String, count: Int) {
val species = species.uppercase()
val count: Int
init {
println("First init block")
println("Species: $species")
}
init {
this.count = if (count >= 0) count else 0
println("Second init block - count: $this.count")
}
fun info() = "$species: $count birds"
}
val hawk = BirdWithData("Hawk", 5)
println(hawk.info())
// Class with property validation
class BirdCounter {
var maxBirds: Int = 100
set(value) {
require(value > 0) { "Max birds must be positive" }
field = value
}
private var count = 0
fun add(number: Int) {
require(number > 0) { "Must add at least 1" }
require(count + number <= maxBirds) { "Would exceed max birds" }
count += number
println("Added $number. Total: $count")
}
fun getCount() = count
}
val counter = BirdCounter()
counter.maxBirds = 50
counter.add(10)
counter.add(20)
// counter.add(30) // Would fail: exceeds max
println("Total: ${counter.getCount()}")
}
The primary constructor is in the class header. init blocks run during initialization in order. Secondary constructors must call the primary constructor using this. require() throws IllegalArgumentException if the condition fails. field refers to the backing field in custom setters.
8.4 Inheritance
Use open for base classes and override for overridden methods.
- The
openkeyword marks a class as inheritable, allowing other classes to extend it. override– Overrides a method from the base class.super– Calls the base class implementation.abstract– Abstract classes and methods.final– Prevents further overriding.
Code Example:
fun main() {
// Base class (must be open)
open class Animal(val name: String) {
open fun makeSound() {
println("$name makes a sound")
}
open val species: String = "Animal"
fun eat() {
println("$name is eating")
}
}
// Derived class
class Bird(name: String, val canFly: Boolean = true) : Animal(name) {
override fun makeSound() {
println("$name chirps")
}
override val species: String = "Bird"
fun fly() {
if (canFly) {
println("$name is flying")
} else {
println("$name cannot fly")
}
}
}
// Another derived class
class Eagle(name: String) : Bird(name, true) {
override fun makeSound() {
super.makeSound() // Calls Bird's makeSound
println("$name screeches loudly!")
}
override val species: String = "Eagle"
fun hunt() {
println("$name is hunting")
}
}
// Testing inheritance
val animal = Animal("Generic Animal")
animal.makeSound()
val sparrow = Bird("Sparrow")
sparrow.makeSound()
sparrow.eat() // Inherited from Animal
sparrow.fly()
val eagle = Eagle("Golden Eagle")
eagle.makeSound()
eagle.hunt()
// Abstract class
abstract class BirdFeature {
abstract val featherColor: String
abstract fun describe()
open fun display() {
println("Color: $featherColor")
}
}
class SparrowDetail : BirdFeature() {
override val featherColor: String = "Brown"
override fun describe() {
println("Sparrow is a small, brown bird")
}
override fun display() {
super.display()
println("Description: ${featherColor}")
}
}
val detail = SparrowDetail()
detail.describe()
detail.display()
// Polymorphism
val animals: List<Animal> = listOf(
Bird("Robin"),
Eagle("Bald Eagle")
)
for (a in animals) {
a.makeSound()
}
}
Classes are final by default; use open to allow inheritance. override marks overridden methods. super calls the parent implementation. Abstract classes cannot be instantiated and can have abstract methods. Polymorphism allows treating derived objects as base types.
8.5 Interfaces
Interfaces define contracts that classes implement. They can have properties and default implementations.
- Interface definition –
interfacekeyword. - Abstract methods – Methods without implementation.
- Default implementations – Methods with body.
- Properties – Can be abstract or have getters.
- Multiple interfaces – A class can implement multiple interfaces.
Code Example:
fun main() {
// Interface definition
interface Flyable {
fun fly()
val maxAltitude: Int
val canFly: Boolean
get() = true
fun glide() {
println("Gliding...")
}
}
interface Swimmable {
fun swim()
val canSwim: Boolean
get() = true
}
interface Soundable {
fun makeSound()
val soundLevel: Int
get() = 3
}
// Class implementing multiple interfaces
class Duck(
val name: String
) : Flyable, Swimmable, Soundable {
override fun fly() {
println("$name flies")
}
override fun swim() {
println("$name swims")
}
override fun makeSound() {
println("$name quacks")
}
override val maxAltitude: Int
get() = 500
override val soundLevel: Int
get() = 5
}
// Class implementing interface partially
class Eagle(val name: String) : Flyable {
override fun fly() {
println("$name soars")
}
override val maxAltitude: Int
get() = 3000
}
// Interface with default implementations
interface BirdBehavior {
val isMigratory: Boolean
get() = false
fun migrate() {
println("Migrating...")
}
fun feed()
fun sleep()
}
class Sparrow : BirdBehavior {
override val isMigratory: Boolean
get() = true
override fun feed() {
println("Sparrow eating seeds")
}
override fun sleep() {
println("Sparrow sleeping on a branch")
}
override fun migrate() {
println("Sparrow migrating south")
}
}
// Testing
val duck = Duck("Donald")
duck.fly()
duck.swim()
duck.makeSound()
duck.glide()
println("Max altitude: ${duck.maxAltitude}")
println("Sound level: ${duck.soundLevel}")
val eagle = Eagle("Golden")
eagle.fly()
eagle.glide()
println("Max altitude: ${eagle.maxAltitude}")
val sparrow = Sparrow()
sparrow.feed()
sparrow.sleep()
sparrow.migrate()
println("Migratory: ${sparrow.isMigratory}")
// Interface as type
val flyers: List<Flyable> = listOf(duck, eagle)
for (flyer in flyers) {
flyer.fly()
}
}
Interfaces define contracts without implementation. Classes implement interfaces using : and , for multiple interfaces. Interfaces can have default implementations. Properties in interfaces are abstract or with getters. Multiple interface inheritance is supported.
8.6 Abstract Classes
Abstract classes combine inheritance and abstraction with some implemented methods.
- Abstract class – Cannot be instantiated.
- Abstract methods – Must be implemented by derived classes.
- Concrete methods – Can provide default implementations.
- Properties – Can be abstract or concrete.
- Constructors – Can have constructors for common initialization.
Code Example:
fun main() {
// Abstract class
abstract class Bird(
val species: String,
var age: Int
) {
// Abstract property
abstract val wingSpan: Double
// Abstract method
abstract fun makeSound()
// Concrete method
fun eat() {
println("$species is eating")
}
// Concrete method with default behavior
open fun sleep() {
println("$species is sleeping")
}
// Abstract method with parameter
abstract fun fly(distance: Int)
}
// Derived class implementing abstract members
class Sparrow(
species: String,
age: Int,
override val wingSpan: Double,
val color: String
) : Bird(species, age) {
override fun makeSound() {
println("$species chirps: Chirp chirp!")
}
override fun fly(distance: Int) {
println("$species flutters $distance meters")
}
// Override concrete method
override fun sleep() {
println("$species sleeps on a branch")
}
}
// Another derived class
class Eagle(
species: String,
age: Int,
override val wingSpan: Double,
val habitat: String
) : Bird(species, age) {
override fun makeSound() {
println("$species screeches: SCREECH!")
}
override fun fly(distance: Int) {
println("$species soars $distance meters")
}
}
// Abstract class with concrete properties
abstract class BirdTracker {
protected val birds = mutableListOf<String>()
fun addBird(species: String) {
birds.add(species)
println("Added $species")
}
abstract fun displayStats()
abstract fun findBird(species: String): Boolean
}
class SimpleBirdTracker : BirdTracker() {
override fun displayStats() {
println("Total birds: ${birds.size}")
println("Species: ${birds.joinToString()}")
}
override fun findBird(species: String): Boolean {
return birds.any { it == species }
}
}
// Testing abstract classes
val sparrow = Sparrow("Sparrow", 2, 0.15, "Brown")
sparrow.makeSound()
sparrow.eat()
sparrow.sleep()
sparrow.fly(10)
println("Wing span: ${sparrow.wingSpan}m")
val eagle = Eagle("Golden Eagle", 5, 2.3, "Mountains")
eagle.makeSound()
eagle.eat()
eagle.sleep()
eagle.fly(100)
val tracker = SimpleBirdTracker()
tracker.addBird("Sparrow")
tracker.addBird("Eagle")
tracker.addBird("Hawk")
tracker.displayStats()
println("Found Hawk: ${tracker.findBird("Hawk")}")
println("Found Robin: ${tracker.findBird("Robin")}")
}
Abstract classes cannot be instantiated directly. Abstract methods must be implemented in derived classes. Concrete methods can be overridden. Abstract classes can have constructors for common initialization. Properties can be abstract or concrete. Abstract classes are used when you need a common base with some implementations.
Advanced Kotlin
9.1 Data Classes
data class automatically provides equals(), hashCode(), toString(), copy(), and component functions.
- Auto-generated methods –
equals,hashCode,toString,copy. - Component functions –
component1(),component2(), etc. - Destructuring – Can be decomposed into variables.
- Use cases – DTOs, value objects, model classes.
- Requirements – Must have at least one parameter in the primary constructor.
Code Example:
fun main() {
// Data class
data class Bird(
val species: String,
val count: Int,
val weight: Double
)
// Creating data class instances
val sparrow = Bird("Sparrow", 10, 25.5)
val eagle = Bird("Eagle", 3, 4500.0)
// Auto-generated toString
println(sparrow) // Bird(species=Sparrow, count=10, weight=25.5)
println(eagle) // Bird(species=Eagle, count=3, weight=4500.0)
// Auto-generated equals and hashCode
val sparrow2 = Bird("Sparrow", 10, 25.5)
println(sparrow == sparrow2) // true
println(sparrow.hashCode() == sparrow2.hashCode()) // true
// Auto-generated copy
val sparrow3 = sparrow.copy(count = 15)
println(sparrow3) // Bird(species=Sparrow, count=15, weight=25.5)
val sparrow4 = sparrow.copy(species = "Tree Sparrow", weight = 28.0)
println(sparrow4) // Bird(species=Tree Sparrow, count=10, weight=28.0)
// Destructuring
val (species, count, weight) = sparrow
println("Species: $species, Count: $count, Weight: $weight")
// Data class in collections
val birds = listOf(sparrow, eagle, Bird("Hawk", 5, 1200.0))
val sortedByWeight = birds.sortedBy { it.weight }
val maxWeight = birds.maxByOrNull { it.weight }
val sumWeight = birds.sumOf { it.weight }
println("Sorted by weight: $sortedByWeight")
println("Heaviest: $maxWeight")
println("Total weight: $sumWeight")
// Data class with default values
data class BirdSighting(
val species: String,
val count: Int = 0,
val timestamp: Long = System.currentTimeMillis()
)
val sighting = BirdSighting("Eagle", 3)
println(sighting) // BirdSighting(species=Eagle, count=3, timestamp=...)
// Data class with nullable fields
data class BirdObservation(
val species: String,
val location: String? = null,
val notes: String? = null
)
val obs = BirdObservation("Sparrow", notes = "Flying south")
println(obs)
}
Data classes automatically generate essential methods. copy() creates a new instance with some fields modified. Destructuring declarations extract values. Data classes are great for model objects. They must have a primary constructor with at least one parameter.
9.2 Sealed Classes
Sealed classes represent restricted class hierarchies, where all subclasses are known at compile time.
- Sealed class –
sealedkeyword. - Restricted hierarchy – All subclasses are known.
- Subclasses – Must be in the same file.
whenexhaustiveness – Compiler ensures all cases are handled.- Use cases – State machines, result types, events.
Code Example:
fun main() {
// Sealed class for bird results
sealed class BirdResult {
data class Success(val bird: Bird) : BirdResult()
data class Error(val message: String) : BirdResult()
object NotFound : BirdResult()
object Loading : BirdResult()
}
data class Bird(val species: String, val count: Int)
// Function returning sealed class
fun findBird(species: String): BirdResult {
val birds = listOf(
Bird("Sparrow", 10),
Bird("Eagle", 3),
Bird("Hawk", 5)
)
return when {
species.isEmpty() -> BirdResult.Error("Species cannot be empty")
else -> {
val found = birds.find { it.species == species }
if (found != null) BirdResult.Success(found)
else BirdResult.NotFound
}
}
}
// Handling sealed class (exhaustive when)
fun handleBirdResult(result: BirdResult) {
when (result) {
is BirdResult.Success -> {
println("Found: ${result.bird.species} (${result.bird.count} birds)")
}
is BirdResult.Error -> {
println("Error: ${result.message}")
}
BirdResult.NotFound -> {
println("Bird not found")
}
BirdResult.Loading -> {
println("Loading...")
}
}
}
// Testing
handleBirdResult(findBird("Sparrow")) // Found: Sparrow (10 birds)
handleBirdResult(findBird("Robin")) // Bird not found
handleBirdResult(findBird("")) // Error: Species cannot be empty
handleBirdResult(BirdResult.Loading) // Loading...
// Sealed class for bird events
sealed class BirdEvent {
data class BirdSighted(val species: String) : BirdEvent()
data class BirdCounted(val species: String, val count: Int) : BirdEvent()
object CheckComplete : BirdEvent()
data class Error(val message: String) : BirdEvent()
}
fun processEvent(event: BirdEvent) {
when (event) {
is BirdEvent.BirdSighted -> {
println("Sighted: ${event.species}")
}
is BirdEvent.BirdCounted -> {
println("Counted: ${event.species} (${event.count})")
}
BirdEvent.CheckComplete -> {
println("Check complete")
}
is BirdEvent.Error -> {
println("Event error: ${event.message}")
}
}
}
processEvent(BirdEvent.BirdSighted("Eagle"))
processEvent(BirdEvent.BirdCounted("Sparrow", 15))
processEvent(BirdEvent.CheckComplete)
processEvent(BirdEvent.Error("Network issue"))
// Sealed class with type parameters
sealed class Result<T> {
data class Success<T>(val data: T) : Result<T>()
data class Error<T>(val message: String) : Result<T>()
data class Loading<T>(val progress: Int) : Result<T>()
}
fun loadBirds(): Result<List<String>> {
return Result.Success(listOf("Eagle", "Sparrow", "Hawk"))
}
when (val result = loadBirds()) {
is Result.Success -> {
println("Birds: ${result.data}")
}
is Result.Error -> {
println("Error: ${result.message}")
}
is Result.Loading -> {
println("Loading: ${result.progress}%")
}
}
}
Sealed classes restrict inheritance to known subclasses. The compiler can verify when is exhaustive. This makes them ideal for state machines, result types, and event handling. Subclasses must be in the same file. Each subclass can have its own properties.
9.3 Extension Functions
Extension functions add new functionality to existing classes without inheritance.
- Syntax –
fun ClassName.functionName() - Extensions – Can be called like regular methods.
- No access to private members – Only public members.
- Extensions are resolved statically – Based on declaration type.
- Extension properties – Can also be added.
Code Example:
fun main() {
// Extension function for String
fun String.isBird(): Boolean {
val birds = listOf("Eagle", "Sparrow", "Hawk", "Robin", "Finch")
return this in birds
}
println("Eagle".isBird()) // true
println("Cat".isBird()) // false
// Extension function with receiver and arguments
fun String.countVowels(): Int {
return this.filter { it in "aeiouAEIOU" }.length
}
println("Sparrow".countVowels()) // 2 (a, o)
println("Eagle".countVowels()) // 2 (E, a)
// Extension function for List
fun <T> List<T>.secondOrNull(): T? {
return if (this.size >= 2) this[1] else null
}
val birds = listOf("Eagle", "Sparrow", "Hawk")
println(birds.secondOrNull()) // Sparrow
println(listOf("Eagle").secondOrNull()) // null
// Extension with default value
fun List<Int>.averageOrZero(): Double {
return if (this.isEmpty()) 0.0 else this.average()
}
val counts = listOf(10, 20, 30, 40, 50)
println(counts.averageOrZero()) // 30.0
println(emptyList<Int>().averageOrZero()) // 0.0
// Extension property
val String.isLong: Boolean
get() = this.length > 5
println("Sparrow".isLong) // true
println("Eagle".isLong) // false
// Extension for data class
data class Bird(val species: String, val count: Int)
fun Bird.description(): String {
return "$species: $count birds"
}
val sparrow = Bird("Sparrow", 10)
println(sparrow.description()) // Sparrow: 10 birds
// Generic extension
fun <T> List<T>.printWithIndex() {
for ((index, item) in this.withIndex()) {
println("[$index] $item")
}
}
listOf("Eagle", "Sparrow", "Hawk").printWithIndex()
// [0] Eagle
// [1] Sparrow
// [2] Hawk
// Extension with lambda parameter
fun <T> List<T>.filterAndTransform(
filter: (T) -> Boolean,
transform: (T) -> String
): List<String> {
return this.filter(filter).map(transform)
}
val result = listOf(1, 2, 3, 4, 5)
.filterAndTransform(
filter = { it % 2 == 0 },
transform = { "Even: $it" }
)
println(result) // [Even: 2, Even: 4]
}
Extension functions add new functionality to existing types. They can’t access private members. Extensions are resolved statically based on the declared type. Extension properties provide additional properties. They’re useful for utility functions and augmenting third-party libraries.
9.4 Generics
Generics allow writing code that works with different types while maintaining type safety.
- Generic classes – Type parameters in class definition.
- Generic functions – Type parameters in function definition.
- Type constraints –
<T : Comparable<T>>. - Variance –
in(contravariant),out(covariant). - Type reification –
inlinewithreifiedfor runtime type information.
Code Example:
fun main() {
// Generic class
class BirdCage<T>(val bird: T) {
fun getBird(): T = bird
fun displayType() {
println("Bird type: ${bird::class.simpleName}")
}
}
val sparrowCage = BirdCage("Sparrow")
val eagleCage = BirdCage(3) // BirdCage<Int>
println(sparrowCage.getBird()) // Sparrow
println(eagleCage.getBird()) // 3
sparrowCage.displayType() // Bird type: String
eagleCage.displayType() // Bird type: Int
// Generic function
fun <T> getSecondElement(list: List<T>): T? {
return if (list.size >= 2) list[1] else null
}
val birds = listOf("Eagle", "Sparrow", "Hawk")
val numbers = listOf(1, 2, 3, 4, 5)
println(getSecondElement(birds)) // Sparrow
println(getSecondElement(numbers)) // 2
// Generic with type constraint
fun <T : Comparable<T>> maxOf(a: T, b: T): T {
return if (a > b) a else b
}
println(maxOf(10, 20)) // 20
println(maxOf("apple", "banana")) // banana
// println(maxOf(10, "20")) // ERROR: Type mismatch
// Generic with multiple constraints
fun <T> processData(data: T) where T : CharSequence, T : Comparable<T> {
println("Length: ${data.length}")
println("Compare to 'Bird': ${data.compareTo("Bird")}")
}
processData("Sparrow")
// processData(10) // ERROR: 10 is not a CharSequence
// Generic with reified type (inline)
inline fun <reified T> printType(value: Any) {
if (value is T) {
println("Value is of type ${T::class.simpleName}")
} else {
println("Value is not of type ${T::class.simpleName}")
}
}
printType<String>("Sparrow") // Value is of type String
printType<Int>("Sparrow") // Value is not of type Int
// Generic with variance
interface BirdProducer<out T> {
fun produce(): T
}
interface BirdConsumer<in T> {
fun consume(bird: T)
}
// Generic class with constraints
class BirdList<T : Number>(private val items: List<T>) {
fun average(): Double {
return items.map { it.toDouble() }.average()
}
fun sum(): T {
@Suppress("UNCHECKED_CAST")
return items.reduce { acc, d -> (acc.toDouble() + d.toDouble()) as T }
}
}
val counts = BirdList(listOf(1, 2, 3, 4, 5))
println(counts.average()) // 3.0
// val strings = BirdList(listOf("a", "b")) // ERROR: String is not a Number
}
Generics enable type-safe code reuse. Type parameters are specified in angle brackets. Constraints restrict acceptable types. reified with inline allows runtime type checks. Variance (out and in) controls how generic types can be used in subtyping. Generics are extensively used in collections and functional programming.
9.5 Delegation
Delegation allows forwarding method calls to another object. It’s a powerful pattern for code reuse and composition.
- Class delegation –
bykeyword. - Property delegation –
bywith delegated properties. - Lazy delegation –
lazy { }for lazy initialization. - Observable delegation –
observablefor change tracking. - Delegating to map – Map delegation for flexible properties.
Code Example:
fun main() {
// Class delegation
interface Flyable {
fun fly(): String
fun glide(): String
}
interface Swimmable {
fun swim(): String
}
class BirdFlying : Flyable {
override fun fly() = "Flying high"
override fun glide() = "Gliding smoothly"
}
class BirdSwimming : Swimmable {
override fun swim() = "Swimming in water"
}
class Duck(flying: Flyable, swimming: Swimmable) :
Flyable by flying,
Swimmable by swimming {
fun quack() = "Quack!"
}
val duck = Duck(BirdFlying(), BirdSwimming())
println(duck.fly()) // Flying high
println(duck.glide()) // Gliding smoothly
println(duck.swim()) // Swimming in water
println(duck.quack()) // Quack!
// Property delegation - Lazy initialization
class BirdData {
val species: String by lazy {
println("Computing species...")
"Sparrow"
}
val count: Int by lazy {
println("Computing count...")
10
}
}
val data = BirdData()
println("Before accessing properties")
println(data.species) // Computes and returns "Sparrow"
println(data.count) // Computes and returns 10
println(data.species) // Returns cached value
// Observable delegation
import kotlin.properties.Delegates
class BirdTracker {
var totalBirds: Int by Delegates.observable(0) { _, old, new ->
println("Total birds changed from $old to $new")
}
var currentSpecies: String by Delegates.vetoable("Unknown") { _, old, new ->
if (new.isEmpty()) {
println("Species cannot be empty")
false // Reject change
} else {
println("Species changed from $old to $new")
true // Accept change
}
}
}
val tracker = BirdTracker()
tracker.totalBirds = 10 // Total birds changed from 0 to 10
tracker.totalBirds = 20 // Total birds changed from 10 to 20
tracker.currentSpecies = "Sparrow" // Species changed from Unknown to Sparrow
tracker.currentSpecies = "" // Species cannot be empty (rejected)
println(tracker.currentSpecies) // Sparrow
// Delegating to map
class BirdConfig(map: Map<String, Any>) {
val species: String by map
val count: Int by map
val weight: Double by map
}
val config = BirdConfig(
mapOf(
"species" to "Eagle",
"count" to 3,
"weight" to 4500.0
)
)
println("Species: ${config.species}")
println("Count: ${config.count}")
println("Weight: ${config.weight}")
// Custom delegation
class NotEmptyString(var value: String = "") {
operator fun getValue(thisRef: Any?, property: Any): String {
return value
}
operator fun setValue(thisRef: Any?, property: Any, newValue: String) {
value = if (newValue.isEmpty()) "Default" else newValue
}
}
class Bird {
var species: String by NotEmptyString()
}
val bird = Bird()
bird.species = "Sparrow"
println(bird.species) // Sparrow
bird.species = ""
println(bird.species) // Default
}
Class delegation forwards method calls to the delegated object. Property delegation handles property access. lazy initializes values on first access. observable tracks changes. vetoable allows rejecting changes. Map delegation reads values from a map. Custom delegates provide full control over property behavior.
Concurrency
10.1 Coroutines
Coroutines provide lightweight concurrency without blocking threads.
- Lightweight – Many coroutines on few threads.
- Suspend functions – Can pause and resume.
- Coroutine builders –
launch,async,runBlocking. - Structured concurrency – Coroutines follow parent-child relationships.
- Cancelation – Coroutines can be canceled.
Code Example:
import kotlinx.coroutines.*
fun main() = runBlocking {
println("Start coroutine")
// Launch a coroutine
launch {
delay(1000L)
println("World!")
}
println("Hello")
// Output: Hello (then after 1 second) World!
// Multiple coroutines
launch {
for (i in 1..3) {
println("Coroutine 1: $i")
delay(100)
}
}
launch {
for (i in 1..3) {
println("Coroutine 2: $i")
delay(150)
}
}
// Wait for both to complete
delay(500)
println("All done")
// Async/await pattern
suspend fun fetchBirdData(): String {
delay(1000)
return "Bird data: Sparrow, Eagle, Hawk"
}
suspend fun fetchBirdCount(): Int {
delay(800)
return 10
}
val deferredData = async { fetchBirdData() }
val deferredCount = async { fetchBirdCount() }
val result = "${deferredData.await()} (${deferredCount.await()} birds)"
println(result)
// Coroutine with exception handling
suspend fun riskyOperation(): Int {
delay(500)
throw IllegalStateException("Something went wrong!")
return 42
}
try {
val number = async { riskyOperation() }.await()
println("Number: $number")
} catch (e: Exception) {
println("Caught exception: ${e.message}")
}
}
runBlocking creates a coroutine scope for the main thread. launch starts a new coroutine. delay suspends without blocking. async returns a Deferred for results. await gets the result. Coroutines are lightweight and use suspension instead of blocking. Exception handling works with try-catch.
10.2 Structured Concurrency
Coroutines run in scopes, ensuring parent coroutines wait for children to complete.
- CoroutineScope – Defines the lifecycle of coroutines.
- Parent-child relationship – Parent waits for children.
- Cancelation propagation – Canceling parent cancels children.
- Job – Represents a coroutine’s lifecycle.
- SupervisorJob – Allows children to fail independently.
Code Example:
import kotlinx.coroutines.*
fun main() = runBlocking {
// Structured concurrency
println("Parent scope started")
launch {
println("Child 1 started")
delay(500)
println("Child 1 done")
}
launch {
println("Child 2 started")
delay(1000)
println("Child 2 done")
}
println("Parent scope waiting")
// Parent waits automatically for all children
println("Parent scope done")
// Custom scope with cancelation
coroutineScope {
val job = launch {
try {
repeat(10) { i ->
println("Working: $i")
delay(100)
}
} catch (e: CancellationException) {
println("Coroutine canceled: ${e.message}")
} finally {
println("Cleanup done")
}
}
delay(250)
println("Canceling coroutine")
job.cancel()
}
// SupervisorScope - isolated failures
supervisorScope {
val child1 = launch {
throw RuntimeException("Child 1 failed")
}
val child2 = launch {
delay(100)
println("Child 2 still running")
}
// Wait for both
delay(200)
println("Supervisor done")
}
// Context and dispatcher
suspend fun longRunningTask() {
withContext(Dispatchers.IO) {
delay(1000)
println("Task completed on IO thread")
}
}
launch {
longRunningTask()
}
delay(1500)
}
CoroutineScope defines lifecycle boundaries. Parent coroutines wait for children to complete. Canceling a parent cancels all children. supervisorScope allows children to fail independently. withContext changes the dispatcher for a block. Structured concurrency ensures proper cleanup and cancelation
10.3 Async/Await
async and await enable concurrent execution of suspend functions.
- The
asyncfunction launches a coroutine and immediately provides aDeferredobject that represents the result that will become available later. await– Waits for the result of aDeferred.- Concurrent execution – Multiple async operations run in parallel.
- Result aggregation – Combine results from multiple async tasks.
- Error handling – Exceptions are propagated on
await.
Code Example:
import kotlinx.coroutines.*
suspend fun getBirdSpecies(): String {
delay(1000)
return "Sparrow, Eagle, Hawk"
}
suspend fun getBirdCounts(): Map<String, Int> {
delay(800)
return mapOf("Sparrow" to 10, "Eagle" to 3, "Hawk" to 5)
}
suspend fun getBirdWeights(): Map<String, Double> {
delay(1200)
return mapOf("Sparrow" to 25.5, "Eagle" to 4500.0, "Hawk" to 1200.0)
}
fun main() = runBlocking {
println("Starting async operations...")
// Concurrent async calls
val speciesDeferred = async { getBirdSpecies() }
val countsDeferred = async { getBirdCounts() }
val weightsDeferred = async { getBirdWeights() }
// Results are obtained in parallel
val species = speciesDeferred.await()
val counts = countsDeferred.await()
val weights = weightsDeferred.await()
println("Species: $species")
println("Counts: $counts")
println("Weights: $weights")
// Parallel processing with transform
suspend fun processData(): List<Pair<String, Int>> {
val species = listOf("Sparrow", "Eagle", "Hawk")
val counts = species.map { species ->
async { countBirdsBySpecies(species) }
}
return counts.map { it.await() }
}
suspend fun countBirdsBySpecies(species: String): Pair<String, Int> {
delay(300)
return species to (1..20).random()
}
val results = processData()
println("Processed results: $results")
// Timeouts
suspend fun slowOperation(): String {
delay(2000)
return "Slow result"
}
try {
val result = withTimeout(1000) {
slowOperation()
}
println(result)
} catch (e: TimeoutCancellationException) {
println("Operation timed out")
}
// Race condition with select
suspend fun computeValue1(): Int {
delay(400)
return 10
}
suspend fun computeValue2(): Int {
delay(200)
return 20
}
// No built-in select in Kotlin coroutines library
// Use first completed via race
val value = try {
withTimeout(500) {
async { computeValue1() }.await()
}
} catch (e: TimeoutCancellationException) {
println("First computation timed out")
computeValue2()
}
println("Value: $value")
}
async starts concurrent operations. await waits for results. Multiple async calls run in parallel, reducing total time. withTimeout cancels if execution exceeds the limit. Error handling propagates exceptions when await is called.
10.4 Flows
Flows are cold asynchronous streams that emit values over time.
- Cold stream – Starts when collected.
- Emits values –
emitto send values. - Operators –
map,filter,transform, etc. - Collectors –
collectto receive values. - Backpressure – Flow handles backpressure automatically.
Code Example:
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
fun main() = runBlocking {
// Creating a flow
fun simpleFlow(): Flow<Int> = flow {
for (i in 1..5) {
delay(100)
emit(i)
}
}
// Collecting a flow
println("Simple flow:")
simpleFlow().collect { value ->
println(value)
}
// Flow with transformations
println("\nTransformed flow:")
flow {
for (i in 1..10) {
emit(i)
}
}
.filter { it % 2 == 0 }
.map { "Even: $it" }
.take(3)
.collect { println(it) }
// Flow with different source
val birdFlow = flow {
val birds = listOf("Eagle", "Sparrow", "Hawk", "Robin", "Cardinal")
for (bird in birds) {
delay(200)
emit(bird)
}
}
println("\nBird flow:")
birdFlow
.filter { it.length > 4 }
.map { it.uppercase() }
.collect { println("Bird: $it") }
// Flow with error handling
fun errorFlow(): Flow<Int> = flow {
for (i in 1..5) {
if (i == 3) {
throw RuntimeException("Error at $i")
}
emit(i)
}
}.catch { e ->
println("Caught: ${e.message}")
emit(-1)
}
println("\nError flow:")
errorFlow().collect { println("Value: $it") }
// Creating flow from list
val listFlow = listOf(10, 20, 30, 40, 50).asFlow()
println("\nList flow:")
listFlow
.onEach { println("Processing $it") }
.collect { println("Result: ${it * 2}") }
// State flow and shared flow
val stateFlow = MutableStateFlow(0)
// Launch a coroutine to update state
launch {
for (i in 1..5) {
delay(500)
stateFlow.value = i
}
}
println("\nStateFlow:")
stateFlow.take(6).collect { println("State: $it") }
// Shared flow
val sharedFlow = MutableSharedFlow<String>()
// Emit values
launch {
delay(100)
sharedFlow.emit("Eagle")
delay(100)
sharedFlow.emit("Sparrow")
delay(100)
sharedFlow.emit("Hawk")
}
println("\nSharedFlow:")
sharedFlow.take(3).collect { println("Shared: $it") }
}
Flows emit values over time. flow { } creates a flow. emit sends values. collect receives values. Transformations like filter, map, and take work lazily. catch handles errors. StateFlow and SharedFlow are hot flows with persistent state.
10.5 Channels
Channels provide communication between coroutines.
- Channel – Communication pipeline between coroutines.
- Send –
sendto put a value. - Receive –
receiveto get a value. - Buffered – Can have a buffer for capacity.
- Close –
closeto indicate no more values.
Code Example:
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.*
fun main() = runBlocking {
// Simple channel
val channel = Channel<Int>()
// Producer
launch {
for (x in 1..5) {
channel.send(x * x)
println("Sent: ${x * x}")
delay(100)
}
channel.close()
println("Channel closed")
}
// Consumer
println("Receiving:")
for (y in channel) {
println("Received: $y")
}
// Channel with buffer
val bufferedChannel = Channel<Int>(3)
// Sender
launch {
for (x in 1..5) {
bufferedChannel.send(x)
println("Sent to buffer: $x")
}
bufferedChannel.close()
}
// Receiver
launch {
delay(500)
for (y in bufferedChannel) {
println("Received from buffer: $y")
delay(100)
}
}
delay(1500)
// Produce/consume pattern
fun CoroutineScope.produceBirds(): ReceiveChannel<String> = produce {
val birds = listOf("Eagle", "Sparrow", "Hawk", "Robin")
for (bird in birds) {
send(bird)
delay(200)
}
}
println("\nProducer pattern:")
val birdChannel = produceBirds()
birdChannel.consumeEach { bird ->
println("Consumed: $bird")
}
// Fan-out pattern
fun fanOutExample() = runBlocking {
val jobsChannel = Channel<String>(10)
// Producer
launch {
repeat(10) { i ->
jobsChannel.send("Job-$i")
delay(100)
}
jobsChannel.close()
}
// Multiple consumers
repeat(3) { workerId ->
launch {
for (job in jobsChannel) {
println("Worker $workerId processing $job")
delay(150)
}
}
}
}
fanOutExample()
// Channel with timeout
val timeoutChannel = Channel<Int>()
launch {
try {
withTimeout(1000) {
for (x in 1..10) {
timeoutChannel.send(x)
delay(200)
}
}
} catch (e: Exception) {
println("Timeout occurred")
timeoutChannel.close()
}
}
launch {
for (y in timeoutChannel) {
println("Received: $y")
}
}
delay(2000)
}
Channels allow coroutines to communicate. send blocks if the channel is full. receive blocks if the channel is empty. Buffered channels can hold multiple values. produce creates a producer coroutine. consumeEach automatically iterates. Fan-out distributes work among multiple consumers.
File Handling and Serialization
11.1 Reading Files
Kotlin provides convenient functions for reading text and binary files.
readText()– Reads entire file as a string.readLines()– Reads file into list of lines.readBytes()– Reads binary file into a byte array.use– Ensures resources are closed.bufferedReader()– Efficient reading of large files.
Code Example:
import java.io.File
import java.io.IOException
fun main() {
// Create a test file
val testFile = File("birds.txt")
testFile.writeText("Sparrow\nEagle\nHawk\nRobin\nCardinal")
// Read entire file as string
val content = testFile.readText()
println("File content:\n$content")
// Read lines into list
val lines = testFile.readLines()
println("Lines: $lines")
println("First line: ${lines.first()}")
println("Number of lines: ${lines.size}")
// Read with buffered reader (for large files)
val reader = testFile.bufferedReader()
reader.use {
val firstLine = it.readLine()
println("First line: $firstLine")
val allLines = it.readLines()
println("All lines: $allLines")
}
// Read character by character
val charFile = File("characters.txt")
charFile.writeText("Hello, Kotlin!")
val charReader = charFile.bufferedReader()
charReader.use {
var char = it.read()
while (char != -1) {
print(char.toChar())
char = it.read()
}
println()
}
// Read binary file
val binaryFile = File("data.bin")
binaryFile.writeBytes(byteArrayOf(10, 20, 30, 40, 50))
val bytes = binaryFile.readBytes()
println("Bytes: ${bytes.joinToString()}")
// Safe reading with error handling
try {
val missingFile = File("missing.txt")
val content2 = missingFile.readText()
println(content2)
} catch (e: IOException) {
println("Error reading file: ${e.message}")
}
// Reading to different formats
fun readNumbers(file: File): List<Int> {
return file.readLines().mapNotNull { it.toIntOrNull() }
}
val numbersFile = File("numbers.txt")
numbersFile.writeText("10\n20\n30\n40\n50")
val numbers = readNumbers(numbersFile)
println("Numbers: $numbers")
println("Sum: ${numbers.sum()}")
}
readText() and readLines() handle common file reading tasks. use ensures resources are closed. bufferedReader() provides efficient reading. Error handling catches IOException. toIntOrNull() safely converts strings to integers.
11.2 Writing Files
Kotlin provides convenient functions for writing text and binary files.
writeText()– Writes a string to a file.appendText()– Appends to existing file.writeBytes()– Writes binary data.bufferedWriter()– Efficient writing for large files.printWriter()– Formatted writing.
Code Example:
import java.io.File
import java.io.IOException
fun main() {
// Write text file
val birdsFile = File("birds_output.txt")
birdsFile.writeText("Sparrow\nEagle\nHawk\n")
println("Written to file")
// Append to file
birdsFile.appendText("Robin\nCardinal\n")
println("Appended to file")
// Read back
println("File content:\n${birdsFile.readText()}")
// Write with buffered writer (for large files)
val largeFile = File("large_output.txt")
val writer = largeFile.bufferedWriter()
writer.use {
for (i in 1..10) {
it.write("Line $i\n")
}
}
println("Wrote 10 lines with buffered writer")
// Write with print writer (formatted output)
val formattedFile = File("formatted.txt")
val printWriter = formattedFile.printWriter()
printWriter.use {
it.printf("Species: %s, Count: %d, Weight: %.2f\n", "Sparrow", 10, 25.5)
it.printf("Species: %s, Count: %d, Weight: %.2f\n", "Eagle", 3, 4500.0)
}
println("Formatted output:\n${formattedFile.readText()}")
// Write binary file
val binaryFile = File("binary_output.bin")
val bytes = byteArrayOf(10, 20, 30, 40, 50)
binaryFile.writeBytes(bytes)
val readBytes = binaryFile.readBytes()
println("Written bytes: ${readBytes.joinToString()}")
// Writing collections
fun writeBirds(file: File, birds: List<String>) {
file.writeText(birds.joinToString("\n"))
}
val birdList = listOf("Eagle", "Sparrow", "Hawk", "Robin")
val listFile = File("bird_list.txt")
writeBirds(listFile, birdList)
println("Written list:\n${listFile.readText()}")
// Writing with error handling
fun safeWrite(file: File, content: String) {
try {
file.writeText(content)
println("Successfully wrote to ${file.name}")
} catch (e: IOException) {
println("Error writing file: ${e.message}")
}
}
safeWrite(File("safe_write.txt"), "Safe content")
// Writing CSV format
data class Bird(val species: String, val count: Int, val weight: Double)
val birds = listOf(
Bird("Sparrow", 10, 25.5),
Bird("Eagle", 3, 4500.0),
Bird("Hawk", 5, 1200.0)
)
val csvFile = File("birds.csv")
csvFile.writeText("Species,Count,Weight\n")
birds.forEach {
csvFile.appendText("${it.species},${it.count},${it.weight}\n")
}
println("CSV content:\n${csvFile.readText()}")
}
writeText() writes a string to a file, overwriting existing content. appendText() adds to the end. bufferedWriter() handles large writes efficiently. printWriter() supports formatted output. Error handling ensures file operations are safe. CSV and other formats are easily written with these functions.
11.3 JSON Processing
kotlinx.serialization provides type-safe JSON serialization and deserialization.
@Serializable– Marks classes for serialization.Json.encodeToString()– Converts object to JSON.Json.decodeFromString()– Converts JSON to object.- Custom serializers – For custom behavior.
- Polymorphism – Supports sealed classes and interfaces.
Code Example:
import kotlinx.serialization.*
import kotlinx.serialization.json.*
import kotlinx.serialization.modules.*
fun main() {
// Simple serialization
@Serializable
data class Bird(val species: String, val count: Int, val weight: Double)
val sparrow = Bird("Sparrow", 10, 25.5)
// Serialize to JSON
val json = Json.encodeToString(sparrow)
println("JSON: $json")
// {"species":"Sparrow","count":10,"weight":25.5}
// Deserialize from JSON
val decoded = Json.decodeFromString<Bird>(json)
println("Decoded: $decoded")
// Bird(species=Sparrow, count=10, weight=25.5)
// Serialize with pretty printing
val prettyJson = Json {
prettyPrint = true
encodeDefaults = true
}
val pretty = prettyJson.encodeToString(sparrow)
println("Pretty JSON:\n$pretty")
// Nested objects
@Serializable
data class BirdSighting(
val bird: Bird,
val location: String,
val timestamp: Long,
val notes: String? = null
)
val sighting = BirdSighting(
bird = Bird("Eagle", 3, 4500.0),
location = "Mountains",
timestamp = System.currentTimeMillis(),
notes = "Flying south"
)
val sightingJson = Json.encodeToString(sighting)
println("Sighting JSON: $sightingJson")
// Collections
@Serializable
data class BirdList(val birds: List<Bird>)
val birdList = BirdList(
listOf(
Bird("Sparrow", 10, 25.5),
Bird("Eagle", 3, 4500.0),
Bird("Hawk", 5, 1200.0)
)
)
val listJson = Json.encodeToString(birdList)
println("List JSON: $listJson")
val decodedList = Json.decodeFromString<BirdList>(listJson)
println("Decoded list: ${decodedList.birds}")
// Polymorphic serialization
@Serializable
sealed class Animal {
abstract val name: String
}
@Serializable
data class BirdAnimal(override val name: String, val canFly: Boolean) : Animal()
@Serializable
data class MammalAnimal(override val name: String, val furColor: String) : Animal()
// Configure polymorphic serialization
val polymorphicJson = Json {
serializersModule = SerializersModule {
polymorphic(Animal::class) {
subclass(BirdAnimal::class, BirdAnimal.serializer())
subclass(MammalAnimal::class, MammalAnimal.serializer())
}
}
}
val animals: List<Animal> = listOf(
BirdAnimal("Eagle", true),
MammalAnimal("Lion", "Golden")
)
val animalJson = polymorphicJson.encodeToString(animals)
println("Polymorphic JSON: $animalJson")
// Custom serializer
@Serializable
class BirdCounter(@Serializable(with = CustomSerializer::class) val count: Int)
object CustomSerializer : KSerializer<Int> {
override val descriptor = PrimitiveSerialDescriptor("Count", PrimitiveKind.INT)
override fun serialize(encoder: Encoder, value: Int) {
encoder.encodeInt(value.coerceAtLeast(0))
}
override fun deserialize(decoder: Decoder): Int {
return decoder.decodeInt().coerceAtLeast(0)
}
}
val counter = BirdCounter(10)
val counterJson = Json.encodeToString(counter)
println("Counter JSON: $counterJson")
}
@Serializable enables automatic JSON serialization. Json.encodeToString converts objects to JSON. Json.decodeFromString converts JSON to objects. Polymorphic serialization requires a module. Custom serializers provide fine-grained control.
11.4 Configuration Files
Reading and writing configuration files in various formats.
- Properties files –
java.util.Properties. - JSON config – Using kotlinx.serialization.
- YAML – Using third-party libraries.
- Environment variables – System.getenv().
- Command line args – main() parameters.
Code Example:
import java.io.File
import java.util.Properties
import kotlinx.serialization.*
import kotlinx.serialization.json.*
fun main() {
// ---- PROPERTIES FILE ----
data class Config(val host: String, val port: Int, val debug: Boolean)
fun loadProperties(file: File): Config {
val props = Properties()
file.inputStream().use { props.load(it) }
return Config(
host = props.getProperty("database.host", "localhost"),
port = props.getProperty("database.port", "5432").toInt(),
debug = props.getProperty("app.debug", "false").toBoolean()
)
}
fun saveProperties(file: File, config: Config) {
val props = Properties()
props.setProperty("database.host", config.host)
props.setProperty("database.port", config.port.toString())
props.setProperty("app.debug", config.debug.toString())
file.outputStream().use { props.store(it, "Application Configuration") }
}
val configFile = File("config.properties")
configFile.writeText("""
database.host=localhost
database.port=5432
app.debug=true
""".trimIndent())
val config = loadProperties(configFile)
println("Loaded config: $config")
val newConfig = Config("prod-server.com", 8080, false)
saveProperties(File("new_config.properties"), newConfig)
println("Saved new config")
// ---- JSON CONFIGURATION ----
@Serializable
data class JsonConfig(
val database: DatabaseConfig,
val app: AppConfig,
val logging: LoggingConfig
)
@Serializable
data class DatabaseConfig(
val host: String,
val port: Int,
val name: String,
val username: String? = null,
val password: String? = null
)
@Serializable
data class AppConfig(
val name: String,
val version: String,
val debug: Boolean = false
)
@Serializable
data class LoggingConfig(
val level: String = "INFO",
val file: String = "app.log"
)
val jsonConfig = JsonConfig(
database = DatabaseConfig("localhost", 5432, "birds_db"),
app = AppConfig("BirdTracker", "1.0", true),
logging = LoggingConfig("DEBUG", "birds.log")
)
val jsonString = Json.encodeToString(jsonConfig)
println("JSON Config:\n$jsonString")
val parsedConfig = Json.decodeFromString<JsonConfig>(jsonString)
println("Parsed config: ${parsedConfig.app.name}")
// ---- ENVIRONMENT VARIABLES ----
fun loadFromEnv(): Config {
val host = System.getenv("DB_HOST") ?: "localhost"
val port = System.getenv("DB_PORT")?.toIntOrNull() ?: 5432
val debug = System.getenv("APP_DEBUG")?.toBoolean() ?: false
return Config(host, port, debug)
}
val envConfig = loadFromEnv()
println("Environment config: $envConfig")
// ---- COMMAND LINE ARGUMENTS ----
fun parseArgs(args: Array<String>): Config {
var host = "localhost"
var port = 5432
var debug = false
for (i in args.indices step 2) {
when (args[i]) {
"--host" -> host = args.getOrElse(i + 1) { host }
"--port" -> port = args.getOrElse(i + 1) { "5432" }.toIntOrNull() ?: port
"--debug" -> debug = args.getOrElse(i + 1) { "false" }.toBoolean()
}
}
return Config(host, port, debug)
}
// Simulate command line arguments
val args = arrayOf("--host", "192.168.1.100", "--port", "8080", "--debug", "true")
val cliConfig = parseArgs(args)
println("Command line config: $cliConfig")
}
Properties files use java.util.Properties. JSON config uses kotlinx.serialization. Environment variables are accessed with System.getenv(). Command-line arguments are parsed from main parameters. Configuration can be layered from multiple sources.
Testing
12.1 Unit Testing
Unit tests verify individual components in isolation. JUnit is a widely used testing framework for Java applications, commonly used to create and run automated unit tests.
- JUnit – Standard testing framework.
- Annotations –
@Test,@BeforeEach,@AfterEach. - Assertions –
assertEquals,assertTrue,assertThrows. - Test naming – Clear, descriptive test names.
- Arrange-Act-Assert – Standard test structure.
Code Example:
import org.junit.jupiter.api.Assertions.*
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.AfterEach
class BirdTest {
private lateinit var birdTracker: BirdTracker
@BeforeEach
fun setUp() {
birdTracker = BirdTracker()
println("Setup complete")
}
@AfterEach
fun tearDown() {
println("Teardown complete")
}
@Test
fun testAddBird() {
// Arrange
val species = "Sparrow"
val count = 10
// Act
birdTracker.addBird(species, count)
// Assert
assertEquals(count, birdTracker.getCount(species))
assertEquals(1, birdTracker.getUniqueSpecies().size)
}
@Test
fun testAddBirdMultipleTimes() {
// Arrange
val species = "Eagle"
// Act
birdTracker.addBird(species, 3)
birdTracker.addBird(species, 5)
// Assert
assertEquals(8, birdTracker.getCount(species))
}
@Test
fun testAddBirdNegativeCount() {
// Arrange & Act & Assert
assertThrows(IllegalArgumentException::class.java) {
birdTracker.addBird("Hawk", -5)
}
}
@Test
fun testGetUniqueSpecies() {
// Arrange
birdTracker.addBird("Sparrow", 10)
birdTracker.addBird("Eagle", 3)
birdTracker.addBird("Sparrow", 5)
birdTracker.addBird("Hawk", 2)
// Act
val unique = birdTracker.getUniqueSpecies()
// Assert
assertEquals(3, unique.size)
assertTrue(unique.contains("Sparrow"))
assertTrue(unique.contains("Eagle"))
assertTrue(unique.contains("Hawk"))
}
@Test
fun testGetTotalBirds() {
// Arrange
birdTracker.addBird("Sparrow", 10)
birdTracker.addBird("Eagle", 3)
birdTracker.addBird("Hawk", 5)
// Act
val total = birdTracker.getTotalBirds()
// Assert
assertEquals(18, total)
}
}
class BirdTracker {
private val birds = mutableMapOf<String, Int>()
fun addBird(species: String, count: Int) {
require(count >= 0) { "Count must be non-negative" }
birds[species] = (birds[species] ?: 0) + count
}
fun getCount(species: String): Int = birds[species] ?: 0
fun getUniqueSpecies(): Set<String> = birds.keys
fun getTotalBirds(): Int = birds.values.sum()
}
@Test marks test methods. @BeforeEach runs before each test. assertEquals compares expected and actual values. assertThrows verifies exceptions. Tests follow Arrange-Act-Assert pattern. Each test should be independent and isolated.
12.2 Integration Testing
Integration tests verify interactions between components.
- @Test – Standard test annotation.
- @TempDir – Temporary directory for file tests.
- File I/O – Test file operations.
- Database tests – In-memory databases.
- Network tests – Mock external services.
Code Example:
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.io.TempDir
import java.io.File
import kotlin.test.assertEquals
import kotlin.test.assertTrue
class BirdFileProcessorTest {
@TempDir
lateinit var tempDir: File
@Test
fun testProcessBirdFile() {
// Arrange
val processor = BirdFileProcessor()
val inputFile = tempDir.resolve("birds.txt")
inputFile.writeText("""
Sparrow,10,25.5
Eagle,3,4500.0
Hawk,5,1200.0
""".trimIndent())
val outputFile = tempDir.resolve("processed.txt")
// Act
val result = processor.processBirdFile(inputFile, outputFile)
// Assert
assertEquals(3, result)
assertTrue(outputFile.exists())
val content = outputFile.readText()
assertTrue(content.contains("Sparrow"))
assertTrue(content.contains("Eagle"))
assertTrue(content.contains("Hawk"))
}
@Test
fun testProcessBirdFileEmpty() {
// Arrange
val processor = BirdFileProcessor()
val inputFile = tempDir.resolve("empty.txt")
inputFile.writeText("")
val outputFile = tempDir.resolve("empty_processed.txt")
// Act
val result = processor.processBirdFile(inputFile, outputFile)
// Assert
assertEquals(0, result)
assertTrue(outputFile.exists())
assertEquals("", outputFile.readText())
}
@Test
fun testBirdFileValidator() {
// Arrange
val validator = BirdFileValidator()
val validFile = tempDir.resolve("valid.txt")
validFile.writeText("Sparrow,10,25.5\nEagle,3,4500.0")
val invalidFile = tempDir.resolve("invalid.txt")
invalidFile.writeText("Sparrow,10\nEagle,3,4500.0") // Missing weight
// Act & Assert
assertTrue(validator.validateFile(validFile))
assertTrue(!validator.validateFile(invalidFile))
}
}
class BirdFileProcessor {
fun processBirdFile(input: File, output: File): Int {
val lines = input.readLines()
var count = 0
output.bufferedWriter().use { writer ->
for (line in lines) {
val parts = line.split(",")
if (parts.size == 3) {
val species = parts[0].trim()
val countVal = parts[1].trim().toInt()
val weight = parts[2].trim().toDouble()
writer.write("Bird: $species, Count: $countVal, Weight: $weight\n")
count++
}
}
}
return count
}
}
class BirdFileValidator {
fun validateFile(file: File): Boolean {
return try {
val lines = file.readLines()
lines.all { line ->
val parts = line.split(",")
parts.size == 3 && parts.all { it.isNotBlank() }
}
} catch (e: Exception) {
false
}
}
}
@TempDir creates a temporary directory for test files. Integration tests verify that components work together correctly. File operations are tested with real file I/O. Validation ensures input data is correct. Cleanup is automatic with @TempDir.
12.3 Mocking
Mocking replaces dependencies with controlled versions for isolation.
- Mockk – Popular mocking library for Kotlin.
mockk()– Creates a mock object.every { }– Configures mock behavior.verify { }– Verifies interactions.any()– Matches any value.
Code Example:
import io.mockk.*
import org.junit.jupiter.api.Test
import kotlin.test.assertEquals
class BirdServiceTest {
@Test
fun testGetBirdCount() {
// Create mock repository
val repository = mockk<BirdRepository>()
// Configure mock behavior
every { repository.getBirds() } returns listOf(
Bird("Sparrow", 10),
Bird("Eagle", 3),
Bird("Hawk", 5)
)
// Create service with mock
val service = BirdService(repository)
// Act
val total = service.getTotalBirds()
// Assert
assertEquals(18, total)
verify { repository.getBirds() }
}
@Test
fun testFindBird() {
val repository = mockk<BirdRepository>()
every { repository.findBird("Sparrow") } returns Bird("Sparrow", 10)
every { repository.findBird("Robin") } returns null
val service = BirdService(repository)
// Act
val sparrow = service.findBird("Sparrow")
val robin = service.findBird("Robin")
// Assert
assertEquals("Sparrow", sparrow?.species)
assertEquals(10, sparrow?.count)
assertEquals(null, robin)
verify { repository.findBird("Sparrow") }
verify { repository.findBird("Robin") }
}
@Test
fun testExceptionHandling() {
val repository = mockk<BirdRepository>()
every { repository.getBirds() } throws RuntimeException("Database error")
val service = BirdService(repository)
// Act & Assert
try {
service.getTotalBirds()
} catch (e: Exception) {
assertEquals("Database error", e.message)
}
verify { repository.getBirds() }
}
@Test
fun testPartialMock() {
val service = spyk(BirdService(mockk()))
every { service.getBirdSpecies() } returns listOf("Eagle")
// Real implementation for some methods, mocked for others
val count = service.countBirdSpecies("Eagle")
assertEquals(1, count)
verify { service.getBirdSpecies() }
}
}
data class Bird(val species: String, val count: Int)
class BirdService(val repository: BirdRepository) {
fun getTotalBirds(): Int {
return repository.getBirds().sumOf { it.count }
}
fun findBird(species: String): Bird? {
return repository.findBird(species)
}
open fun getBirdSpecies(): List<String> {
return repository.getBirds().map { it.species }
}
fun countBirdSpecies(species: String): Int {
return getBirdSpecies().count { it == species }
}
}
interface BirdRepository {
fun getBirds(): List<Bird>
fun findBird(species: String): Bird?
}
mockk() creates a mock. every { } configures expected behavior. verify { } checks interactions. spyk() creates a spy for partial mocking. Mocks isolate the code being tested. This allows testing without external dependencies.
12.4 Test Automation
Automating tests with CI/CD ensures code quality.
- GitHub Actions – CI/CD platform.
- Workflow – Defines automation steps.
- Gradle tasks –
./gradlew test - Test reports – HTML reports for results.
- Coverage tools –
JaCoCofor coverage.
Code Example:
# .github/workflows/kotlin-ci.yml
name: Kotlin CI
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up JDK 17
uses: actions/setup-java@v3
with:
java-version: '17'
distribution: 'temurin'
- name: Grant execute permission for gradlew
run: chmod +x gradlew
- name: Build with Gradle
run: ./gradlew build
- name: Run tests
run: ./gradlew test
- name: Upload test results
uses: actions/upload-artifact@v3
with:
name: test-results
path: build/reports/tests/
- name: Upload coverage report
uses: actions/upload-artifact@v3
with:
name: coverage-report
path: build/reports/jacoco/
Gradle Configuration:
// build.gradle.kts
plugins {
kotlin("jvm") version "1.9.0"
id("jacoco")
}
dependencies {
implementation("org.jetbrains.kotlin:kotlin-stdlib")
testImplementation("org.junit.jupiter:junit-jupiter:5.9.2")
testImplementation("io.mockk:mockk:1.13.5")
}
tasks.test {
useJUnitPlatform()
finalizedBy(tasks.jacocoTestReport)
}
tasks.jacocoTestReport {
dependsOn(tasks.test)
reports {
xml.required.set(true)
html.required.set(true)
}
}
tasks.jacocoTestCoverageVerification {
violationRules {
rule {
limit {
minimum = 0.80.toBigDecimal()
}
}
}
}
Test Configuration:
// src/test/kotlin/TestConfig.kt
class TestConfig {
companion object {
fun createTestBirdService(): BirdService {
val repository = mockk<BirdRepository>()
every { repository.getBirds() } returns listOf(
Bird("Sparrow", 10),
Bird("Eagle", 3)
)
return BirdService(repository)
}
fun createTestData(): List<Bird> {
return listOf(
Bird("Sparrow", 10),
Bird("Eagle", 3),
Bird("Hawk", 5),
Bird("Robin", 8)
)
}
}
}
GitHub Actions runs tests on every push. Gradle handles building and testing. Test reports are generated as HTML. Coverage reports show which code is tested. Quality gates enforce minimum coverage.Automated testing helps verify that code behaves as expected and makes it easier to maintain consistent software quality.
Backend Development
13.1 Ktor
Ktor is a lightweight, asynchronous web framework for building backend applications.
- Embedded server – Runs with embedded Netty.
- Routing – Defines HTTP endpoints.
- Serialization – Supports JSON and other formats.
- Authentication – JWT and other methods.
- WebSockets – Real-time communication.
Code Example:
import io.ktor.application.*
import io.ktor.http.*
import io.ktor.response.*
import io.ktor.request.*
import io.ktor.routing.*
import io.ktor.serialization.json.*
import io.ktor.features.*
import io.ktor.server.engine.*
import io.ktor.server.netty.*
import kotlinx.serialization.Serializable
@Serializable
data class Bird(val id: Int, val species: String, val count: Int)
class BirdService {
private val birds = mutableListOf(
Bird(1, "Sparrow", 10),
Bird(2, "Eagle", 3),
Bird(3, "Hawk", 5)
)
private var nextId = 4
fun getBirds(): List<Bird> = birds
fun getBird(id: Int): Bird? = birds.find { it.id == id }
fun addBird(species: String, count: Int): Bird {
val bird = Bird(nextId++, species, count)
birds.add(bird)
return bird
}
fun updateBird(id: Int, species: String?, count: Int?): Bird? {
val bird = getBird(id) ?: return null
bird.species = species ?: bird.species
bird.count = count ?: bird.count
return bird
}
fun deleteBird(id: Int): Boolean {
return birds.removeIf { it.id == id }
}
}
fun main() {
val birdService = BirdService()
embeddedServer(Netty, port = 8080) {
install(ContentNegotiation) {
json()
}
routing {
// Get all birds
get("/birds") {
call.respond(birdService.getBirds())
}
// Get bird by ID
get("/birds/{id}") {
val id = call.parameters["id"]?.toIntOrNull()
if (id == null) {
call.respond(HttpStatusCode.BadRequest, "Invalid ID")
return@get
}
val bird = birdService.getBird(id)
if (bird != null) {
call.respond(bird)
} else {
call.respond(HttpStatusCode.NotFound, "Bird not found")
}
}
// Create bird
post("/birds") {
val request = call.receive<Bird>()
val bird = birdService.addBird(request.species, request.count)
call.respond(HttpStatusCode.Created, bird)
}
// Update bird
put("/birds/{id}") {
val id = call.parameters["id"]?.toIntOrNull()
if (id == null) {
call.respond(HttpStatusCode.BadRequest, "Invalid ID")
return@put
}
val request = call.receive<Bird>()
val bird = birdService.updateBird(id, request.species, request.count)
if (bird != null) {
call.respond(bird)
} else {
call.respond(HttpStatusCode.NotFound, "Bird not found")
}
}
// Delete bird
delete("/birds/{id}") {
val id = call.parameters["id"]?.toIntOrNull()
if (id == null) {
call.respond(HttpStatusCode.BadRequest, "Invalid ID")
return@delete
}
if (birdService.deleteBird(id)) {
call.respond(HttpStatusCode.NoContent)
} else {
call.respond(HttpStatusCode.NotFound, "Bird not found")
}
}
// Health check
get("/health") {
call.respond(mapOf("status" to "healthy", "birds" to birdService.getBirds().size))
}
}
}.start(wait = true)
}
Ktor’s embedded server runs the application. routing defines endpoints. json() handles JSON serialization. CRUD operations are implemented for birds. Health check provides monitoring. The server runs on port 8080.
13.2 Spring Boot
Spring Boot provides a comprehensive framework for building enterprise applications.
- Dependency injection – Spring container manages beans.
- REST controllers –
@RestControllerdefines endpoints. - Data JPA – Database access with JPA.
- Exception handling – Global exception handlers.
- Testing – Integration with test framework.
Code Example:
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication
import org.springframework.http.HttpStatus
import org.springframework.web.bind.annotation.*
import org.springframework.web.server.ResponseStatusException
import javax.persistence.*
@SpringBootApplication
class BirdApplication
fun main() {
runApplication<BirdApplication>()
}
@Entity
data class Bird(
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
val id: Long = 0,
val species: String,
val count: Int,
val weight: Double
)
interface BirdRepository : org.springframework.data.jpa.repository.JpaRepository<Bird, Long>
@RestController
@RequestMapping("/api/birds")
class BirdController(private val repository: BirdRepository) {
@GetMapping
fun getBirds(): List<Bird> = repository.findAll()
@GetMapping("/{id}")
fun getBird(@PathVariable id: Long): Bird {
return repository.findById(id).orElseThrow {
ResponseStatusException(HttpStatus.NOT_FOUND, "Bird not found")
}
}
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
fun createBird(@RequestBody bird: Bird): Bird = repository.save(bird)
@PutMapping("/{id}")
fun updateBird(@PathVariable id: Long, @RequestBody bird: Bird): Bird {
if (!repository.existsById(id)) {
throw ResponseStatusException(HttpStatus.NOT_FOUND, "Bird not found")
}
return repository.save(bird.copy(id = id))
}
@DeleteMapping("/{id}")
@ResponseStatus(HttpStatus.NO_CONTENT)
fun deleteBird(@PathVariable id: Long) {
if (!repository.existsById(id)) {
throw ResponseStatusException(HttpStatus.NOT_FOUND, "Bird not found")
}
repository.deleteById(id)
}
@GetMapping("/search")
fun searchBirds(@RequestParam species: String): List<Bird> {
return repository.findAll().filter { it.species.contains(species, ignoreCase = true) }
}
}
@RestControllerAdvice
class GlobalExceptionHandler {
@ExceptionHandler(ResponseStatusException::class)
fun handleNotFound(ex: ResponseStatusException): Map<String, String> {
return mapOf("error" to ex.reason ?: "Not found")
}
@ExceptionHandler(Exception::class)
fun handleGeneric(ex: Exception): Map<String, String> {
return mapOf("error" to "Internal server error: ${ex.message}")
}
}
Spring Boot manages beans through dependency injection. @Entity marks data classes. @RestController defines endpoints. JpaRepository provides CRUD operations. Exception handling returns proper HTTP status codes. The application runs with embedded Tomcat.
13.3 REST APIs
REST API design principles and best practices.
- HTTP methods – GET, POST, PUT, DELETE.
- HTTP status codes indicate the outcome of a request. For example, 200 means the request succeeded, 201 indicates that a resource was successfully created, and 404 means the requested resource could not be found.
- Error handling – Consistent error responses.
- Pagination – Limit and offset parameters.
- Filtering – Query parameters for filtering.
Code Example:
import io.ktor.application.*
import io.ktor.http.*
import io.ktor.response.*
import io.ktor.request.*
import io.ktor.routing.*
import io.ktor.features.*
import io.ktor.serialization.json.*
import kotlinx.serialization.Serializable
@Serializable
data class Bird(
val id: Int,
val species: String,
val count: Int,
val weight: Double,
val isMigratory: Boolean
)
@Serializable
data class CreateBirdRequest(
val species: String,
val count: Int,
val weight: Double,
val isMigratory: Boolean
)
@Serializable
data class UpdateBirdRequest(
val species: String?,
val count: Int?,
val weight: Double?,
val isMigratory: Boolean?
)
@Serializable
data class ApiResponse<T>(
val success: Boolean,
val data: T? = null,
val error: String? = null
)
@Serializable
data class PaginatedResponse<T>(
val items: List<T>,
val total: Int,
val page: Int,
val pageSize: Int
)
class BirdService {
private val birds = mutableListOf(
Bird(1, "Sparrow", 10, 25.5, true),
Bird(2, "Eagle", 3, 4500.0, false),
Bird(3, "Hawk", 5, 1200.0, true)
)
private var nextId = 4
fun getBirds(filter: String? = null): List<Bird> {
return if (filter.isNullOrBlank()) {
birds
} else {
birds.filter { it.species.contains(filter, ignoreCase = true) }
}
}
fun getPagedBirds(page: Int, pageSize: Int, filter: String? = null): PaginatedResponse<Bird> {
val all = getBirds(filter)
val start = page * pageSize
val end = minOf(start + pageSize, all.size)
val items = if (start < all.size) all.subList(start, end) else emptyList()
return PaginatedResponse(items, all.size, page, pageSize)
}
fun getBird(id: Int): Bird? = birds.find { it.id == id }
fun addBird(request: CreateBirdRequest): Bird {
val bird = Bird(nextId++, request.species, request.count, request.weight, request.isMigratory)
birds.add(bird)
return bird
}
fun updateBird(id: Int, request: UpdateBirdRequest): Bird? {
val bird = getBird(id) ?: return null
val updated = bird.copy(
species = request.species ?: bird.species,
count = request.count ?: bird.count,
weight = request.weight ?: bird.weight,
isMigratory = request.isMigratory ?: bird.isMigratory
)
val index = birds.indexOfFirst { it.id == id }
birds[index] = updated
return updated
}
fun deleteBird(id: Int): Boolean = birds.removeIf { it.id == id }
}
fun main() {
val birdService = BirdService()
embeddedServer(Netty, port = 8080) {
install(ContentNegotiation) { json() }
routing {
// GET /birds - list all birds with filtering
get("/birds") {
val filter = call.request.queryParameters["filter"]
val birds = birdService.getBirds(filter)
call.respond(ApiResponse(success = true, data = birds))
}
// GET /birds/paged - paginated list
get("/birds/paged") {
val page = call.request.queryParameters["page"]?.toIntOrNull() ?: 0
val pageSize = call.request.queryParameters["pageSize"]?.toIntOrNull() ?: 10
val filter = call.request.queryParameters["filter"]
val result = birdService.getPagedBirds(page, pageSize, filter)
call.respond(ApiResponse(success = true, data = result))
}
// GET /birds/{id} - get single bird
get("/birds/{id}") {
val id = call.parameters["id"]?.toIntOrNull()
if (id == null) {
call.respond(HttpStatusCode.BadRequest, ApiResponse(success = false, error = "Invalid ID"))
return@get
}
val bird = birdService.getBird(id)
if (bird != null) {
call.respond(ApiResponse(success = true, data = bird))
} else {
call.respond(HttpStatusCode.NotFound, ApiResponse(success = false, error = "Bird not found"))
}
}
// POST /birds - create new bird
post("/birds") {
try {
val request = call.receive<CreateBirdRequest>()
val bird = birdService.addBird(request)
call.respond(HttpStatusCode.Created, ApiResponse(success = true, data = bird))
} catch (e: Exception) {
call.respond(HttpStatusCode.BadRequest, ApiResponse(success = false, error = e.message))
}
}
// PUT /birds/{id} - update bird
put("/birds/{id}") {
val id = call.parameters["id"]?.toIntOrNull()
if (id == null) {
call.respond(HttpStatusCode.BadRequest, ApiResponse(success = false, error = "Invalid ID"))
return@put
}
try {
val request = call.receive<UpdateBirdRequest>()
val bird = birdService.updateBird(id, request)
if (bird != null) {
call.respond(ApiResponse(success = true, data = bird))
} else {
call.respond(HttpStatusCode.NotFound, ApiResponse(success = false, error = "Bird not found"))
}
} catch (e: Exception) {
call.respond(HttpStatusCode.BadRequest, ApiResponse(success = false, error = e.message))
}
}
// DELETE /birds/{id} - delete bird
delete("/birds/{id}") {
val id = call.parameters["id"]?.toIntOrNull()
if (id == null) {
call.respond(HttpStatusCode.BadRequest, ApiResponse(success = false, error = "Invalid ID"))
return@delete
}
if (birdService.deleteBird(id)) {
call.respond(ApiResponse(success = true))
} else {
call.respond(HttpStatusCode.NotFound, ApiResponse(success = false, error = "Bird not found"))
}
}
}
}.start(wait = true)
}
REST APIs use HTTP methods for CRUD operations. Status codes indicate success or failure. Query parameters handle filtering and pagination. Consistent response formats improve API usability. Error handling returns descriptive messages.
13.4 Database Access (Exposed)
Exposed is a lightweight SQL framework for Kotlin.
- Table definitions – Define tables as objects.
- DSL queries – Type-safe query building.
- Transactions – ACID transactions.
- Relationships – Foreign keys and joins.
- Migrations – Schema changes.
Code Example:
import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.transactions.transaction
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
import java.time.LocalDateTime
// Database configuration
fun initDatabase() {
Database.connect(
url = "jdbc:h2:mem:birds;DB_CLOSE_DELAY=-1",
driver = "org.h2.Driver",
user = "sa",
password = ""
)
transaction {
SchemaUtils.create(Birds, Sightings)
println("Database initialized")
}
}
// Table definitions
object Birds : Table() {
val id = integer("id").autoIncrement()
val species = varchar("species", 100)
val count = integer("count")
val weight = double("weight")
val isMigratory = bool("is_migratory")
val createdAt = datetime("created_at")
val updatedAt = datetime("updated_at")
override val primaryKey = PrimaryKey(id)
}
object Sightings : Table() {
val id = integer("id").autoIncrement()
val birdId = integer("bird_id") references Birds.id
val location = varchar("location", 200)
val sightedAt = datetime("sighted_at")
val observer = varchar("observer", 100)
val notes = varchar("notes", 500).nullable()
override val primaryKey = PrimaryKey(id)
}
// Data classes
data class Bird(
val id: Int? = null,
val species: String,
val count: Int,
val weight: Double,
val isMigratory: Boolean,
val createdAt: LocalDateTime = LocalDateTime.now(),
val updatedAt: LocalDateTime = LocalDateTime.now()
)
data class Sighting(
val id: Int? = null,
val birdId: Int,
val location: String,
val sightedAt: LocalDateTime = LocalDateTime.now(),
val observer: String,
val notes: String? = null
)
// Repository functions
class BirdRepository {
fun createBird(bird: Bird): Int {
return transaction {
Birds.insert {
it[species] = bird.species
it[count] = bird.count
it[weight] = bird.weight
it[isMigratory] = bird.isMigratory
it[createdAt] = bird.createdAt
it[updatedAt] = bird.updatedAt
} get Birds.id
}
}
fun getBird(id: Int): Bird? {
return transaction {
Birds.select { Birds.id eq id }
.map { rowToBird(it) }
.singleOrNull()
}
}
fun getBirds(): List<Bird> {
return transaction {
Birds.selectAll()
.map { rowToBird(it) }
}
}
fun updateBird(id: Int, bird: Bird): Boolean {
return transaction {
val updatedRows = Birds.update({ Birds.id eq id }) {
it[species] = bird.species
it[count] = bird.count
it[weight] = bird.weight
it[isMigratory] = bird.isMigratory
it[updatedAt] = LocalDateTime.now()
}
updatedRows > 0
}
}
fun deleteBird(id: Int): Boolean {
return transaction {
val deletedRows = Birds.deleteWhere { Birds.id eq id }
deletedRows > 0
}
}
fun addSighting(sighting: Sighting): Int {
return transaction {
Sightings.insert {
it[birdId] = sighting.birdId
it[location] = sighting.location
it[sightedAt] = sighting.sightedAt
it[observer] = sighting.observer
it[notes] = sighting.notes
} get Sightings.id
}
}
fun getSightingsByBird(birdId: Int): List<Sighting> {
return transaction {
Sightings.select { Sightings.birdId eq birdId }
.map { rowToSighting(it) }
}
}
fun getRecentSightings(limit: Int = 10): List<Sighting> {
return transaction {
Sightings.selectAll()
.orderBy(Sightings.sightedAt to SortOrder.DESC)
.limit(limit)
.map { rowToSighting(it) }
}
}
private fun rowToBird(row: ResultRow): Bird {
return Bird(
id = row[Birds.id],
species = row[Birds.species],
count = row[Birds.count],
weight = row[Birds.weight],
isMigratory = row[Birds.isMigratory],
createdAt = row[Birds.createdAt],
updatedAt = row[Birds.updatedAt]
)
}
private fun rowToSighting(row: ResultRow): Sighting {
return Sighting(
id = row[Sightings.id],
birdId = row[Sightings.birdId],
location = row[Sightings.location],
sightedAt = row[Sightings.sightedAt],
observer = row[Sightings.observer],
notes = row[Sightings.notes]
)
}
}
fun main() {
initDatabase()
val repository = BirdRepository()
// Create birds
val sparrowId = repository.createBird(
Bird(species = "Sparrow", count = 10, weight = 25.5, isMigratory = true)
)
val eagleId = repository.createBird(
Bird(species = "Eagle", count = 3, weight = 4500.0, isMigratory = false)
)
// Add sightings
repository.addSighting(Sighting(
birdId = sparrowId,
location = "Park",
observer = "John",
notes = "Flock of sparrows"
))
repository.addSighting(Sighting(
birdId = eagleId,
location = "Mountains",
observer = "Jane",
notes = "Eagle soaring"
))
// Query data
println("All birds:")
repository.getBirds().forEach { println(" ${it.species}: ${it.count} birds") }
println("\nRecent sightings:")
repository.getRecentSightings(5).forEach {
val bird = repository.getBird(it.birdId)
println(" ${bird?.species} at ${it.location} (${it.observer})")
}
}
Exposed uses type-safe table definitions. Transactions ensure data consistency. Queries are constructed using Kotlin DSL. Relationships between tables use foreign keys. Repository pattern separates database logic from business logic.
Android Development
14.1 Android Fundamentals
Android development with Kotlin follows the Activity lifecycle and uses modern UI frameworks.
- Activity lifecycle – onCreate, onStart, onResume, etc.
- Layouts – XML or Compose UI.
- Intents – Navigation between screens.
- Permissions – Request runtime permissions.
- Resources – Strings, colors, dimensions.
Code Example:
// MainActivity.kt
import android.os.Bundle
import android.widget.Button
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
class MainActivity : AppCompatActivity() {
private lateinit var birdCountText: TextView
private lateinit var addBirdButton: Button
private var birdCount = 0
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// Initialize views
birdCountText = findViewById(R.id.birdCountText)
addBirdButton = findViewById(R.id.addBirdButton)
// Set initial text
birdCountText.text = "Birds: $birdCount"
// Set click listener
addBirdButton.setOnClickListener {
birdCount++
birdCountText.text = "Birds: $birdCount"
}
}
override fun onStart() {
super.onStart()
// Called when activity becomes visible
println("onStart: Activity started")
}
override fun onResume() {
super.onResume()
// Called when activity is in foreground
println("onResume: Activity resumed")
}
override fun onPause() {
super.onPause()
// Called when activity is partially obscured
println("onPause: Activity paused")
}
override fun onStop() {
super.onStop()
// Called when activity is no longer visible
println("onStop: Activity stopped")
}
override fun onDestroy() {
super.onDestroy()
// Called when activity is destroyed
println("onDestroy: Activity destroyed")
}
}
// BirdDetailActivity.kt
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
class BirdDetailActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_bird_detail)
val birdName = intent.getStringExtra("bird_name") ?: "Unknown"
val birdCount = intent.getIntExtra("bird_count", 0)
supportActionBar?.title = birdName
findViewById<TextView>(R.id.detailText).text =
"$birdName has $birdCount birds"
}
}
// activity_main.xml (layout)
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:gravity="center"
android:padding="24dp">
<TextView
android:id="@+id/birdCountText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Birds: 0"
android:textSize="24sp"
android:textStyle="bold"/>
<Button
android:id="@+id/addBirdButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Add Bird"
android:layout_marginTop="16dp"/>
<Button
android:id="@+id/detailButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="View Details"
android:layout_marginTop="8dp"/>
</LinearLayout>
Activity lifecycle manages app state. onCreate initializes views. findViewById gets view references. Click listeners handle user interaction. Intents navigate between activities. Layout XML defines UI structure.
14.2 Jetpack Compose
Jetpack Compose is a modern UI framework for building Android apps with Kotlin.
- Composable functions – Building blocks of UI.
- State management –
mutableStateOfandState. - Recomposition – UI updates automatically.
- Layouts – Column, Row, Box.
- Modifiers – Styling and positioning.
Code Example:
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.*
import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.foundation.lazy.LazyColumn
// Main Activity
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
BirdTrackerApp()
}
}
}
// App theme
@Composable
fun BirdTrackerApp() {
MaterialTheme {
BirdTrackerScreen()
}
}
// Main screen
@Composable
fun BirdTrackerScreen() {
var birdList by remember {
mutableStateOf(listOf("Sparrow", "Eagle", "Hawk"))
}
var newBirdName by remember { mutableStateOf("") }
var birdCount by remember { mutableStateOf(0) }
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = "Bird Tracker",
style = MaterialTheme.typography.h4,
modifier = Modifier.padding(bottom = 16.dp)
)
Text(
text = "Total birds: $birdCount",
style = MaterialTheme.typography.h6,
modifier = Modifier.padding(bottom = 16.dp)
)
// Add bird section
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
OutlinedTextField(
value = newBirdName,
onValueChange = { newBirdName = it },
label = { Text("Bird name") },
modifier = Modifier.weight(1f)
)
Button(
onClick = {
if (newBirdName.isNotBlank()) {
birdList = birdList + newBirdName
birdCount++
newBirdName = ""
}
}
) {
Text("Add")
}
}
Spacer(modifier = Modifier.height(16.dp))
// Bird list
LazyColumn {
items(birdList) { bird ->
BirdItem(bird = bird, onRemove = {
birdList = birdList.filter { it != bird }
birdCount--
})
}
}
}
}
// Individual bird item
@Composable
fun BirdItem(
bird: String,
onRemove: () -> Unit
) {
Card(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 4.dp),
elevation = 4.dp
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text(text = bird, style = MaterialTheme.typography.body1)
TextButton(onClick = onRemove) {
Text("Remove", color = MaterialTheme.colors.error)
}
}
}
}
// Preview
@Preview
@Composable
fun PreviewBirdTracker() {
BirdTrackerApp()
}
Composable functions define the UI. remember keeps state across recompositions. mutableStateOf creates observable state. Recomposition updates UI when state changes. LazyColumn efficiently displays lists. Modifiers style and position UI elements.
14.3 Architecture Components (ViewModel)
ViewModel manages UI data and survives configuration changes.
- ViewModel – Stores UI data across lifecycle changes.
- StateFlow – Observable state for UI.
- Coroutines – Background operations.
- Dependency injection – Inject repositories.
- Testing – ViewModel testing.
Code Example:
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
// Data classes
data class Bird(
val id: Int = 0,
val species: String,
val count: Int,
val weight: Double
)
// Repository interface
interface BirdRepository {
suspend fun getBirds(): List<Bird>
suspend fun addBird(bird: Bird): Bird
suspend fun deleteBird(id: Int): Boolean
}
// ViewModel
class BirdViewModel(
private val repository: BirdRepository
) : ViewModel() {
private val _uiState = MutableStateFlow(BirdUiState())
val uiState: StateFlow<BirdUiState> = _uiState
private val _events = MutableSharedFlow<BirdEvent>()
val events: SharedFlow<BirdEvent> = _events
init {
loadBirds()
}
fun loadBirds() {
viewModelScope.launch {
_uiState.update { it.copy(isLoading = true, error = null) }
try {
val birds = repository.getBirds()
_uiState.update {
it.copy(
isLoading = false,
birds = birds,
totalBirds = birds.sumOf { bird -> bird.count }
)
}
_events.emit(BirdEvent.LoadSuccess)
} catch (e: Exception) {
_uiState.update {
it.copy(isLoading = false, error = e.message)
}
_events.emit(BirdEvent.LoadError(e.message ?: "Unknown error"))
}
}
}
fun addBird(species: String, count: Int, weight: Double) {
viewModelScope.launch {
try {
val newBird = Bird(species = species, count = count, weight = weight)
val added = repository.addBird(newBird)
val current = _uiState.value.birds
_uiState.update {
it.copy(
birds = current + added,
totalBirds = it.totalBirds + added.count
)
}
_events.emit(BirdEvent.AddSuccess(added))
} catch (e: Exception) {
_events.emit(BirdEvent.AddError(e.message ?: "Failed to add bird"))
}
}
}
fun deleteBird(id: Int) {
viewModelScope.launch {
try {
val success = repository.deleteBird(id)
if (success) {
val current = _uiState.value.birds
val deleted = current.find { it.id == id }
_uiState.update {
it.copy(
birds = current.filter { it.id != id },
totalBirds = it.totalBirds - (deleted?.count ?: 0)
)
}
_events.emit(BirdEvent.DeleteSuccess(id))
} else {
_events.emit(BirdEvent.DeleteError("Bird not found"))
}
} catch (e: Exception) {
_events.emit(BirdEvent.DeleteError(e.message ?: "Failed to delete bird"))
}
}
}
}
// UI State
data class BirdUiState(
val birds: List<Bird> = emptyList(),
val isLoading: Boolean = false,
val totalBirds: Int = 0,
val error: String? = null
)
// Events
sealed class BirdEvent {
object LoadSuccess : BirdEvent()
data class LoadError(val message: String) : BirdEvent()
data class AddSuccess(val bird: Bird) : BirdEvent()
data class AddError(val message: String) : BirdEvent()
data class DeleteSuccess(val id: Int) : BirdEvent()
data class DeleteError(val message: String) : BirdEvent()
}
// Compose UI using ViewModel
import androidx.compose.runtime.*
import androidx.lifecycle.viewmodel.compose.viewModel
@Composable
fun BirdScreen(
viewModel: BirdViewModel = viewModel()
) {
val uiState by viewModel.uiState.collectAsState()
val events = viewModel.events.collectAsState(initial = null)
var newSpecies by remember { mutableStateOf("") }
var newCount by remember { mutableStateOf("") }
var newWeight by remember { mutableStateOf("") }
Column(modifier = Modifier.padding(16.dp)) {
if (uiState.isLoading) {
CircularProgressIndicator()
}
uiState.error?.let {
Text(text = "Error: $it", color = Color.Red)
}
Text("Total Birds: ${uiState.totalBirds}", style = MaterialTheme.typography.h6)
Spacer(modifier = Modifier.height(16.dp))
// Add bird form
OutlinedTextField(
value = newSpecies,
onValueChange = { newSpecies = it },
label = { Text("Species") }
)
OutlinedTextField(
value = newCount,
onValueChange = { newCount = it },
label = { Text("Count") },
modifier = Modifier.padding(top = 8.dp)
)
OutlinedTextField(
value = newWeight,
onValueChange = { newWeight = it },
label = { Text("Weight") },
modifier = Modifier.padding(top = 8.dp)
)
Button(
onClick = {
val count = newCount.toIntOrNull() ?: 0
val weight = newWeight.toDoubleOrNull() ?: 0.0
if (newSpecies.isNotBlank() && count > 0) {
viewModel.addBird(newSpecies, count, weight)
newSpecies = ""
newCount = ""
newWeight = ""
}
},
modifier = Modifier.padding(top = 8.dp)
) {
Text("Add Bird")
}
Spacer(modifier = Modifier.height(16.dp))
// Bird list
LazyColumn {
items(uiState.birds) { bird ->
BirdItem(bird = bird) {
viewModel.deleteBird(bird.id)
}
}
}
}
}
ViewModel stores UI state and survives configuration changes. StateFlow exposes observable state. SharedFlow emits events. Coroutines handle background operations. UI collects state and events for display. Dependency injection provides repositories.
Kotlin Multiplatform
15.1 Shared Code
Kotlin Multiplatform shares code between Android, iOS, and other platforms.
- commonMain – Shared code for all platforms.
- expect/actual – Platform-specific implementations.
- Platform interfaces – Define expectations.
- Shared logic – Business logic, models, validations.
- Serialization – Cross-platform data formats.
Code Example:
// commonMain - Shared code
package com.example.birdtracker
import kotlinx.serialization.Serializable
import kotlinx.datetime.LocalDateTime
import kotlin.random.Random
// Shared model
@Serializable
data class Bird(
val id: Int = 0,
val species: String,
val count: Int,
val weight: Double,
val isMigratory: Boolean = false,
val createdAt: String = LocalDateTime.now().toString()
)
// Platform interface
expect class Platform() {
val platformName: String
fun getCurrentTime(): String
fun getDeviceInfo(): String
}
// Shared business logic
class BirdService {
private val birds = mutableListOf(
Bird(1, "Sparrow", 10, 25.5, true),
Bird(2, "Eagle", 3, 4500.0, false),
Bird(3, "Hawk", 5, 1200.0, true)
)
private var nextId = 4
fun getBirds(): List<Bird> = birds.toList()
fun getBird(id: Int): Bird? = birds.find { it.id == id }
fun addBird(species: String, count: Int, weight: Double, isMigratory: Boolean): Bird {
val bird = Bird(nextId++, species, count, weight, isMigratory)
birds.add(bird)
return bird
}
fun updateBird(id: Int, species: String?, count: Int?, weight: Double?, isMigratory: Boolean?): Bird? {
val bird = getBird(id) ?: return null
val updated = bird.copy(
species = species ?: bird.species,
count = count ?: bird.count,
weight = weight ?: bird.weight,
isMigratory = isMigratory ?: bird.isMigratory
)
val index = birds.indexOfFirst { it.id == id }
birds[index] = updated
return updated
}
fun deleteBird(id: Int): Boolean = birds.removeIf { it.id == id }
fun getTotalBirds(): Int = birds.sumOf { it.count }
fun getHeaviestBird(): Bird? = birds.maxByOrNull { it.weight }
fun getFilteredBirds(predicate: (Bird) -> Boolean): List<Bird> {
return birds.filter(predicate)
}
fun getSpeciesList(): List<String> = birds.map { it.species }.distinct()
fun getRandomBird(): Bird = birds[Random.nextInt(birds.size)]
fun clearBirds() {
birds.clear()
nextId = 1
}
}
// Shared validation
class BirdValidator {
fun validateBird(species: String, count: Int, weight: Double): ValidationResult {
return when {
species.isBlank() -> ValidationResult.Error("Species cannot be empty")
species.length > 50 -> ValidationResult.Error("Species too long (max 50 characters)")
count < 0 -> ValidationResult.Error("Count cannot be negative")
count > 1000 -> ValidationResult.Error("Count too large (max 1000)")
weight <= 0 -> ValidationResult.Error("Weight must be positive")
weight > 10000 -> ValidationResult.Error("Weight too large (max 10000g)")
else -> ValidationResult.Success
}
}
}
sealed class ValidationResult {
object Success : ValidationResult()
data class Error(val message: String) : ValidationResult()
}
// Shared utility
class BirdLogger {
fun log(message: String, level: LogLevel = LogLevel.INFO) {
val time = Platform().getCurrentTime()
println("[${level.name}] [$time] $message")
}
enum class LogLevel {
DEBUG, INFO, WARNING, ERROR
}
}
// Shared constants
object BirdConstants {
const val MAX_SPECIES_LENGTH = 50
const val MAX_BIRD_COUNT = 1000
const val MAX_BIRD_WEIGHT = 10000.0
const val DEFAULT_BIRD_WEIGHT = 100.0
val MIGRATORY_SPECIES = setOf(
"Sparrow", "Robin", "Swallow", "Goose"
)
val BIRDS_OF_PREY = setOf(
"Eagle", "Hawk", "Falcon", "Owl"
)
}
Common code contains platform-independent logic. expect declarations define platform requirements. Models and services are shared. Validation logic works on all platforms. Constants and utilities are platform-independent.
15.2 Android Targets
Android-specific implementation for Kotlin Multiplatform.
- androidMain – Android-specific code.
- Actual implementations – Platform-specific APIs.
- Android dependencies – Android libraries.
- Android lifecycle – Integration with Android.
- UI integration – Compose or Views.
Code Example:
// androidMain - Android implementation
package com.example.birdtracker
import android.content.Context
import android.os.Build
import android.provider.Settings
import java.text.SimpleDateFormat
import java.util.*
actual class Platform actual constructor() {
actual val platformName: String = "Android ${Build.VERSION.SDK_INT}"
actual fun getCurrentTime(): String {
return SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault())
.format(Date())
}
actual fun getDeviceInfo(): String {
return "Android ${Build.VERSION.RELEASE} (SDK ${Build.VERSION.SDK_INT}) " +
"on ${Build.MANUFACTURER} ${Build.MODEL}"
}
fun getAndroidId(context: Context): String {
return Settings.Secure.getString(
context.contentResolver,
Settings.Secure.ANDROID_ID
) ?: "Unknown"
}
}
// Android dependency injection
class AndroidBirdService(private val context: Context) {
private val service = BirdService()
private val logger = BirdLogger()
fun getBirdsWithLogging(): List<Bird> {
logger.log("Getting birds from Android app", BirdLogger.LogLevel.INFO)
val result = service.getBirds()
logger.log("Found ${result.size} birds", BirdLogger.LogLevel.INFO)
return result
}
fun addBirdWithValidation(species: String, count: Int, weight: Double, isMigratory: Boolean): Bird? {
val validator = BirdValidator()
val result = validator.validateBird(species, count, weight)
when (result) {
is ValidationResult.Error -> {
logger.log("Validation failed: ${result.message}", BirdLogger.LogLevel.ERROR)
return null
}
ValidationResult.Success -> {
logger.log("Adding bird: $species ($count birds, ${weight}g)", BirdLogger.LogLevel.INFO)
return service.addBird(species, count, weight, isMigratory)
}
}
}
}
Android-specific code provides actual implementations. actual matches expect declarations. Android APIs like Build and Settings are used. Context provides Android environment. Dependency injection passes Context to services.
15.3 iOS Targets
iOS-specific implementation for Kotlin Multiplatform.
- iosMain – iOS-specific code.
- Foundation framework – NSDate, NSBundle.
- iOS dependencies – Apple frameworks.
- Bridging – Kotlin to Objective-C/Swift.
- Native UI integration – SwiftUI integration.
Code Example:
// iosMain - iOS implementation
package com.example.birdtracker
import platform.Foundation.NSDate
import platform.Foundation.NSDateFormatter
import platform.Foundation.NSBundle
import platform.UIKit.UIDevice
actual class Platform actual constructor() {
actual val platformName: String = getDeviceName()
actual fun getCurrentTime(): String {
val date = NSDate()
val formatter = NSDateFormatter()
formatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
return formatter.stringFromDate(date)
}
actual fun getDeviceInfo(): String {
val device = UIDevice.currentDevice
return "iOS ${device.systemVersion} on ${device.name} (${device.model})"
}
private fun getDeviceName(): String {
val bundle = NSBundle.mainBundle
val appName = bundle.infoDictionary?.get("CFBundleName") as? String ?: "iOS App"
val version = bundle.infoDictionary?.get("CFBundleShortVersionString") as? String ?: "1.0"
return "$appName v$version (iOS)"
}
}
// iOS-specific service
class iOSBirdService {
private val service = BirdService()
private val logger = BirdLogger()
fun getBirds(): List<Bird> {
logger.log("Getting birds from iOS app", BirdLogger.LogLevel.INFO)
return service.getBirds()
}
fun addBird(species: String, count: Int, weight: Double, isMigratory: Boolean): Bird? {
val validator = BirdValidator()
val result = validator.validateBird(species, count, weight)
when (result) {
is ValidationResult.Error -> {
logger.log("iOS validation failed: ${result.message}", BirdLogger.LogLevel.ERROR)
return null
}
ValidationResult.Success -> {
logger.log("iOS adding bird: $species", BirdLogger.LogLevel.INFO)
return service.addBird(species, count, weight, isMigratory)
}
}
}
}
// Swift integration helper
import kotlinx.cinterop.ObjCAction
@ObjCAction
fun getPlatformName(): String {
return Platform().platformName
}
@ObjCAction
fun createBirdService(): BirdService {
return BirdService()
}
iOS-specific code implements actual declarations. Apple frameworks like Foundation and UIKit are used. NSDate and NSDateFormatter handle dates. UIDevice provides device information. ObjCAction enables Swift/Kotlin interop.
15.4 Desktop Targets (Compose Multiplatform)
Compose Multiplatform enables desktop applications.
- Compose Desktop – Cross-platform UI.
- JVM dependencies – Desktop JVM runtime.
- Window management – Create and manage windows.
- Theming – Consistent UI across platforms.
- Native integration – File system, system tray.
Code Example:
// desktopMain - Desktop implementation
import androidx.compose.desktop.ui.tooling.preview.Preview
import androidx.compose.foundation.layout.*
import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Window
import androidx.compose.ui.window.application
import kotlinx.coroutines.launch
// Desktop platform implementation
actual class Platform actual constructor() {
actual val platformName: String = System.getProperty("os.name")
actual fun getCurrentTime(): String {
return java.time.LocalDateTime.now().toString()
}
actual fun getDeviceInfo(): String {
return "${System.getProperty("os.name")} ${System.getProperty("os.version")} " +
"on ${System.getProperty("os.arch")}"
}
}
// Desktop main application
fun main() = application {
Window(
onCloseRequest = ::exitApplication,
title = "Bird Tracker",
width = 600.dp,
height = 400.dp
) {
App()
}
}
@Composable
fun App() {
val birdService = remember { BirdService() }
val logger = remember { BirdLogger() }
val scope = rememberCoroutineScope()
var birds by remember { mutableStateOf(birdService.getBirds()) }
var newSpecies by remember { mutableStateOf("") }
var newCount by remember { mutableStateOf("") }
var newWeight by remember { mutableStateOf("") }
var isMigratory by remember { mutableStateOf(false) }
var message by remember { mutableStateOf<String?>(null) }
MaterialTheme {
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp)
) {
// Title
Text(
text = "Bird Tracker - Desktop",
style = MaterialTheme.typography.h4,
modifier = Modifier.padding(bottom = 16.dp)
)
// Status message
message?.let {
Text(
text = it,
color = if (it.contains("Error")) MaterialTheme.colors.error else MaterialTheme.colors.primary,
modifier = Modifier.padding(bottom = 8.dp)
)
}
// Stats
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceEvenly
) {
Card(modifier = Modifier.weight(1f).padding(4.dp)) {
Column(horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier.padding(8.dp)) {
Text("Total Birds", style = MaterialTheme.typography.caption)
Text("${birds.sumOf { it.count }}", style = MaterialTheme.typography.h5)
}
}
Card(modifier = Modifier.weight(1f).padding(4.dp)) {
Column(horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier.padding(8.dp)) {
Text("Species", style = MaterialTheme.typography.caption)
Text("${birds.size}", style = MaterialTheme.typography.h5)
}
}
}
Spacer(modifier = Modifier.height(16.dp))
// Add bird form
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(8.dp)) {
OutlinedTextField(
value = newSpecies,
onValueChange = { newSpecies = it },
label = { Text("Species") },
modifier = Modifier.weight(1f)
)
OutlinedTextField(
value = newCount,
onValueChange = { newCount = it },
label = { Text("Count") },
modifier = Modifier.width(80.dp)
)
OutlinedTextField(
value = newWeight,
onValueChange = { newWeight = it },
label = { Text("Weight") },
modifier = Modifier.width(80.dp)
)
Checkbox(
checked = isMigratory,
onCheckedChange = { isMigratory = it }
)
Text("Migratory", modifier = Modifier.align(Alignment.CenterVertically))
}
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
Button(
onClick = {
try {
val count = newCount.toIntOrNull() ?: 0
val weight = newWeight.toDoubleOrNull() ?: 0.0
if (newSpecies.isNotBlank() && count > 0) {
scope.launch {
val added = birdService.addBird(newSpecies, count, weight, isMigratory)
birds = birdService.getBirds()
message = "Added ${added.species}"
newSpecies = ""
newCount = ""
newWeight = ""
isMigratory = false
}
} else {
message = "Error: Please fill all fields correctly"
}
} catch (e: Exception) {
message = "Error: ${e.message}"
}
},
modifier = Modifier.weight(1f)
) {
Text("Add Bird")
}
Button(
onClick = {
scope.launch {
birds = birdService.getBirds()
message = "Refreshed list"
}
},
modifier = Modifier.weight(1f)
) {
Text("Refresh")
}
Button(
onClick = {
scope.launch {
birdService.clearBirds()
birds = birdService.getBirds()
message = "Cleared all birds"
}
},
modifier = Modifier.weight(1f),
colors = ButtonDefaults.buttonColors(backgroundColor = MaterialTheme.colors.error)
) {
Text("Clear All")
}
}
Spacer(modifier = Modifier.height(16.dp))
// Bird list
Divider()
LazyColumn(
modifier = Modifier.weight(1f)
) {
items(birds) { bird ->
BirdItemDesktop(bird = bird) {
scope.launch {
birdService.deleteBird(bird.id)
birds = birdService.getBirds()
message = "Deleted ${bird.species}"
}
}
}
}
}
}
}
@Composable
fun BirdItemDesktop(
bird: Bird,
onDelete: () -> Unit
) {
Card(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 4.dp)
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(12.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Column {
Text(text = bird.species, style = MaterialTheme.typography.body1)
Text(
text = "${bird.count} birds, ${bird.weight}g" +
if (bird.isMigratory) " (Migratory)" else "",
style = MaterialTheme.typography.caption
)
}
Button(
onClick = onDelete,
colors = ButtonDefaults.buttonColors(backgroundColor = MaterialTheme.colors.error)
) {
Text("Delete")
}
}
}
}
@Preview
@Composable
fun PreviewApp() {
MaterialTheme {
App()
}
}
Compose Desktop uses the same API as Android Compose. The application runs on the JVM. Desktop-specific implementations use java.time for dates. System.getProperty gets OS information. The UI is fully responsive and cross-platform.
Performance Optimization
16.1 Memory Management
Optimizing memory usage for better performance.
- Object creation – Avoid unnecessary object creation.
- Memory leaks – Prevent reference leaks.
- Caching – Use caches effectively.
- Collections – Choose appropriate collection types.
- String handling – Use
StringBuilderfor concatenation.
Code Example:
fun main() {
// ---- STRING CONCATENATION ----
fun inefficientConcat() {
var result = ""
for (i in 1..1000) {
result += "Bird $i, " // Creates new String each iteration
}
}
fun efficientConcat() {
val builder = StringBuilder()
for (i in 1..1000) {
builder.append("Bird $i, ") // Efficient
}
val result = builder.toString()
}
// ---- COLLECTION CHOICES ----
fun collectionEfficiency() {
// Use array for primitive types
val primitiveArray = IntArray(1000000) { it }
val primitiveList = List(1000000) { it } // Boxed, more memory
// Use mutableList for frequent modifications
val mutable = mutableListOf<Int>()
mutable.add(10) // O(1)
// Use set for unique values
val unique = mutableSetOf<Int>()
unique.add(10) // O(1)
unique.add(10) // Duplicate ignored
}
// ---- CACHING ----
class BirdCache {
private val cache = mutableMapOf<Int, Bird>()
private val maxSize = 100
fun getOrCompute(id: Int, compute: () -> Bird): Bird {
return cache.getOrPut(id) {
compute().also {
if (cache.size > maxSize) {
evictOldest()
}
}
}
}
private fun evictOldest() {
if (cache.isNotEmpty()) {
val oldest = cache.keys.first()
cache.remove(oldest)
}
}
fun clear() = cache.clear()
fun size() = cache.size
}
// ---- LAZY INITIALIZATION ----
class HeavyResource {
init {
println("Heavy resource initialized")
}
fun expensiveOperation() = "Result"
}
class BirdProcessor {
private val heavyResource by lazy { HeavyResource() }
fun process() {
// Heavy resource only created when first accessed
println(heavyResource.expensiveOperation())
}
}
// ---- OBJECT REUSE ----
data class Bird(val species: String, val count: Int, val weight: Double)
class BirdFactory {
private val cache = mutableMapOf<String, Bird>()
fun createBird(species: String, count: Int, weight: Double): Bird {
val key = "$species-$count-$weight"
return cache.getOrPut(key) {
Bird(species, count, weight)
}
}
}
}
String concatenation creates new objects each time. StringBuilder is more efficient. Choosing the right collection reduces memory overhead. Caching prevents recomputation. Lazy initialization delays resource allocation. Object reuse reduces garbage collection pressure.
16.2 JVM Optimization
Optimizing Kotlin/JVM performance.
- Inline functions – Reduce function call overhead.
- Tail recursion – Optimize recursive functions.
- Primitive arrays – Use
IntArray,DoubleArray. - JVM flags –
-Xmx,-XX:+UseG1GC. - Profiling – Identify bottlenecks.
Code Example:
fun main() {
// ---- INLINE FUNCTIONS ----
inline fun measureTime(block: () -> Unit) {
val start = System.currentTimeMillis()
block()
val end = System.currentTimeMillis()
println("Time: ${end - start}ms")
}
measureTime {
repeat(1000000) {
// Some operation
}
}
// ---- TAIL RECURSION ----
tailrec fun factorial(n: Int, acc: Long = 1): Long {
return if (n <= 1) acc else factorial(n - 1, acc * n)
}
println("Factorial(10): ${factorial(10)}")
// ---- PRIMITIVE ARRAYS ----
fun sumPrimitiveArray() {
val array = IntArray(1000000) { it }
var sum = 0
for (i in array) {
sum += i
}
println("Sum: $sum")
}
// ---- JVM FLAGS ----
println("Available processors: ${Runtime.getRuntime().availableProcessors()}")
println("Max memory: ${Runtime.getRuntime().maxMemory() / 1024 / 1024} MB")
// ---- EFFICIENT COLLECTION OPERATIONS ----
fun efficientOperations() {
val list = (1..100000).toList()
// Use sequences for large collections with multiple operations
val result = list.asSequence()
.filter { it % 2 == 0 }
.map { it * 2 }
.take(100)
.toList()
println("Result size: ${result.size}")
}
// ---- ARRAY OPTIMIZATION ----
fun arrayOptimization() {
// Use array for fixed size
val fixed = Array(10) { index -> "Bird $index" }
// Pre-size mutable lists when possible
val sizedList = ArrayList<String>(1000)
repeat(1000) { sizedList.add("Bird $it") }
// Use specialized arrays
val intArray = IntArray(1000) { it }
val doubleArray = DoubleArray(1000) { it.toDouble() }
}
// ---- MEMORY PROFILING ----
fun memoryProfiling() {
val heap = Runtime.getRuntime()
println("Used memory: ${(heap.totalMemory() - heap.freeMemory()) / 1024 / 1024} MB")
println("Free memory: ${heap.freeMemory() / 1024 / 1024} MB")
println("Max memory: ${heap.maxMemory() / 1024 / 1024} MB")
System.gc() // Suggest garbage collection
Thread.sleep(100)
println("After GC:")
println("Used memory: ${(heap.totalMemory() - heap.freeMemory()) / 1024 / 1024} MB")
println("Free memory: ${heap.freeMemory() / 1024 / 1024} MB")
}
}
Inline functions eliminate call overhead. Tail recursion optimizes recursive loops. Primitive arrays avoid boxing overhead. JVM flags control memory and GC. Sequences defer execution until needed. Runtime methods provide memory information.
16.3 Lazy Evaluation
Lazy evaluation postpones a computation until its result is actually required, avoiding unnecessary work.
lazy– Delegate for lazy initialization.- Sequences – Lazy collections.
asSequence()– Convert to lazy sequence.- Lazy operations –
map,filteron sequences. - Terminal operations –
toList(),forEach.
Code Example:
fun main() {
// ---- LAZY PROPERTIES ----
val expensiveBirdData by lazy {
println("Computing expensive bird data...")
val data = (1..1000).map { "Bird $it" }
println("Computed ${data.size} birds")
data
}
println("Before accessing property")
val birds = expensiveBirdData
println("After accessing property: ${birds.size} birds")
val birdsAgain = expensiveBirdData // No recomputation
println("Cached value: ${birdsAgain.size} birds")
// ---- LAZY COLLECTIONS (SEQUENCES) ----
fun getBirds(): List<String> {
println("Getting all birds...")
return (1..1000).map { "Bird $it" }
}
fun getHeavyBirds(): List<String> {
println("Heavy bird processing...")
return (1..1000).filter { it % 2 == 0 }
.map { "Heavy $it" }
}
// With regular collections - all operations execute immediately
val regularResult = getBirds()
.filter { it.length > 5 }
.map { it.uppercase() }
.take(10)
println("Regular result: ${regularResult.size}")
// With sequences - lazy evaluation
val sequenceResult = getBirds().asSequence()
.filter {
println("Filtering: $it")
it.length > 5
}
.map {
println("Mapping: $it")
it.uppercase()
}
.take(10)
.toList() // Terminal operation triggers execution
println("Sequence result: ${sequenceResult.size}")
// ---- LAZY RANGES ----
val infiniteRange = generateSequence(1) { it + 1 }
val first10Evens = infiniteRange
.filter { it % 2 == 0 }
.take(10)
.toList()
println("First 10 evens: $first10Evens")
// ---- LAZY BIRD GENERATION ----
fun lazyBirdGenerator(): Sequence<String> = sequence {
var id = 1
while (true) {
yield("Bird $id")
id++
}
}
val birdSequence = lazyBirdGenerator()
val first5Birds = birdSequence.take(5).toList()
println("First 5 birds: $first5Birds")
// ---- LAZY WITH STATE ----
fun birdFactory(): Sequence<Bird> = sequence {
var id = 1
val species = listOf("Sparrow", "Eagle", "Hawk", "Robin")
while (true) {
val randomSpecies = species[id % species.size]
yield(Bird(id, randomSpecies, (1..10).random(), (1..100).random().toDouble()))
id++
}
}
val birdsSeq = birdFactory()
.filter { it.weight > 50.0 }
.take(5)
.toList()
println("Birds > 50g: $birdsSeq")
}
data class Bird(val id: Int, val species: String, val count: Int, val weight: Double)
lazy delegates initialize once on first access. Sequences are lazy collections that don’t execute until a terminal operation. generateSequence creates infinite lazy sequences. sequence builder yields values lazily. Lazy evaluation saves memory and computation by only processing needed elements.
16.4 Profiling
Profiling identifies performance bottlenecks.
- JVM profilers – VisualVM, YourKit.
- Timing measurements – Measure execution time.
- Memory profiling – Heap dumps, allocations.
- CPU profiling – Hot methods, bottlenecks.
- Benchmarking – JMH for microbenchmarks.
Code Example:
import kotlin.system.measureTimeMillis
import kotlin.system.measureNanoTime
fun main() {
// ---- BASIC TIMING ----
fun timedOperation(label: String, block: () -> Unit) {
val time = measureTimeMillis {
block()
}
println("$label took ${time}ms")
}
timedOperation("Simple loop") {
repeat(1000000) { /* empty */ }
}
timedOperation("Collection operation") {
(1..100000).toList().map { it * 2 }.filter { it > 0 }.sum()
}
// ---- MICROBENCHMARKING ----
fun benchmark(iterations: Int = 100, block: () -> Unit) {
// Warm-up
repeat(10) { block() }
val times = mutableListOf<Long>()
repeat(iterations) {
val time = measureNanoTime {
block()
}
times.add(time)
}
val avgTime = times.average()
println("Average time: ${avgTime / 1_000_000}ms")
}
println("\nString concatenation benchmark:")
benchmark {
var result = ""
repeat(100) {
result += "x"
}
}
println("StringBuilder benchmark:")
benchmark {
val builder = StringBuilder()
repeat(100) {
builder.append("x")
}
builder.toString()
}
// ---- PROFILING WITH SYSTEM ----
fun memoryUsage(label: String, block: () -> Unit) {
val heap = Runtime.getRuntime()
val before = heap.totalMemory() - heap.freeMemory()
block()
val after = heap.totalMemory() - heap.freeMemory()
println("$label memory delta: ${(after - before) / 1024} KB")
// Suggest GC
heap.gc()
}
println("\nMemory usage:")
memoryUsage("List creation") {
val list = (1..100000).toList()
}
// ---- PROFILING WITH SYSTEM ----
fun getMemoryInfo(): Map<String, Long> {
val runtime = Runtime.getRuntime()
return mapOf(
"totalMemory" to runtime.totalMemory() / 1024 / 1024,
"freeMemory" to runtime.freeMemory() / 1024 / 1024,
"maxMemory" to runtime.maxMemory() / 1024 / 1024
)
}
println("\nMemory info:")
getMemoryInfo().forEach { (key, value) ->
println("$key: ${value} MB")
}
// ---- DETECTING HOTSPOTS ----
class BirdProcessor {
private val birds = mutableListOf<Bird>()
fun addBirds(count: Int) {
repeat(count) {
birds.add(Bird(it, "Sparrow", 1, 25.0))
}
}
fun processBirds(): Double {
return birds.map { it.weight }.average()
}
}
timedOperation("Bird processing") {
val processor = BirdProcessor()
processor.addBirds(100000)
processor.processBirds()
}
}
data class Bird(val id: Int, val species: String, val count: Int, val weight: Double)
measureTimeMillis and measureNanoTime time operations. Benchmarking uses warm-up for accurate results. Memory tracking shows allocations. Runtime provides memory info. Identifying hotspots guides optimization efforts. Profiling tools provide deeper insights.
Software Architecture
17.1 Clean Architecture
Clean Architecture organizes code into layers with clear dependencies.
- Domain layer – Business logic and entities.
- Data layer – Repositories and data sources.
- Presentation layer – UI and ViewModels.
- Dependency rule – Dependencies point inward.
- Use cases – Application-specific business rules.
Code Example:
// ---- DOMAIN LAYER ----
// Entities
data class Bird(
val id: Int,
val species: String,
val count: Int,
val weight: Double,
val isMigratory: Boolean = false
)
// Repository interface
interface BirdRepository {
suspend fun getBirds(): List<Bird>
suspend fun getBird(id: Int): Bird?
suspend fun addBird(bird: Bird): Bird
suspend fun updateBird(bird: Bird): Bird
suspend fun deleteBird(id: Int): Boolean
}
// Use cases
class GetBirdsUseCase(
private val repository: BirdRepository
) {
suspend operator fun invoke(): List<Bird> = repository.getBirds()
}
class AddBirdUseCase(
private val repository: BirdRepository,
private val validator: BirdValidator
) {
suspend operator fun invoke(species: String, count: Int, weight: Double, isMigratory: Boolean): Result<Bird> {
return try {
val validation = validator.validateBird(species, count, weight)
when (validation) {
is ValidationResult.Error -> Result.Error(validation.message)
ValidationResult.Success -> {
val bird = Bird(
id = 0,
species = species,
count = count,
weight = weight,
isMigratory = isMigratory
)
val result = repository.addBird(bird)
Result.Success(result)
}
}
} catch (e: Exception) {
Result.Error(e.message ?: "Unknown error")
}
}
}
class DeleteBirdUseCase(
private val repository: BirdRepository
) {
suspend operator fun invoke(id: Int): Result<Boolean> {
return try {
val result = repository.deleteBird(id)
if (result) Result.Success(true) else Result.Error("Bird not found")
} catch (e: Exception) {
Result.Error(e.message ?: "Unknown error")
}
}
}
sealed class Result<out T> {
data class Success<T>(val data: T) : Result<T>()
data class Error(val message: String) : Result<Nothing>()
}
// Validator
class BirdValidator {
fun validateBird(species: String, count: Int, weight: Double): ValidationResult {
return when {
species.isBlank() -> ValidationResult.Error("Species cannot be empty")
species.length > 50 -> ValidationResult.Error("Species too long")
count < 0 -> ValidationResult.Error("Count cannot be negative")
weight <= 0 -> ValidationResult.Error("Weight must be positive")
else -> ValidationResult.Success
}
}
}
sealed class ValidationResult {
object Success : ValidationResult()
data class Error(val message: String) : ValidationResult()
}
// ---- DATA LAYER ----
class InMemoryBirdRepository : BirdRepository {
private val birds = mutableListOf(
Bird(1, "Sparrow", 10, 25.5, true),
Bird(2, "Eagle", 3, 4500.0, false),
Bird(3, "Hawk", 5, 1200.0, true)
)
private var nextId = 4
override suspend fun getBirds(): List<Bird> = birds.toList()
override suspend fun getBird(id: Int): Bird? = birds.find { it.id == id }
override suspend fun addBird(bird: Bird): Bird {
val newBird = bird.copy(id = nextId++)
birds.add(newBird)
return newBird
}
override suspend fun updateBird(bird: Bird): Bird {
val index = birds.indexOfFirst { it.id == bird.id }
if (index != -1) {
birds[index] = bird
return bird
}
throw IllegalArgumentException("Bird not found")
}
override suspend fun deleteBird(id: Int): Boolean = birds.removeIf { it.id == id }
}
// ---- PRESENTATION LAYER ----
class BirdViewModel(
private val getBirds: GetBirdsUseCase,
private val addBird: AddBirdUseCase,
private val deleteBird: DeleteBirdUseCase
) : ViewModel() {
private val _uiState = MutableStateFlow(BirdUiState())
val uiState: StateFlow<BirdUiState> = _uiState
private val _events = MutableSharedFlow<BirdEvent>()
val events: SharedFlow<BirdEvent> = _events
init {
loadBirds()
}
fun loadBirds() {
viewModelScope.launch {
_uiState.update { it.copy(isLoading = true) }
try {
val birds = getBirds()
_uiState.update {
it.copy(
isLoading = false,
birds = birds,
totalBirds = birds.sumOf { b -> b.count }
)
}
_events.emit(BirdEvent.LoadSuccess)
} catch (e: Exception) {
_uiState.update {
it.copy(isLoading = false, error = e.message)
}
_events.emit(BirdEvent.LoadError(e.message ?: "Unknown error"))
}
}
}
fun addBird(species: String, count: Int, weight: Double, isMigratory: Boolean) {
viewModelScope.launch {
val result = addBird(species, count, weight, isMigratory)
when (result) {
is Result.Success -> {
_events.emit(BirdEvent.AddSuccess(result.data))
loadBirds()
}
is Result.Error -> {
_events.emit(BirdEvent.AddError(result.message))
}
}
}
}
fun deleteBird(id: Int) {
viewModelScope.launch {
val result = deleteBird(id)
when (result) {
is Result.Success -> {
_events.emit(BirdEvent.DeleteSuccess(id))
loadBirds()
}
is Result.Error -> {
_events.emit(BirdEvent.DeleteError(result.message))
}
}
}
}
}
data class BirdUiState(
val birds: List<Bird> = emptyList(),
val isLoading: Boolean = false,
val totalBirds: Int = 0,
val error: String? = null
)
sealed class BirdEvent {
object LoadSuccess : BirdEvent()
data class LoadError(val message: String) : BirdEvent()
data class AddSuccess(val bird: Bird) : BirdEvent()
data class AddError(val message: String) : BirdEvent()
data class DeleteSuccess(val id: Int) : BirdEvent()
data class DeleteError(val message: String) : BirdEvent()
}
Clean Architecture separates concerns into layers. Domain layer contains business logic. Data layer handles external data sources. Presentation layer manages UI state. Dependency rule ensures outer layers depend on inner layers. Use cases encapsulate application logic.
17.2 Layered Architecture (Controller-Service-Repository)
Layered architecture separates concerns into three main layers.
- Controller layer – Handles HTTP requests/responses.
- Service layer – Business logic and orchestration.
- Repository layer – Data access and persistence.
- Dependency injection – Inject dependencies between layers.
- DTOs – Data transfer objects for API.
Code Example:
// ---- CONTROLLER LAYER ----
@RestController
@RequestMapping("/api/birds")
class BirdController(
private val birdService: BirdService
) {
@GetMapping
fun getBirds(): ResponseEntity<List<BirdResponse>> {
val birds = birdService.getAllBirds()
val responses = birds.map { BirdResponse.fromDomain(it) }
return ResponseEntity.ok(responses)
}
@GetMapping("/{id}")
fun getBird(@PathVariable id: Int): ResponseEntity<BirdResponse> {
val bird = birdService.getBird(id)
?: return ResponseEntity.notFound().build()
return ResponseEntity.ok(BirdResponse.fromDomain(bird))
}
@PostMapping
fun createBird(@RequestBody request: CreateBirdRequest): ResponseEntity<BirdResponse> {
val bird = birdService.createBird(request.toDomain())
return ResponseEntity.status(HttpStatus.CREATED)
.body(BirdResponse.fromDomain(bird))
}
@PutMapping("/{id}")
fun updateBird(
@PathVariable id: Int,
@RequestBody request: UpdateBirdRequest
): ResponseEntity<BirdResponse> {
val bird = birdService.updateBird(id, request.toDomain())
?: return ResponseEntity.notFound().build()
return ResponseEntity.ok(BirdResponse.fromDomain(bird))
}
@DeleteMapping("/{id}")
fun deleteBird(@PathVariable id: Int): ResponseEntity<Void> {
return if (birdService.deleteBird(id)) {
ResponseEntity.noContent().build()
} else {
ResponseEntity.notFound().build()
}
}
@GetMapping("/search")
fun searchBirds(@RequestParam species: String): ResponseEntity<List<BirdResponse>> {
val birds = birdService.searchBirds(species)
val responses = birds.map { BirdResponse.fromDomain(it) }
return ResponseEntity.ok(responses)
}
}
// ---- SERVICE LAYER ----
@Service
class BirdService(
private val birdRepository: BirdRepository,
private val birdValidator: BirdValidator
) {
fun getAllBirds(): List<Bird> = birdRepository.findAll()
fun getBird(id: Int): Bird? = birdRepository.findById(id)
fun createBird(bird: Bird): Bird {
require(birdValidator.validateBird(bird.species, bird.count, bird.weight))
return birdRepository.save(bird)
}
fun updateBird(id: Int, bird: Bird): Bird? {
if (!birdRepository.existsById(id)) {
return null
}
require(birdValidator.validateBird(bird.species, bird.count, bird.weight))
return birdRepository.save(bird.copy(id = id))
}
fun deleteBird(id: Int): Boolean {
if (!birdRepository.existsById(id)) {
return false
}
birdRepository.deleteById(id)
return true
}
fun searchBirds(species: String): List<Bird> {
return birdRepository.findBySpeciesContaining(species)
}
}
// ---- REPOSITORY LAYER ----
interface BirdRepository {
fun findAll(): List<Bird>
fun findById(id: Int): Bird?
fun findBySpeciesContaining(species: String): List<Bird>
fun save(bird: Bird): Bird
fun existsById(id: Int): Boolean
fun deleteById(id: Int)
}
// ---- DTOs ----
data class BirdResponse(
val id: Int,
val species: String,
val count: Int,
val weight: Double,
val isMigratory: Boolean
) {
companion object {
fun fromDomain(bird: Bird): BirdResponse {
return BirdResponse(
id = bird.id,
species = bird.species,
count = bird.count,
weight = bird.weight,
isMigratory = bird.isMigratory
)
}
}
}
data class CreateBirdRequest(
val species: String,
val count: Int,
val weight: Double,
val isMigratory: Boolean
) {
fun toDomain(): Bird {
return Bird(
id = 0,
species = species,
count = count,
weight = weight,
isMigratory = isMigratory
)
}
}
data class UpdateBirdRequest(
val species: String,
val count: Int,
val weight: Double,
val isMigratory: Boolean
) {
fun toDomain(): Bird {
return Bird(
id = 0,
species = species,
count = count,
weight = weight,
isMigratory = isMigratory
)
}
}
Controller layer handles HTTP requests. Service layer contains business logic. Repository layer handles data persistence. DTOs separate API contracts from domain models. Dependency injection connects layers. Each layer has a specific responsibility.
17.3 Modularization (Gradle)
Modularization separates code into independent modules.
- App module – Entry point and configuration.
- Core modules – Shared utilities and extensions.
- Feature modules – Independent features.
- Library modules – Reusable components.
- Build configuration – Gradle settings.
Code Example:
// settings.gradle.kts
rootProject.name = "BirdTracker"
include(
":app",
":core:utils",
":core:database",
":core:network",
":feature:birdlist",
":feature:birddetail",
":feature:search",
":feature:profile",
":library:design",
":library:testing"
)
// build.gradle.kts (app module)
plugins {
id("com.android.application")
id("org.jetbrains.kotlin.android")
id("org.jetbrains.kotlin.kapt")
id("dagger.hilt.android.plugin")
}
android {
namespace = "com.example.birdtracker"
compileSdk = 34
}
dependencies {
implementation(project(":core:utils"))
implementation(project(":core:database"))
implementation(project(":core:network"))
implementation(project(":feature:birdlist"))
implementation(project(":feature:birddetail"))
implementation(project(":feature:search"))
implementation(project(":feature:profile"))
implementation(project(":library:design"))
implementation(libs.androidx.core)
implementation(libs.androidx.compose.ui)
implementation(libs.hilt.android)
kapt(libs.hilt.compiler)
}
// build.gradle.kts (core:utils)
plugins {
id("org.jetbrains.kotlin.android")
id("org.jetbrains.kotlin.kapt")
}
dependencies {
api(libs.kotlinx.coroutines)
api(libs.kotlinx.serialization)
implementation(libs.androidx.core)
}
// build.gradle.kts (feature:birdlist)
plugins {
id("com.android.library")
id("org.jetbrains.kotlin.android")
id("dagger.hilt.android.plugin")
}
android {
namespace = "com.example.feature.birdlist"
}
dependencies {
implementation(project(":core:utils"))
implementation(project(":library:design"))
implementation(libs.hilt.android)
kapt(libs.hilt.compiler)
}
// Module structure
/*
BirdTracker/
├── app/
│ └── src/
│ └── main/
│ └── java/
├── core/
│ ├── utils/
│ │ └── src/
│ ├── database/
│ │ └── src/
│ └── network/
│ └── src/
├── feature/
│ ├── birdlist/
│ │ └── src/
│ ├── birddetail/
│ │ └── src/
│ ├── search/
│ │ └── src/
│ └── profile/
│ └── src/
└── library/
├── design/
│ └── src/
└── testing/
└── src/
*/
Modules separate concerns and improve build times. Core modules provide shared functionality. Feature modules implement specific features. Library modules contain reusable components. Dependencies between modules are explicit. Gradle manages the build configuration.
17.4 Event-Driven Systems (SharedFlow)
Event-driven systems use SharedFlow for communication between components.
- SharedFlow – Hot flow for event broadcasting.
- Event types – Define events as sealed classes.
- Event handlers – Process events asynchronously.
- Decoupling – Components communicate via events.
- State management – Combine events with state.
Code Example:
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.channels.Channel
// ---- EVENT DEFINITION ----
sealed class BirdEvent {
data class BirdAdded(val bird: Bird) : BirdEvent()
data class BirdRemoved(val id: Int) : BirdEvent()
data class BirdUpdated(val bird: Bird) : BirdEvent()
object BirdsCleared : BirdEvent()
data class BirdSighted(val species: String) : BirdEvent()
}
sealed class BirdState {
object Loading : BirdState()
data class Loaded(val birds: List<Bird>) : BirdState()
data class Error(val message: String) : BirdState()
}
// ---- EVENT BUS ----
class BirdEventBus {
private val _events = MutableSharedFlow<BirdEvent>(
replay = 0,
extraBufferCapacity = 64,
onBufferOverflow = BufferOverflow.DROP_OLDEST
)
val events: SharedFlow<BirdEvent> = _events.asSharedFlow()
suspend fun emit(event: BirdEvent) {
_events.emit(event)
}
fun tryEmit(event: BirdEvent): Boolean {
return _events.tryEmit(event)
}
}
// ---- EVENT PROCESSOR ----
class BirdEventProcessor(
private val eventBus: BirdEventBus,
private val repository: BirdRepository
) {
private val scope = CoroutineScope(SupervisorJob())
init {
// Process events as they arrive
scope.launch {
eventBus.events.collect { event ->
processEvent(event)
}
}
}
private suspend fun processEvent(event: BirdEvent) {
when (event) {
is BirdEvent.BirdAdded -> {
println("Processing BirdAdded: ${event.bird.species}")
// Perform side effects like logging, analytics
// Update caches, etc.
}
is BirdEvent.BirdRemoved -> {
println("Processing BirdRemoved: ${event.id}")
// Handle bird removal
}
is BirdEvent.BirdUpdated -> {
println("Processing BirdUpdated: ${event.bird.species}")
// Handle bird update
}
is BirdEvent.BirdsCleared -> {
println("Processing BirdsCleared")
// Handle clearing
}
is BirdEvent.BirdSighted -> {
println("Bird sighted: ${event.species}")
// Handle sighting
}
}
}
fun stop() {
scope.cancel()
}
}
// ---- EVENT CONSUMER ----
class BirdEventConsumer(
private val eventBus: BirdEventBus
) {
private val scope = CoroutineScope(Dispatchers.IO)
fun startConsuming() {
scope.launch {
eventBus.events
.filterIsInstance<BirdEvent.BirdAdded>()
.collect { event ->
// Only process BirdAdded events
println("New bird added: ${event.bird.species}")
// Update UI, databases, etc.
}
}
scope.launch {
eventBus.events
.filter { it is BirdEvent.BirdRemoved }
.collect { event ->
if (event is BirdEvent.BirdRemoved) {
println("Bird removed: ${event.id}")
}
}
}
}
fun stop() {
scope.cancel()
}
}
// ---- SERVICE USING EVENTS ----
class BirdEventService(
private val eventBus: BirdEventBus,
private val repository: BirdRepository
) {
suspend fun addBird(bird: Bird) {
try {
val saved = repository.addBird(bird)
eventBus.emit(BirdEvent.BirdAdded(saved))
} catch (e: Exception) {
println("Error adding bird: ${e.message}")
}
}
suspend fun deleteBird(id: Int) {
try {
val success = repository.deleteBird(id)
if (success) {
eventBus.emit(BirdEvent.BirdRemoved(id))
}
} catch (e: Exception) {
println("Error deleting bird: ${e.message}")
}
}
}
// ---- COMPOSE UI WITH EVENTS ----
import androidx.compose.runtime.*
import androidx.compose.material.*
@Composable
fun BirdEventsExample(
service: BirdEventService
) {
val eventBus = remember { BirdEventBus() }
val processor = remember { BirdEventProcessor(eventBus, InMemoryBirdRepository()) }
val consumer = remember { BirdEventConsumer(eventBus) }
var events by remember { mutableStateOf<List<String>>(emptyList()) }
LaunchedEffect(Unit) {
consumer.startConsuming()
eventBus.events
.map { event ->
when (event) {
is BirdEvent.BirdAdded -> "Added: ${event.bird.species}"
is BirdEvent.BirdRemoved -> "Removed: ${event.id}"
is BirdEvent.BirdUpdated -> "Updated: ${event.bird.species}"
is BirdEvent.BirdsCleared -> "Cleared all"
is BirdEvent.BirdSighted -> "Sighted: ${event.species}"
}
}
.collect { eventMessage ->
events = (events + eventMessage).takeLast(10)
}
}
Column {
Text("Recent Events", style = MaterialTheme.typography.h6)
Spacer(modifier = Modifier.height(8.dp))
Button(onClick = {
CoroutineScope(Dispatchers.IO).launch {
service.addBird(Bird(0, "Sparrow", 10, 25.5, true))
}
}) {
Text("Add Sparrow")
}
Button(onClick = {
CoroutineScope(Dispatchers.IO).launch {
service.deleteBird(1)
}
}) {
Text("Delete Bird")
}
Button(onClick = {
CoroutineScope(Dispatchers.IO).launch {
eventBus.emit(BirdEvent.BirdSighted("Eagle"))
}
}) {
Text("Sight Eagle")
}
Spacer(modifier = Modifier.height(16.dp))
Text("Last 10 events:")
events.forEach { event ->
Text("• $event", style = MaterialTheme.typography.body2)
}
}
}
SharedFlow broadcasts events to multiple consumers. Events are defined as sealed classes for type safety. Event processors handle events asynchronously. Event bus decouples event producers from consumers. UI consumes events for updates.
Production Readiness
18.1 Logging (SLF4J + Logback)
Logging provides visibility into application behavior.
- SLF4J – Simple logging facade.
- Logback – Implementation for SLF4J.
- Log levels categorize messages according to their importance and purpose. Common levels include TRACE, DEBUG, INFO, WARN, and ERROR, ranging from detailed diagnostic information to serious problems.
- Configuration –
logback.xmlfor configuration. - Structured logging – JSON format for machine parsing.
Code Example:
// build.gradle.kts dependencies
dependencies {
implementation("org.slf4j:slf4j-api:2.0.9")
implementation("ch.qos.logback:logback-classic:1.4.14")
implementation("ch.qos.logback:logback-core:1.4.14")
implementation("net.logstash.logback:logstash-logback-encoder:7.4")
}
// logback.xml (resources directory)
<configuration>
<property name="LOG_PATH" value="${LOG_PATH:-./logs}" />
<property name="LOG_FILE" value="${LOG_FILE:-application}" />
<!-- Console appender -->
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<!-- File appender -->
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_PATH}/${LOG_FILE}.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${LOG_PATH}/${LOG_FILE}.%d{yyyy-MM-dd}.%i.log</fileNamePattern>
<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<maxFileSize>10MB</maxFileSize>
</timeBasedFileNamingAndTriggeringPolicy>
<maxHistory>30</maxHistory>
</rollingPolicy>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<!-- JSON appender for structured logging -->
<appender name="JSON" class="ch.qos.logback.core.FileAppender">
<file>${LOG_PATH}/application.json.log</file>
<encoder class="net.logstash.logback.encoder.LogstashEncoder" />
</appender>
<!-- Root logger -->
<root level="INFO">
<appender-ref ref="CONSOLE" />
<appender-ref ref="FILE" />
<appender-ref ref="JSON" />
</root>
</configuration>
// ---- LOGGER USAGE ----
import org.slf4j.LoggerFactory
import org.slf4j.Logger
// Direct usage
class BirdService {
private val logger = LoggerFactory.getLogger(BirdService::class.java)
fun addBird(bird: Bird) {
logger.info("Adding bird: ${bird.species}")
try {
// Operation
logger.debug("Bird added successfully: ${bird.species}")
} catch (e: Exception) {
logger.error("Failed to add bird: ${bird.species}", e)
throw e
}
}
}
// With companion object
class BirdController {
companion object {
private val logger = LoggerFactory.getLogger(BirdController::class.java)
}
fun getBirds(): List<Bird> {
logger.info("Getting all birds")
return emptyList()
}
}
// Structured logging
data class LogContext(
val userId: String? = null,
val sessionId: String? = null,
val requestId: String? = null,
val correlationId: String? = null
)
class BirdLogger {
private val logger = LoggerFactory.getLogger("BirdLogger")
private var context = LogContext()
fun withContext(context: LogContext): BirdLogger {
this.context = context
return this
}
fun info(message: String, vararg args: Any) {
val contextMap = mapOf(
"userId" to context.userId,
"sessionId" to context.sessionId,
"requestId" to context.requestId,
"correlationId" to context.correlationId
)
logger.info("{} | context={} | message={}", args.joinToString(), contextMap, message)
}
fun error(message: String, throwable: Throwable? = null) {
val contextMap = mapOf(
"userId" to context.userId,
"sessionId" to context.sessionId,
"requestId" to context.requestId,
"correlationId" to context.correlationId
)
if (throwable != null) {
logger.error("message={} | context={}", message, contextMap, throwable)
} else {
logger.error("message={} | context={}", message, contextMap)
}
}
}
SLF4J provides a logging facade. Logback handles the actual logging. Log levels control verbosity. Configuration defines appenders and patterns. Structured logging outputs JSON for machine parsing. MDC (Mapped Diagnostic Context) attaches contextual information to log entries, making it easier to associate messages with a particular request, user session, or operation.
18.2 Monitoring (Health check in Ktor)
Health checks monitor application status.
- Health endpoints –
/health,/ready,/live. - Readiness – Application is ready for traffic.
- Liveness – Application is running.
- Dependency checks – Database, external services.
- Metrics – Collect performance metrics.
Code Example:
import io.ktor.application.*
import io.ktor.response.*
import io.ktor.routing.*
import io.ktor.server.engine.*
import io.ktor.server.netty.*
import java.sql.DriverManager
import kotlinx.coroutines.*
// ---- HEALTH CHECK SERVICE ----
data class HealthStatus(
val status: String,
val checks: Map<String, CheckStatus>,
val timestamp: String,
val version: String
)
data class CheckStatus(
val status: String,
val message: String? = null,
val details: Map<String, Any>? = null
)
class HealthCheckService {
private val checks = mutableListOf<HealthCheck>()
private val version = "1.0.0"
fun addCheck(check: HealthCheck) {
checks.add(check)
}
suspend fun checkAll(): HealthStatus {
val results = mutableMapOf<String, CheckStatus>()
var overall = "UP"
for (check in checks) {
try {
val result = check.check()
results[check.name] = result
if (result.status != "UP") {
overall = "DEGRADED"
}
} catch (e: Exception) {
results[check.name] = CheckStatus(
status = "DOWN",
message = e.message
)
overall = "DOWN"
}
}
return HealthStatus(
status = overall,
checks = results,
timestamp = java.time.Instant.now().toString(),
version = version
)
}
}
interface HealthCheck {
val name: String
suspend fun check(): CheckStatus
}
// ---- DATABASE HEALTH CHECK ----
class DatabaseHealthCheck(
private val url: String,
private val username: String,
private val password: String
) : HealthCheck {
override val name = "database"
override suspend fun check(): CheckStatus {
return try {
val connection = DriverManager.getConnection(url, username, password)
connection.use {
val statement = it.createStatement()
val result = statement.executeQuery("SELECT 1")
if (result.next()) {
CheckStatus(
status = "UP",
details = mapOf(
"connected" to true
)
)
} else {
CheckStatus(
status = "DOWN",
message = "Query failed"
)
}
}
} catch (e: Exception) {
CheckStatus(
status = "DOWN",
message = e.message
)
}
}
}
// ---- SERVICE HEALTH CHECK ----
class BirdServiceHealthCheck(
private val birdService: BirdService
) : HealthCheck {
override val name = "bird-service"
override suspend fun check(): CheckStatus {
return try {
val birds = birdService.getBirds()
CheckStatus(
status = "UP",
details = mapOf(
"totalBirds" to birds.size
)
)
} catch (e: Exception) {
CheckStatus(
status = "DOWN",
message = e.message
)
}
}
}
// ---- ENVIRONMENT CHECK ----
class EnvironmentHealthCheck : HealthCheck {
override val name = "environment"
override suspend fun check(): CheckStatus {
val checks = mutableMapOf<String, Any>()
checks["memory"] = mapOf(
"free" to Runtime.getRuntime().freeMemory() / 1024 / 1024,
"total" to Runtime.getRuntime().totalMemory() / 1024 / 1024,
"max" to Runtime.getRuntime().maxMemory() / 1024 / 1024
)
checks["processors"] = Runtime.getRuntime().availableProcessors()
return CheckStatus(
status = "UP",
details = checks
)
}
}
// ---- KTOR APPLICATION WITH HEALTH CHECKS ----
fun main() {
val healthService = HealthCheckService()
// Add health checks
healthService.addCheck(EnvironmentHealthCheck())
healthService.addCheck(DatabaseHealthCheck(
url = "jdbc:h2:mem:test",
username = "sa",
password = ""
))
healthService.addCheck(BirdServiceHealthCheck(BirdService()))
embeddedServer(Netty, port = 8080) {
routing {
// Full health check
get("/health") {
val status = healthService.checkAll()
val httpStatus = when (status.status) {
"UP" -> HttpStatusCode.OK
"DEGRADED" -> HttpStatusCode.OK
"DOWN" -> HttpStatusCode.ServiceUnavailable
else -> HttpStatusCode.InternalServerError
}
call.respond(httpStatus, status)
}
// Liveness check - basic availability
get("/health/live") {
call.respond(mapOf("status" to "alive"))
}
// Readiness check - ready to serve traffic
get("/health/ready") {
val status = healthService.checkAll()
if (status.status == "DOWN") {
call.respond(HttpStatusCode.ServiceUnavailable, status)
} else {
call.respond(HttpStatusCode.OK, status)
}
}
}
}.start(wait = true)
}
// ---- METRICS COLLECTION ----
class MetricsCollector {
private val metrics = mutableMapOf<String, MutableList<Long>>()
fun recordMetric(name: String, value: Long) {
metrics.getOrPut(name) { mutableListOf() }.add(value)
// Trim to keep only recent values
if (metrics[name]?.size ?: 0 > 1000) {
metrics[name]?.removeAt(0)
}
}
fun getStats(name: String): Map<String, Any> {
val values = metrics[name] ?: return emptyMap()
return mapOf(
"min" to values.minOrNull(),
"max" to values.maxOrNull(),
"avg" to values.average(),
"count" to values.size,
"last" to values.lastOrNull()
)
}
}
// Usage example
class BirdMetrics {
private val metrics = MetricsCollector()
fun recordBirdAddition(species: String, count: Int) {
metrics.recordMetric("bird.addition", count.toLong())
metrics.recordMetric("bird.species.$species.count", 1)
}
fun getMetricReport(): Map<String, Any> {
return mapOf(
"bird.addition" to metrics.getStats("bird.addition"),
"total_metrics" to metrics.stats()
)
}
private fun MetricsCollector.stats(): Map<String, Any> {
return this::class.members
.filter { it.name.startsWith("getStats") }
.associate { it.name to it.call(this) }
}
}
Health checks verify application status. Liveness checks indicate the application is running. Readiness checks indicate the application can handle requests. Dependency checks verify external services. Metrics collect performance data. Endpoints provide monitoring information.
18.3 Security (JWT authentication in Ktor)
JWT authentication secures API endpoints.
- JWT – JSON Web Tokens for authentication.
- Authentication – Verify token before allowing access.
- Authorization – Check user permissions.
- Token generation – Create tokens on login.
- Token validation – Verify token validity.
Code Example:
import io.ktor.application.*
import io.ktor.auth.*
import io.ktor.auth.jwt.*
import io.ktor.response.*
import io.ktor.routing.*
import io.ktor.server.engine.*
import io.ktor.server.netty.*
import com.auth0.jwt.JWT
import com.auth0.jwt.algorithms.Algorithm
import java.time.Instant
import java.time.temporal.ChronoUnit
// ---- USER SERVICE ----
data class User(
val id: Int,
val username: String,
val password: String,
val role: String
)
class UserService {
private val users = listOf(
User(1, "admin", "admin123", "ADMIN"),
User(2, "user", "user123", "USER"),
User(3, "viewer", "viewer123", "VIEWER")
)
fun findUser(username: String, password: String): User? {
return users.find { it.username == username && it.password == password }
}
fun findUser(id: Int): User? {
return users.find { it.id == id }
}
}
// ---- JWT CONFIGURATION ----
class JwtConfig {
companion object {
const val ISSUER = "bird-tracker"
const val AUDIENCE = "bird-tracker-users"
const val SECRET = "my-secret-key-change-in-production"
const val TOKEN_VALIDITY_MINUTES = 60L
}
val algorithm = Algorithm.HMAC256(SECRET)
fun createToken(user: User): String {
return JWT.create()
.withIssuer(ISSUER)
.withAudience(AUDIENCE)
.withSubject(user.id.toString())
.withClaim("username", user.username)
.withClaim("role", user.role)
.withExpiresAt(Instant.now().plus(TOKEN_VALIDITY_MINUTES, ChronoUnit.MINUTES))
.sign(algorithm)
}
}
// ---- AUTHENTICATION SERVICE ----
class AuthService {
private val userService = UserService()
private val jwtConfig = JwtConfig()
fun authenticate(username: String, password: String): String? {
val user = userService.findUser(username, password)
return user?.let { jwtConfig.createToken(it) }
}
fun validateToken(token: String): User? {
return try {
val decoded = JWT.require(jwtConfig.algorithm)
.withIssuer(JwtConfig.ISSUER)
.withAudience(JwtConfig.AUDIENCE)
.build()
.verify(token)
val userId = decoded.subject?.toIntOrNull()
userId?.let { userService.findUser(it) }
} catch (e: Exception) {
null
}
}
}
// ---- KTOR APPLICATION WITH AUTH ----
fun main() {
val authService = AuthService()
val jwtConfig = JwtConfig()
embeddedServer(Netty, port = 8080) {
// Install authentication
install(Authentication) {
jwt("auth-jwt") {
verifier(JWT.require(jwtConfig.algorithm)
.withIssuer(JwtConfig.ISSUER)
.withAudience(JwtConfig.AUDIENCE)
.build()
)
validate { credential ->
val userId = credential.payload.subject?.toIntOrNull()
val user = userId?.let { authService.validateToken(credential.token) }
if (user != null) {
JWTPrincipal(credential.payload)
} else {
null
}
}
}
basic("auth-basic") {
validate { credentials ->
val user = authService.validateToken(credentials.name)
if (user != null) {
UserIdPrincipal(credentials.name)
} else {
null
}
}
}
}
routing {
// Public endpoint - login
post("/auth/login") {
val body = call.receive<LoginRequest>()
val token = authService.authenticate(body.username, body.password)
if (token != null) {
call.respond(mapOf("token" to token))
} else {
call.respond(HttpStatusCode.Unauthorized, mapOf("error" to "Invalid credentials"))
}
}
// Protected endpoint - all authenticated users
authenticate("auth-jwt") {
get("/birds") {
val principal = call.principal<JWTPrincipal>()
val claims = principal?.payload
val username = claims?.getClaim("username")?.asString() ?: "Unknown"
call.respond(mapOf(
"message" to "Hello $username!",
"birds" to listOf("Sparrow", "Eagle", "Hawk")
))
}
}
// Role-based access - admin only
authenticate("auth-jwt") {
get("/birds/admin") {
val principal = call.principal<JWTPrincipal>()
val role = principal?.payload?.getClaim("role")?.asString()
if (role != "ADMIN") {
call.respond(HttpStatusCode.Forbidden, mapOf("error" to "Admin access required"))
return@get
}
call.respond(mapOf(
"message" to "Admin panel",
"users" to listOf("admin", "user", "viewer")
))
}
}
// Refresh token
post("/auth/refresh") {
val body = call.receive<RefreshRequest>()
val user = authService.validateToken(body.token)
if (user != null) {
val newToken = jwtConfig.createToken(user)
call.respond(mapOf("token" to newToken))
} else {
call.respond(HttpStatusCode.Unauthorized, mapOf("error" to "Invalid token"))
}
}
// Logout
post("/auth/logout") {
// Client-side: discard token
// Server-side: could add token to blacklist
call.respond(mapOf("message" to "Logged out successfully"))
}
}
}.start(wait = true)
}
// ---- REQUEST/RESPONSE DTOs ----
data class LoginRequest(
val username: String,
val password: String
)
data class RefreshRequest(
val token: String
)
data class AuthResponse(
val token: String,
val refreshToken: String? = null
)
// ---- TOKEN BLACKLIST ----
class TokenBlacklist {
private val blacklisted = mutableSetOf<String>()
fun add(token: String) {
blacklisted.add(token)
}
fun isBlacklisted(token: String): Boolean {
return token in blacklisted
}
fun cleanup() {
// Remove expired tokens from blacklist
blacklisted.clear()
}
}
// ---- PERMISSION CHECKER ----
class PermissionChecker {
fun checkPermission(role: String, requiredRole: String): Boolean {
val roleHierarchy = mapOf(
"ADMIN" to listOf("ADMIN", "MANAGER", "USER", "VIEWER"),
"MANAGER" to listOf("MANAGER", "USER", "VIEWER"),
"USER" to listOf("USER", "VIEWER"),
"VIEWER" to listOf("VIEWER")
)
return roleHierarchy[role]?.contains(requiredRole) ?: false
}
}
// ---- SECURE ENDPOINT WITH PERMISSION ----
fun Routing.secureEndpoint() {
authenticate("auth-jwt") {
get("/secure/data") {
val principal = call.principal<JWTPrincipal>()
val role = principal?.payload?.getClaim("role")?.asString() ?: "VIEWER"
val checker = PermissionChecker()
if (!checker.checkPermission(role, "USER")) {
call.respond(HttpStatusCode.Forbidden, mapOf("error" to "Insufficient permissions"))
return@get
}
call.respond(mapOf(
"data" to "Sensitive bird data",
"role" to role
))
}
}
}
JWT tokens contain user identity and claims. Authentication verifies token validity. Authorization checks user permissions. Token generation creates signed tokens. Token validation verifies token integrity and expiration. Role-based access controls endpoint access.
18.4 CI/CD (GitHub Actions)
CI/CD automates building, testing, and deployment.
- Continuous Integration – Build and test on every push.
- Continuous Delivery – Automate release process.
- GitHub Actions – Workflow definition in YAML.
- Build steps – Compile, test, package.
- Deployment – Deploy to environments.
Code Example:
# .github/workflows/ci.yml
name: CI Pipeline
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main ]
jobs:
build-and-test:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Set up JDK 17
uses: actions/setup-java@v3
with:
java-version: '17'
distribution: 'temurin'
cache: 'gradle'
- name: Grant execute permission for gradlew
run: chmod +x gradlew
- name: Cache Gradle dependencies
uses: actions/cache@v3
with:
path: ~/.gradle/caches
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*') }}
restore-keys: |
${{ runner.os }}-gradle-
- name: Build and test
run: ./gradlew build test
- name: Run static analysis
run: ./gradlew detekt
- name: Generate test report
run: ./gradlew jacocoTestReport
- name: Upload test results
uses: actions/upload-artifact@v3
with:
name: test-reports
path: build/reports/tests/
- name: Upload coverage report
uses: actions/upload-artifact@v3
with:
name: coverage-report
path: build/reports/jacoco/
- name: Upload build artifacts
uses: actions/upload-artifact@v3
with:
name: app-jars
path: build/libs/*.jar
docker-build:
needs: build-and-test
runs-on: ubuntu-latest
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Login to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Build and push Docker image
uses: docker/build-push-action@v4
with:
context: .
push: true
tags: |
${{ secrets.DOCKER_USERNAME }}/bird-tracker:latest
${{ secrets.DOCKER_USERNAME }}/bird-tracker:${{ github.sha }}
cache-from: type=registry,ref=${{ secrets.DOCKER_USERNAME }}/bird-tracker:buildcache
cache-to: type=registry,ref=${{ secrets.DOCKER_USERNAME }}/bird-tracker:buildcache,mode=max
deploy:
needs: docker-build
runs-on: ubuntu-latest
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
steps:
- name: Deploy to production
uses: appleboy/ssh-action@v0.1.9
with:
host: ${{ secrets.DEPLOY_HOST }}
username: ${{ secrets.DEPLOY_USER }}
key: ${{ secrets.DEPLOY_KEY }}
script: |
docker pull ${{ secrets.DOCKER_USERNAME }}/bird-tracker:latest
docker stop bird-tracker || true
docker rm bird-tracker || true
docker run -d --name bird-tracker \
-p 8080:8080 \
-e SPRING_PROFILES_ACTIVE=production \
${{ secrets.DOCKER_USERNAME }}/bird-tracker:latest
notify:
needs: deploy
runs-on: ubuntu-latest
if: success()
steps:
- name: Send notification
uses: slackapi/slack-github-action@v1.24.0
with:
channel-id: 'deployments'
slack-message: '✅ Deployment successful!'
env:
SLACK_BOT_TOKEN: ${{ secrets.SLACK_TOKEN }}
Gradle Configuration:
// build.gradle.kts for CI
plugins {
kotlin("jvm") version "1.9.0"
id("jacoco")
id("io.gitlab.arturbosch.detekt") version "1.23.0"
}
dependencies {
// Dependencies
testImplementation("org.junit.jupiter:junit-jupiter:5.9.2")
testImplementation("io.mockk:mockk:1.13.5")
}
tasks.test {
useJUnitPlatform()
finalizedBy(tasks.jacocoTestReport)
}
tasks.jacocoTestReport {
dependsOn(tasks.test)
reports {
xml.required.set(true)
html.required.set(true)
}
}
tasks.jacocoTestCoverageVerification {
violationRules {
rule {
limit {
minimum = 0.80.toBigDecimal()
}
}
}
}
detekt {
config = files("detekt.yml")
buildUponDefaultConfig = true
}
tasks.register("ci") {
dependsOn("build", "test", "jacocoTestReport", "detekt")
}
// Dockerfile
FROM openjdk:17-slim
WORKDIR /app
COPY build/libs/*.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]
// docker-compose.yml
version: '3.8'
services:
app:
build: .
ports:
- "8080:8080"
environment:
- SPRING_PROFILES_ACTIVE=docker
- DB_HOST=postgres
depends_on:
- postgres
networks:
- bird-network
postgres:
image: postgres:15
environment:
- POSTGRES_DB=birds
- POSTGRES_USER=user
- POSTGRES_PASSWORD=password
ports:
- "5432:5432"
networks:
- bird-network
redis:
image: redis:7
ports:
- "6379:6379"
networks:
- bird-network
networks:
bird-network:
driver: bridge
CI/CD automates the entire software delivery pipeline. CI runs on every push, building and testing the code. Static analysis checks code quality. Docker builds container images. Deployment automatically deploys to production. Notifications keep the team informed.
Real-World Projects
19.1 Console Application (Address Book)
A console-based address book application.
- CRUD operations – Create, Read, Update, Delete contacts.
- Data persistence – Save to JSON file.
- Search – Find contacts by name.
- Validation – Validate input data.
- Error handling – Handle user input errors.
Code Example:
import kotlinx.serialization.*
import kotlinx.serialization.json.*
import java.io.File
@Serializable
data class Contact(
val id: Int,
val name: String,
val phone: String,
val email: String? = null,
val address: String? = null
)
class AddressBook {
private val contacts = mutableListOf<Contact>()
private var nextId = 1
private val dataFile = File("contacts.json")
init {
loadFromFile()
}
fun addContact(name: String, phone: String, email: String? = null, address: String? = null): Contact {
val contact = Contact(nextId++, name, phone, email, address)
contacts.add(contact)
saveToFile()
return contact
}
fun getContact(id: Int): Contact? = contacts.find { it.id == id }
fun getAllContacts(): List<Contact> = contacts.toList()
fun searchContacts(query: String): List<Contact> {
val lowerQuery = query.lowercase()
return contacts.filter {
it.name.lowercase().contains(lowerQuery) ||
it.phone.contains(query) ||
it.email?.lowercase()?.contains(lowerQuery) == true
}
}
fun updateContact(id: Int, name: String? = null, phone: String? = null,
email: String? = null, address: String? = null): Contact? {
val index = contacts.indexOfFirst { it.id == id }
if (index == -1) return null
val old = contacts[index]
val updated = Contact(
id = old.id,
name = name ?: old.name,
phone = phone ?: old.phone,
email = email ?: old.email,
address = address ?: old.address
)
contacts[index] = updated
saveToFile()
return updated
}
fun deleteContact(id: Int): Boolean {
val removed = contacts.removeIf { it.id == id }
if (removed) saveToFile()
return removed
}
fun deleteAllContacts() {
contacts.clear()
nextId = 1
saveToFile()
}
fun getContactCount(): Int = contacts.size
private fun saveToFile() {
try {
val json = Json.encodeToString(contacts)
dataFile.writeText(json)
} catch (e: Exception) {
println("Error saving contacts: ${e.message}")
}
}
private fun loadFromFile() {
if (!dataFile.exists()) return
try {
val json = dataFile.readText()
val loaded = Json.decodeFromString<List<Contact>>(json)
contacts.clear()
contacts.addAll(loaded)
nextId = (contacts.map { it.id }.maxOrNull() ?: 0) + 1
} catch (e: Exception) {
println("Error loading contacts: ${e.message}")
}
}
}
fun main() {
val addressBook = AddressBook()
var running = true
while (running) {
println("\n=== Address Book ===")
println("1. Add Contact")
println("2. View Contacts")
println("3. Search Contacts")
println("4. Edit Contact")
println("5. Delete Contact")
println("6. Delete All")
println("7. Exit")
print("Choose an option: ")
when (readLine()?.toIntOrNull()) {
1 -> addContact(addressBook)
2 -> viewContacts(addressBook)
3 -> searchContacts(addressBook)
4 -> editContact(addressBook)
5 -> deleteContact(addressBook)
6 -> deleteAllContacts(addressBook)
7 -> {
running = false
println("Goodbye!")
}
else -> println("Invalid option. Please try again.")
}
}
}
fun addContact(addressBook: AddressBook) {
println("\nAdd New Contact")
print("Name: ")
val name = readLine() ?: return
print("Phone: ")
val phone = readLine() ?: return
print("Email (optional): ")
val email = readLine()?.takeIf { it.isNotBlank() }
print("Address (optional): ")
val address = readLine()?.takeIf { it.isNotBlank() }
if (name.isBlank() || phone.isBlank()) {
println("Name and phone are required!")
return
}
val contact = addressBook.addContact(name, phone, email, address)
println("Contact added with ID: ${contact.id}")
}
fun viewContacts(addressBook: AddressBook) {
val contacts = addressBook.getAllContacts()
if (contacts.isEmpty()) {
println("No contacts found.")
return
}
println("\n=== Contacts (${contacts.size}) ===")
contacts.forEach { contact ->
println("ID: ${contact.id}")
println(" Name: ${contact.name}")
println(" Phone: ${contact.phone}")
if (contact.email != null) println(" Email: ${contact.email}")
if (contact.address != null) println(" Address: ${contact.address}")
println()
}
}
fun searchContacts(addressBook: AddressBook) {
print("Search for (name, phone, or email): ")
val query = readLine() ?: return
val results = addressBook.searchContacts(query)
if (results.isEmpty()) {
println("No contacts found.")
return
}
println("\n=== Search Results (${results.size}) ===")
results.forEach { contact ->
println("ID: ${contact.id} | ${contact.name} | ${contact.phone}")
}
}
fun editContact(addressBook: AddressBook) {
print("Enter contact ID to edit: ")
val id = readLine()?.toIntOrNull() ?: run {
println("Invalid ID")
return
}
val contact = addressBook.getContact(id)
if (contact == null) {
println("Contact not found.")
return
}
println("\nEditing Contact: ${contact.name}")
println("Press Enter to keep current value")
print("Name (${contact.name}): ")
val name = readLine()?.takeIf { it.isNotBlank() }
print("Phone (${contact.phone}): ")
val phone = readLine()?.takeIf { it.isNotBlank() }
print("Email (${contact.email ?: "none"}): ")
val email = readLine()?.takeIf { it.isNotBlank() }
print("Address (${contact.address ?: "none"}): ")
val address = readLine()?.takeIf { it.isNotBlank() }
val updated = addressBook.updateContact(id, name, phone, email, address)
if (updated != null) {
println("Contact updated successfully!")
} else {
println("Failed to update contact.")
}
}
fun deleteContact(addressBook: AddressBook) {
print("Enter contact ID to delete: ")
val id = readLine()?.toIntOrNull() ?: run {
println("Invalid ID")
return
}
val contact = addressBook.getContact(id)
if (contact == null) {
println("Contact not found.")
return
}
println("Delete contact: ${contact.name} (${contact.phone})")
print("Are you sure? (y/n): ")
if (readLine()?.lowercase() == "y") {
if (addressBook.deleteContact(id)) {
println("Contact deleted successfully!")
} else {
println("Failed to delete contact.")
}
}
}
fun deleteAllContacts(addressBook: AddressBook) {
if (addressBook.getContactCount() == 0) {
println("No contacts to delete.")
return
}
print("Delete all ${addressBook.getContactCount()} contacts? (y/n): ")
if (readLine()?.lowercase() == "y") {
addressBook.deleteAllContacts()
println("All contacts deleted successfully!")
}
}
The address book stores contacts with CRUD operations. JSON serialization provides data persistence. Search functionality finds contacts by name, phone, or email. Input validation ensures data quality. The interactive menu guides user operations.
19.2 REST API (Product Catalog)
A REST API designed to manage a product catalog, including operations for creating, retrieving, updating, and deleting product records.
- REST endpoints – CRUD operations for products.
- Data validation – Validate product data.
- Pagination – Page through products.
- Filtering – Filter by category or price.
- Error handling – Consistent error responses.
Code Example:
import io.ktor.application.*
import io.ktor.http.*
import io.ktor.response.*
import io.ktor.request.*
import io.ktor.routing.*
import io.ktor.features.*
import io.ktor.serialization.json.*
import io.ktor.server.engine.*
import io.ktor.server.netty.*
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.*
// ---- DATA MODELS ----
@Serializable
data class Product(
val id: Int,
val name: String,
val description: String,
val price: Double,
val category: String,
val stock: Int,
val weight: Double,
val createdAt: String,
val updatedAt: String
)
@Serializable
data class CreateProductRequest(
val name: String,
val description: String,
val price: Double,
val category: String,
val stock: Int,
val weight: Double
)
@Serializable
data class UpdateProductRequest(
val name: String? = null,
val description: String? = null,
val price: Double? = null,
val category: String? = null,
val stock: Int? = null,
val weight: Double? = null
)
@Serializable
data class PaginatedResponse<T>(
val items: List<T>,
val total: Int,
val page: Int,
val pageSize: Int,
val totalPages: Int
)
@Serializable
data class ApiError(
val error: String,
val status: Int,
val timestamp: String,
val path: String? = null
)
// ---- PRODUCT SERVICE ----
class ProductService {
private val products = mutableListOf<Product>(
Product(
id = 1,
name = "Sparrow Food",
description = "Premium bird food for sparrows",
price = 19.99,
category = "Food",
stock = 100,
weight = 500.0,
createdAt = java.time.Instant.now().toString(),
updatedAt = java.time.Instant.now().toString()
),
Product(
id = 2,
name = "Eagle Nest",
description = "Large nesting platform for eagles",
price = 299.99,
category = "Nesting",
stock = 5,
weight = 5000.0,
createdAt = java.time.Instant.now().toString(),
updatedAt = java.time.Instant.now().toString()
),
Product(
id = 3,
name = "Hawk Feeder",
description = "Durable feeder for hawks",
price = 59.99,
category = "Feeder",
stock = 20,
weight = 1000.0,
createdAt = java.time.Instant.now().toString(),
updatedAt = java.time.Instant.now().toString()
)
)
private var nextId = 4
fun getProducts(page: Int = 0, pageSize: Int = 10, category: String? = null): PaginatedResponse<Product> {
var filtered = products.toList()
if (category != null) {
filtered = filtered.filter { it.category.equals(category, ignoreCase = true) }
}
val total = filtered.size
val totalPages = (total + pageSize - 1) / pageSize
val start = page * pageSize
val end = minOf(start + pageSize, total)
val items = if (start < total) filtered.subList(start, end) else emptyList()
return PaginatedResponse(
items = items,
total = total,
page = page,
pageSize = pageSize,
totalPages = totalPages
)
}
fun getProduct(id: Int): Product? = products.find { it.id == id }
fun createProduct(request: CreateProductRequest): Product {
val now = java.time.Instant.now().toString()
val product = Product(
id = nextId++,
name = request.name,
description = request.description,
price = request.price,
category = request.category,
stock = request.stock,
weight = request.weight,
createdAt = now,
updatedAt = now
)
products.add(product)
return product
}
fun updateProduct(id: Int, request: UpdateProductRequest): Product? {
val index = products.indexOfFirst { it.id == id }
if (index == -1) return null
val old = products[index]
val now = java.time.Instant.now().toString()
val updated = Product(
id = old.id,
name = request.name ?: old.name,
description = request.description ?: old.description,
price = request.price ?: old.price,
category = request.category ?: old.category,
stock = request.stock ?: old.stock,
weight = request.weight ?: old.weight,
createdAt = old.createdAt,
updatedAt = now
)
products[index] = updated
return updated
}
fun deleteProduct(id: Int): Boolean = products.removeIf { it.id == id }
fun searchProducts(query: String): List<Product> {
val lowerQuery = query.lowercase()
return products.filter {
it.name.lowercase().contains(lowerQuery) ||
it.description.lowercase().contains(lowerQuery) ||
it.category.lowercase().contains(lowerQuery)
}
}
fun getCategories(): List<String> = products.map { it.category }.distinct()
fun getProductStats(): Map<String, Any> {
return mapOf(
"totalProducts" to products.size,
"totalStock" to products.sumOf { it.stock },
"averagePrice" to products.map { it.price }.average(),
"categories" to getCategories()
)
}
}
// ---- MAIN APPLICATION ----
fun main() {
val productService = ProductService()
embeddedServer(Netty, port = 8080) {
install(ContentNegotiation) {
json(Json {
prettyPrint = true
encodeDefaults = false
})
}
install(CORS) {
anyHost()
allowHeaders { true }
}
routing {
// Health check
get("/health") {
call.respond(mapOf("status" to "healthy"))
}
// Stats endpoint
get("/api/products/stats") {
call.respond(productService.getProductStats())
}
// Categories endpoint
get("/api/products/categories") {
call.respond(productService.getCategories())
}
// Get products with pagination and filtering
get("/api/products") {
val page = call.request.queryParameters["page"]?.toIntOrNull() ?: 0
val pageSize = call.request.queryParameters["pageSize"]?.toIntOrNull() ?: 10
val category = call.request.queryParameters["category"]
val search = call.request.queryParameters["search"]
val result = if (search != null) {
val items = productService.searchProducts(search)
PaginatedResponse(
items = items,
total = items.size,
page = page,
pageSize = pageSize,
totalPages = (items.size + pageSize - 1) / pageSize
)
} else {
productService.getProducts(page, pageSize, category)
}
call.respond(result)
}
// Get single product
get("/api/products/{id}") {
val id = call.parameters["id"]?.toIntOrNull()
if (id == null) {
call.respond(HttpStatusCode.BadRequest, ApiError(
error = "Invalid product ID",
status = 400,
timestamp = java.time.Instant.now().toString()
))
return@get
}
val product = productService.getProduct(id)
if (product != null) {
call.respond(product)
} else {
call.respond(HttpStatusCode.NotFound, ApiError(
error = "Product not found",
status = 404,
timestamp = java.time.Instant.now().toString(),
path = "/api/products/$id"
))
}
}
// Create product
post("/api/products") {
try {
val request = call.receive<CreateProductRequest>()
// Validation
when {
request.name.isBlank() -> call.respond(HttpStatusCode.BadRequest, ApiError(
error = "Product name is required",
status = 400,
timestamp = java.time.Instant.now().toString()
))
request.price <= 0 -> call.respond(HttpStatusCode.BadRequest, ApiError(
error = "Price must be greater than 0",
status = 400,
timestamp = java.time.Instant.now().toString()
))
request.stock < 0 -> call.respond(HttpStatusCode.BadRequest, ApiError(
error = "Stock cannot be negative",
status = 400,
timestamp = java.time.Instant.now().toString()
))
else -> {
val product = productService.createProduct(request)
call.respond(HttpStatusCode.Created, product)
}
}
} catch (e: Exception) {
call.respond(HttpStatusCode.BadRequest, ApiError(
error = "Invalid request: ${e.message}",
status = 400,
timestamp = java.time.Instant.now().toString()
))
}
}
// Update product
put("/api/products/{id}") {
val id = call.parameters["id"]?.toIntOrNull()
if (id == null) {
call.respond(HttpStatusCode.BadRequest, ApiError(
error = "Invalid product ID",
status = 400,
timestamp = java.time.Instant.now().toString()
))
return@put
}
try {
val request = call.receive<UpdateProductRequest>()
val product = productService.updateProduct(id, request)
if (product != null) {
call.respond(product)
} else {
call.respond(HttpStatusCode.NotFound, ApiError(
error = "Product not found",
status = 404,
timestamp = java.time.Instant.now().toString(),
path = "/api/products/$id"
))
}
} catch (e: Exception) {
call.respond(HttpStatusCode.BadRequest, ApiError(
error = "Invalid request: ${e.message}",
status = 400,
timestamp = java.time.Instant.now().toString()
))
}
}
// Delete product
delete("/api/products/{id}") {
val id = call.parameters["id"]?.toIntOrNull()
if (id == null) {
call.respond(HttpStatusCode.BadRequest, ApiError(
error = "Invalid product ID",
status = 400,
timestamp = java.time.Instant.now().toString()
))
return@delete
}
if (productService.deleteProduct(id)) {
call.respond(HttpStatusCode.NoContent)
} else {
call.respond(HttpStatusCode.NotFound, ApiError(
error = "Product not found",
status = 404,
timestamp = java.time.Instant.now().toString(),
path = "/api/products/$id"
))
}
}
}
}.start(wait = true)
}
The product catalog API provides CRUD operations. Pagination handles large datasets. Filtering by category and search by query. Validation ensures data quality. Consistent error responses provide helpful feedback. Stats endpoints provide summary information.
Career Readiness
20.1 Portfolio Development
Building a professional portfolio to showcase Kotlin skills.
- Project selection – Showcase diverse skills.
- Code quality – Clean, well-documented code.
- README files – Clear project documentation.
- GitHub presence – Organized repositories.
- Live demos – Deploy applications online.
Code Example:
# Bird Tracker - Kotlin Multiplatform Project
[](https://kotlinlang.org/)
[](LICENSE)
## 📝 Description
Bird Tracker is a Kotlin Multiplatform application for tracking bird sightings across different platforms. It demonstrates modern Kotlin development with clean architecture, coroutines, and cross-platform support.
## 🚀 Features
- 📱 Android, iOS, and Desktop support
- 🔍 Search and filter bird sightings- 📊 Statistics and visualization
- 💾 Offline data storage
- 🌍 Internationalization support
- 🔒 Secure authentication
- 📸 Photo upload capability
## 🏗️ Architecture
## 🛠️ Tech Stack
- **Language**: Kotlin 1.9
- **Multiplatform**: Kotlin Multiplatform
- **UI**: Compose Multiplatform
- **Database**: SQLDelight (Shared)
- **Networking**: Ktor Client
- **Serialization**: kotlinx.serialization
- **Concurrency**: Coroutines
- **Testing**: Kotlin Test, JUnit
- **CI/CD**: GitHub Actions
## 🚀 Getting Started
### Prerequisites
- JDK 17+
- Android Studio / IntelliJ IDEA
- Xcode (for iOS)
- Kotlin 1.9+
### Installation
```bash
# Clone the repository
git clone https://github.com/yourusername/bird-tracker.git
# Navigate to project directory
cd bird-tracker
# Build the project
./gradlew build
# Run tests
./gradlew test
# Run on Android
./gradlew :androidApp:installDebug
# Run on Desktop
./gradlew :desktopApp:run
Documentation
Testing
# Run all tests
./gradlew test
# Run specific platform tests
./gradlew :androidApp:test
./gradlew :shared:iosTest
Deployment
# Build Android APK
./gradlew :androidApp:assembleRelease
# Build Desktop application
./gradlew :desktopApp:packageDistributionForCurrentOS
Contributing
- Fork the repository
- Create your feature branch (
git checkout -b feature/AmazingFeature) - Commit changes (
git commit -m 'Add AmazingFeature') - Push to branch (
git push origin feature/AmazingFeature) - Open a Pull Request
License
This project is licensed under the MIT License – see the LICENSE file for details.
Author
Your Name
- GitHub: @yourusername
- LinkedIn: Your LinkedIn
- Portfolio: yourportfolio.com
Acknowledgments
- JetBrains for Kotlin
- Android Open Source Project
- Kotlin Multiplatform community
**Portfolio Projects:**
```kotlin
// 1. Weather App with Kotlin Multiplatform
// 2. E-commerce Backend with Ktor
// 3. Android Banking App with Compose
// 4. Desktop CRM with Compose Desktop
// 5. Game Development with LibGDX
// 6. Data Science with Kotlin Notebook
// 7. Discord Bot with Kord
// 8. GraphQL API with KGraphQL
// 9. Web Scraper with Jsoup
// 10. Cryptocurrency Tracker with Compose
A professional portfolio demonstrates practical Kotlin skills. README files document projects clearly. Projects should showcase different technologies and patterns. Quality code with tests and documentation stands out. Live demos provide interactive demonstrations.
20.2 Interview Preparation
Common Kotlin interview questions and answers.
- Core concepts – Kotlin features and syntax.
- Functional programming – Lambdas, extensions.
- Concurrency – Coroutines, flows.
- Architecture – Design patterns, Clean Architecture.
- Problem solving – Algorithm and data structure questions.
Code Example:
// ---- Q1: Explain val vs var ----
// val = read-only reference, cannot be reassigned
// var = mutable reference, can be reassigned
fun q1() {
val name = "Sparrow"
// name = "Eagle" // ERROR: val cannot be reassigned
var count = 10
count = 15 // OK: var can be reassigned
}
// ---- Q2: What are data classes? ----
// Automatically provide equals(), hashCode(), toString(), copy(), componentN()
data class Bird(val species: String, val count: Int)
fun q2() {
val bird1 = Bird("Sparrow", 10)
val bird2 = bird1.copy(count = 15)
println(bird1) // Bird(species=Sparrow, count=10)
println(bird2) // Bird(species=Sparrow, count=15)
}
// ---- Q3: Explain coroutines ----
// Lightweight concurrency with suspend functions
import kotlinx.coroutines.*
fun q3() = runBlocking {
launch {
delay(1000)
println("World!")
}
println("Hello") // Hello (then after 1s) World!
}
// ---- Q4: What are extension functions? ----
// Add new functions to existing classes
fun String.isBird(): Boolean {
return this in listOf("Eagle", "Sparrow", "Hawk")
}
fun q4() {
println("Eagle".isBird()) // true
println("Cat".isBird()) // false
}
// ---- Q5: Explain sealed classes ----
// Restricted class hierarchies, all subclasses known at compile time
sealed class Result {
data class Success(val data: String) : Result()
data class Error(val message: String) : Result()
object Loading : Result()
}
fun q5(result: Result) {
when (result) {
is Result.Success -> println("Data: ${result.data}")
is Result.Error -> println("Error: ${result.message}")
Result.Loading -> println("Loading...")
}
}
// ---- Q6: What are higher-order functions? ----
// Functions that take functions as parameters or return functions
fun q6() {
val numbers = listOf(1, 2, 3, 4, 5)
val doubled = numbers.map { it * 2 }
val evens = numbers.filter { it % 2 == 0 }
println(doubled) // [2, 4, 6, 8, 10]
println(evens) // [2, 4]
}
// ---- Q7: Explain Flow ----
// Cold asynchronous stream
import kotlinx.coroutines.flow.*
fun q7() = runBlocking {
flow {
for (i in 1..3) {
emit(i)
delay(100)
}
}.map { it * it }
.collect { println(it) } // 1 4 9
}
// ---- Q8: What are scope functions? ----
// let, apply, run, with, also
fun q8() {
val bird = Bird("Sparrow", 10)
// let - execute on non-null
bird.let { println(it.species) } // Sparrow
// apply - configure object
val updatedBird = bird.apply {
// can access properties
}
// with - execute with context
with(bird) {
println("$species: $count") // Sparrow: 10
}
// run - combine let and with
val result = bird.run { "$species: $count" }
println(result) // Sparrow: 10
}
// ---- Q9: Explain lazy initialization ----
// Defer initialization until first access
fun q9() {
val heavyData: String by lazy {
println("Computing...")
"Heavy data loaded"
}
println("Before access")
println(heavyData) // Computing... Heavy data loaded
println(heavyData) // Heavy data loaded (cached)
}
// ---- Q10: What is the difference between List and Array? ----
// List is immutable, Array is mutable
fun q10() {
val list = listOf(1, 2, 3) // Immutable
// list[0] = 10 // ERROR: Cannot modify
val array = arrayOf(1, 2, 3) // Mutable
array[0] = 10 // OK
// Preferred: List for safety, Array for performance
}
// ---- Algorithm Question: Find duplicates ----
fun findDuplicates(birds: List<String>): Set<String> {
return birds.groupBy { it }
.filter { it.value.size > 1 }
.keys
}
fun testFindDuplicates() {
val birds = listOf("Eagle", "Sparrow", "Eagle", "Hawk", "Sparrow", "Robin")
val duplicates = findDuplicates(birds)
println(duplicates) // [Eagle, Sparrow]
}
// ---- Algorithm Question: Two Sum ----
fun twoSum(numbers: IntArray, target: Int): Pair<Int, Int>? {
val map = mutableMapOf<Int, Int>()
for (i in numbers.indices) {
val complement = target - numbers[i]
if (map.containsKey(complement)) {
return map[complement] to i
}
map[numbers[i]] = i
}
return null
}
// ---- Design Question: Singleton ----
object BirdConfig {
val version = "1.0"
val maxBirds = 100
fun display() {
println("Version: $version, Max: $maxBirds")
}
}
Interview questions test knowledge of Kotlin features and concepts. Functional programming patterns are common. Coroutines and flows demonstrate concurrency understanding. Architecture patterns show design skills. Algorithm questions test problem-solving abilities.
Final Advice
Kotlin feels modern and refreshing because it combines the best of object-oriented and functional programming while eliminating many of Java’s pain points. Its null safety prevents entire categories of bugs, and its concise syntax makes code more readable and maintainable.
Begin with the first stage today by running “Hello, World!” yourself on your own computer using the terminal.Then break it on purpose—remove a semicolon, misspell println, forget a closing brace—and read the resulting error message slowly and carefully instead of panicking.
That single habit, repeated consistently over weeks and months, is genuinely how every strong Kotlin programmer built their foundation.
Use the AI prompts provided for each concept. They’re designed to give you code examples, clear explanations, and practical exercises. Copy them into your favorite AI assistant and work through the examples yourself. Write the code, run it, modify it, break it, and fix it.
Good luck, and welcome to Kotlin. The journey is challenging, but the destination is worth it.