Kotlin
App DevelopmentOverview
Modern, concise JVM language by JetBrains. Primary language for Android development. Also used for server-side, multiplatform, and desktop apps.
Resources
Popular learning and reference links:
Installation & Getting Started
Install the Kotlin compiler standalone, or get it bundled with IntelliJ IDEA or Android Studio. Kotlin runs on the JVM, so a JDK is required.
# macOS (Homebrew)
brew install kotlin
# SDKMAN (recommended for JVM languages)
curl -s https://get.sdkman.io | bash
sdk install kotlin
sdk install java 21-tem # Also install a JDK
# Linux (snap)
sudo snap install kotlin --classic
# Verify
kotlin -version
kotlinc -version
java -version # JDK required
# REPL — Kotlin has a built-in REPL
kotlinc # Start REPL
# >>> val x = 42
# >>> println(x)
# Kotlin Playground — try in the browser
# https://play.kotlinlang.org/
# Run a script (.kts file)
kotlin script.main.kts
# Compile and run
kotlinc hello.kt -include-runtime -d hello.jar
java -jar hello.jar
# Run directly (Kotlin 1.9+)
kotlin hello.kt
# Kotlin Notebooks — in IntelliJ IDEA
# Similar to Jupyter, for interactive exploration Project Scaffolding
Use IntelliJ IDEA, Android Studio, or the Kotlin project wizard.
# Using Gradle init
gradle init --type kotlin-application
# Project structure:
# app/
# build.gradle.kts
# src/
# main/kotlin/App.kt
# test/kotlin/AppTest.kt
# Run the project
./gradlew run
# Build
./gradlew build
# Android project: use Android Studio wizard
# Multiplatform: use start.kotlinlang.org
# Kotlin scripting
echo 'println("Hello!")' > script.main.kts
kotlin script.main.kts Package Management
Uses Gradle (Kotlin DSL or Groovy) to manage dependencies from Maven Central.
// build.gradle.kts
dependencies {
implementation("com.squareup.okhttp3:okhttp:4.12.0")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.0")
testImplementation("junit:junit:4.13.2")
}
// Version catalogs (libs.versions.toml)
// [versions]
// okhttp = "4.12.0"
// [libraries]
// okhttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" }
# Sync dependencies
./gradlew build
# Show dependency tree
./gradlew dependencies Tooling & Formatter/Linter
Kotlin uses ktlint for linting/formatting and detekt for static analysis. IntelliJ IDEA provides built-in formatting.
# ktlint — linter + formatter (follows Kotlin style guide)
brew install ktlint
ktlint # Lint
ktlint --format # Auto-format
# detekt — static analysis
# Add to build.gradle.kts:
# plugins { id("io.gitlab.arturbosch.detekt") version "1.23.0" }
./gradlew detekt
# ktfmt — Google's Kotlin formatter
# Alternative to ktlint with Google/Meta style
# detekt.yml
complexity:
LongMethod:
threshold: 30
ComplexCondition:
threshold: 4
style:
MaxLineLength:
maxLineLength: 120
WildcardImport:
active: true
// build.gradle.kts — Spotless plugin (multi-formatter)
plugins {
id("com.diffplug.spotless") version "6.25.0"
}
spotless {
kotlin {
ktlint()
target("src/**/*.kt")
}
}
// Run: ./gradlew spotlessApply Build & Compile Model
Compiled to JVM bytecode (default), or native/JS. Kotlin targets multiple backends via the Kotlin compiler.
# Compile to JVM bytecode
kotlinc hello.kt -include-runtime -d hello.jar
java -jar hello.jar
# Build with Gradle
./gradlew build
./gradlew run
# Kotlin/Native — compile to native binary (no JVM)
# Uses Kotlin/Native compiler (LLVM-based)
# Output: native executable
# Kotlin/JS — compile to JavaScript
# Output: .js files for browser or Node.js
# Kotlin Multiplatform — shared code across targets
./gradlew :shared:build
Execution model:
- JVM: Source → Kotlin IR → JVM bytecode (.class/.jar) → JIT (HotSpot)
- Native: Source → Kotlin IR → LLVM IR → Machine code
- JS: Source → Kotlin IR → JavaScript
- WASM: Source → Kotlin IR → WebAssembly (experimental)
- JVM target benefits from HotSpot JIT, tiered compilation, and GC
- Gradle build can be slow — use build cache and configuration cache
Libraries & Frameworks
Kotlin has access to the entire Java ecosystem plus Kotlin-specific libraries. Strong in Android, server, and multiplatform.
Android - Jetpack Compose, Android KTX, Hilt (DI), Room, Navigation, Lifecycle
Server - Ktor, Spring Boot (Kotlin support), Micronaut, http4k, Javalin
Multiplatform - Kotlin Multiplatform (KMP), Compose Multiplatform, KMM
Coroutines - kotlinx.coroutines, Flow, Channels
Serialization - kotlinx.serialization, Moshi, Gson (Java)
Networking - Ktor Client, OkHttp, Retrofit
Databases - Exposed, SQLDelight, Room (Android), ktorm
DI - Koin, Hilt (Android), Kodein
Testing - JUnit 5, Kotest, MockK, Turbine (Flow testing)
Build - Gradle Kotlin DSL, Amper (experimental)
Desktop - Compose for Desktop, TornadoFX
Functional - Arrow (FP library), kotlin-result
Testing
JUnit 5 is the standard. Kotest is a Kotlin-native alternative with expressive DSLs. MockK is the preferred mocking library.
// JUnit 5 with Kotlin
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.Assertions.*
class MathTest {
@Test
fun `adds two numbers`() {
assertEquals(3, 1 + 2)
}
@Test
fun `list contains element`() {
val list = listOf(1, 2, 3)
assertTrue(2 in list)
assertEquals(3, list.size)
}
}
// Parameterized tests
import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.CsvSource
class ParameterizedMathTest {
@ParameterizedTest
@CsvSource("1,2,3", "0,0,0", "-1,1,0")
fun `adds correctly`(a: Int, b: Int, expected: Int) {
assertEquals(expected, a + b)
}
}
// Kotest — Kotlin-native testing framework
import io.kotest.core.spec.style.StringSpec
import io.kotest.matchers.shouldBe
import io.kotest.matchers.collections.shouldContain
class MathSpec : StringSpec({
"addition works" {
(1 + 2) shouldBe 3
}
"list contains element" {
listOf(1, 2, 3) shouldContain 2
}
})
# Run tests
./gradlew test # All tests
./gradlew test --tests "MathTest" # Specific class
./gradlew test --tests "*adds*" # Pattern match Debugging
IntelliJ IDEA and Android Studio have excellent debuggers. Standard JVM debugging tools also apply.
// Print debugging
println("value: $value")
println("struct: $myObject")
// Logging (SLF4J / Logback — JVM standard)
import org.slf4j.LoggerFactory
val logger = LoggerFactory.getLogger(MyClass::class.java)
logger.info("Processing item {}", item.id)
logger.error("Failed", exception)
logger.debug("Details: {}", data)
// kotlin-logging — idiomatic Kotlin wrapper
import io.github.oshai.kotlinlogging.KotlinLogging
val logger = KotlinLogging.logger {}
logger.info { "Processing ${item.id}" } // Lazy evaluation
// require / check — preconditions
require(age >= 0) { "Age must be non-negative: $age" }
check(isInitialized) { "Must be initialized first" }
// Throws IllegalArgumentException / IllegalStateException
# IntelliJ IDEA / Android Studio
# Shift+F9: Start debugging
# Click gutter for breakpoints
# Features: conditional breakpoints, evaluate expression,
# watches, coroutine debugger, inline variable values
# Coroutine debugging
# IntelliJ shows coroutine stack traces
# -Dkotlinx.coroutines.debug for enhanced output
# Android debugging
# Android Studio > Debug tab
# Layout Inspector: view hierarchy
# Database Inspector: live SQLite queries
# Network Inspector: HTTP traffic
# JVM debugging (command-line)
java -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005 -jar app.jar
# Attach IntelliJ: Run > Attach to Process
# jconsole / VisualVM — JVM monitoring
jconsole # Memory, threads, MBeans Variables
Use val for immutable and var for mutable. Type inference is the default.
// Immutable (val)
val name = "Alice"
val age = 30
// Mutable (var)
var count = 0
count += 1
// Explicit type annotation
val pi: Double = 3.14159
var label: String = "hello"
// Nullable types
var maybeValue: String? = null
maybeValue = "exists"
// Constants (compile-time)
const val MAX_SIZE = 100
// Destructuring
val (x, y) = Pair(1, 2)
val (first, second) = listOf("a", "b") Types
Statically typed with null safety, data classes, sealed classes, and generics.
// Primitives
val s: String = "hello"
val n: Int = 42
val f: Double = 3.14
val b: Boolean = true
// Collections
val nums: List<Int> = listOf(1, 2, 3)
val mutableNums = mutableListOf(1, 2, 3)
val ages: Map<String, Int> = mapOf("Alice" to 30)
val unique: Set<Int> = setOf(1, 2, 3)
// Data classes
data class User(val name: String, val age: Int)
// Sealed classes (restricted hierarchy)
sealed class Shape {
data class Circle(val radius: Double) : Shape()
data class Rect(val w: Double, val h: Double) : Shape()
}
// Enums
enum class Color { RED, GREEN, BLUE }
// Nullable types
val maybe: Int? = null
val sure: Int = 42 Data Structures
Kotlin distinguishes mutable and immutable collection interfaces: List/MutableList, Map/MutableMap, Set/MutableSet.
// List — ordered (immutable by default)
val list = listOf(1, 2, 3)
val mutable = mutableListOf(1, 2, 3)
mutable.add(4)
mutable.removeAt(0)
list[0] // 1
list.size // 3
list.contains(2) // true
// Map — key-value
val map = mapOf("alice" to 95, "bob" to 87)
val mutableMap = mutableMapOf("alice" to 95)
mutableMap["charlie"] = 72
map["alice"] // 95 (nullable)
map.getOrDefault("missing", 0)
// Set — unique values
val set = setOf(1, 2, 3)
val mutableSet = mutableSetOf(1, 2, 3)
mutableSet.add(4)
set.contains(2) // true
set intersect setOf(2, 3, 5) // [2, 3]
// Pair and Triple
val pair = "age" to 30
val (key, value) = pair
// Collection operations (rich stdlib)
val doubled = list.map { it * 2 }
val evens = list.filter { it % 2 == 0 }
val sum = list.reduce { acc, x -> acc + x }
val grouped = items.groupBy { it.category }
val chunked = list.chunked(2)
val windowed = list.windowed(3)
// Sequences — lazy evaluation
val result = (1..1_000_000).asSequence()
.filter { it % 2 == 0 }
.map { it * it }
.take(10)
.toList()
// Destructuring
data class Point(val x: Int, val y: Int)
val (x, y) = Point(10, 20)
// Array (JVM array, fixed size)
val arr = arrayOf(1, 2, 3)
val intArr = intArrayOf(1, 2, 3) // Primitive array Functions
Functions support default arguments, named parameters, and lambdas.
// Basic function
fun greet(name: String): String {
return "Hello, $name!"
}
// Single-expression function
fun add(a: Int, b: Int) = a + b
// Default and named parameters
fun power(base: Int, exp: Int = 2): Int {
return base.toDouble().pow(exp).toInt()
}
power(base = 3, exp = 4)
// Lambda
val double = { x: Int -> x * 2 }
val sum = listOf(1, 2, 3).fold(0) { acc, x -> acc + x }
// Extension function
fun String.addExclamation() = "$this!"
"hello".addExclamation() // "hello!"
// Generic function
fun <T> first(items: List<T>): T = items.first()
// Higher-order function
fun apply(x: Int, fn: (Int) -> Int): Int = fn(x) Conditionals
if is an expression. when replaces switch with pattern matching.
// If expression
val label = if (x > 0) "positive" else "non-positive"
// If / else
if (x > 0) {
println("positive")
} else if (x == 0) {
println("zero")
} else {
println("negative")
}
// When expression (like switch)
val result = when (shape) {
is Shape.Circle -> Math.PI * shape.radius * shape.radius
is Shape.Rect -> shape.w * shape.h
}
// When with conditions
val category = when {
age <= 12 -> "child"
age <= 17 -> "teen"
age <= 64 -> "adult"
else -> "senior"
}
// Null safety
val len = maybeStr?.length ?: 0
val city = user?.address?.city
// Safe cast
val str = value as? String Loops
for loops with ranges, while, and rich collection functions.
// For with range
for (i in 0 until 5) {
println(i)
}
// For over collection
for (item in listOf("a", "b", "c")) {
println(item)
}
// With index
for ((i, item) in listOf("a", "b").withIndex()) {
println("$i: $item")
}
// While
var n = 0
while (n < 3) {
n++
}
// Do-while
do {
n--
} while (n > 0)
// Functional
val squares = (0 until 10).map { it * it }
val evens = listOf(1, 2, 3, 4).filter { it % 2 == 0 }
val sum = listOf(1, 2, 3).reduce { acc, x -> acc + x }
// Repeat
repeat(3) { println("hello") } Generics & Type System
Reified generics (inline functions), declaration-site variance (in/out), and type projections. Null safety built into the type system.
// Generic function
fun <T> identity(value: T): T = value
// Constraints (upper bounds)
fun <T : Comparable<T>> max(a: T, b: T): T =
if (a > b) a else b
// Multiple bounds
fun <T> process(item: T) where T : Serializable, T : Comparable<T> {
// T must implement both
}
// Generic class
class Stack<T> {
private val items = mutableListOf<T>()
fun push(item: T) = items.add(item)
fun pop(): T = items.removeLast()
}
// Reified type parameters (inline functions only)
inline fun <reified T> isType(value: Any): Boolean = value is T
isType<String>("hello") // true — type available at runtime!
// Variance
// out = covariant (producer)
interface Source<out T> { fun next(): T }
// in = contravariant (consumer)
interface Sink<in T> { fun accept(item: T) }
// Use-site variance (type projections)
fun copy(from: Array<out Any>, to: Array<in Any>) {
for (i in from.indices) to[i] = from[i]
}
// Null safety — built into the type system
val name: String = "Alice" // Non-null
val nullable: String? = null // Nullable
val length = nullable?.length // Safe call → Int?
val forced = nullable!!.length // Throws if null
val default = nullable ?: "Unknown" // Elvis operator
// Smart casts
fun describe(obj: Any): String = when (obj) {
is String -> "String of length ${obj.length}"
is Int -> "Int: $obj"
else -> "Unknown"
} Inheritance & Composition
Single class inheritance (classes are final by default). Interfaces with default methods, sealed hierarchies, and delegation for composition.
// Classes are final by default — use `open`
open class Animal(val name: String) {
open fun speak(): String = "$name makes a sound"
}
class Dog(name: String) : Animal(name) {
override fun speak(): String = "$name barks"
}
// Abstract classes
abstract class Shape {
abstract fun area(): Double
fun describe() = "Area: ${area()}"
}
// Interfaces (with default methods)
interface Serializable {
fun toJson(): String
}
interface Printable {
fun printOut() { println(toJson()) }
fun toJson(): String
}
class Report : Serializable, Printable {
override fun toJson() = "{}"
}
// Sealed classes (restricted hierarchy)
sealed class Result {
data class Success(val data: String) : Result()
data class Error(val msg: String) : Result()
}
// Delegation (composition pattern)
interface Closer { fun close() }
class FileCloser : Closer { override fun close() {} }
class Resource(closer: Closer) : Closer by closer
// Resource delegates close() to closer Functional Patterns
First-class functions, extension functions, scope functions, and rich collection APIs. Kotlin blends OOP and FP naturally.
// Lambdas
val double = { x: Int -> x * 2 }
val add: (Int, Int) -> Int = { a, b -> a + b }
// Collection methods
val nums = listOf(1, 2, 3, 4, 5)
val squared = nums.map { it * it }
val evens = nums.filter { it % 2 == 0 }
val sum = nums.reduce { acc, x -> acc + x }
// Chaining
val result = (0 until 100)
.filter { it % 2 == 0 }
.map { it * it }
.take(10)
.sum()
// Scope functions
val user = User("Alice", 30).apply {
println(name) // configure
}
val len = name?.let { it.length } ?: 0
// Sealed classes (algebraic data types)
sealed class Result<out T> {
data class Ok<T>(val value: T) : Result<T>()
data class Err(val msg: String) : Result<Nothing>()
}
fun <T, U> Result<T>.map(fn: (T) -> U): Result<U> =
when (this) {
is Result.Ok -> Result.Ok(fn(value))
is Result.Err -> this
}
// Higher-order + extension functions
fun <T> T.applyFn(fn: (T) -> T): T = fn(this)
5.applyFn { it * 2 } // 10 Concurrency
Coroutines — lightweight, structured concurrency built on top of the JVM. First-class support via kotlinx.coroutines.
import kotlinx.coroutines.*
// Launch a coroutine
fun main() = runBlocking {
launch {
delay(1000)
println("World!")
}
println("Hello,")
}
// async/await — concurrent decomposition
suspend fun fetchBoth(): Pair<User, Posts> = coroutineScope {
val user = async { fetchUser() }
val posts = async { fetchPosts() }
Pair(user.await(), posts.await())
}
// Structured concurrency — parent waits for children
coroutineScope {
launch { doTaskA() }
launch { doTaskB() }
} // Both complete before continuing
// Flow — reactive streams (cold)
import kotlinx.coroutines.flow.*
fun numbers(): Flow<Int> = flow {
for (i in 1..5) {
delay(100)
emit(i)
}
}
numbers()
.filter { it % 2 == 0 }
.map { it * 10 }
.collect { println(it) }
// Channels — hot communication between coroutines
val channel = Channel<Int>()
launch { channel.send(42) }
val value = channel.receive()
// Dispatchers — control thread pool
withContext(Dispatchers.IO) { readFile() }
withContext(Dispatchers.Default) { compute() }
withContext(Dispatchers.Main) { updateUI() } Modules & Imports
Kotlin uses packages (like Java) and Gradle modules for project structure. Files don’t need to match directory structure, but conventionally do. Visibility modifiers control access.
// Package declaration
package com.myapp.models
// Imports
import java.util.Date
import kotlinx.coroutines.launch
import com.myapp.utils.Helper
// Import with alias
import com.myapp.models.User as AppUser
// Wildcard import
import kotlin.math.*
// Top-level declarations (no class needed)
fun helper() = "I'm a top-level function"
val PI = 3.14159
// Visibility modifiers
public class PublicApi // visible everywhere (default)
internal class ModuleOnly // same Gradle module
private class FilePrivate // same file
// protected — subclasses only (class members)
// Gradle multi-module structure:
// app/
// build.gradle.kts
// core/
// build.gradle.kts
// settings.gradle.kts:
// include(":app", ":core")
// Depend on another module:
// dependencies {
// implementation(project(":core"))
// }
// Kotlin Multiplatform (expect/actual)
expect fun platformName(): String // common
actual fun platformName() = "JVM" // jvm Error Handling
Uses try/catch/finally. All exceptions are unchecked. Result type for functional error handling.
// Try / catch
try {
val n = "abc".toInt()
} catch (e: NumberFormatException) {
println("Parse error: ${e.message}")
} catch (e: Exception) {
println("Error: ${e.message}")
} finally {
println("always runs")
}
// Try as expression
val number = try {
input.toInt()
} catch (e: NumberFormatException) {
-1
}
// Throwing
fun divide(a: Int, b: Int): Int {
require(b != 0) { "Cannot divide by zero" }
return a / b
}
// Result type
fun safeParse(s: String): Result<Int> = runCatching {
s.toInt()
}
val result = safeParse("42")
.getOrDefault(0)
// Custom exception
class AppException(msg: String, val code: Int)
: Exception(msg) Memory Management
JVM garbage collector — generational GC with multiple collector options (G1, ZGC, Shenandoah). Kotlin/Native uses its own GC.
// Memory is automatically managed by the JVM GC
val list = mutableListOf(1, 2, 3)
// GC collects when objects are unreachable
// Closeable — deterministic cleanup (like IDisposable)
import java.io.Closeable
class ManagedResource : Closeable {
override fun close() { /* release resources */ }
}
// use — auto-close (like try-with-resources)
FileReader("data.txt").use { reader ->
reader.readText()
} // reader.close() called automatically
// BufferedReader
File("data.txt").bufferedReader().use { it.readText() }
// Value classes — avoid boxing overhead
@JvmInline
value class UserId(val id: Long)
// No heap allocation at runtime (inlined)
// Sequences — lazy, avoids intermediate collections
val result = (1..1_000_000).asSequence()
.filter { it % 2 == 0 }
.map { it * it }
.take(10)
.toList() // Only one list allocated
// WeakReference
import java.lang.ref.WeakReference
val weak = WeakReference(heavyObject)
weak.get() // Returns object or null if GC'd
// JVM GC tuning
// -XX:+UseG1GC (default since JDK 9)
// -XX:+UseZGC (low-latency, JDK 15+)
// -Xmx512m (max heap size)
// -XX:+PrintGCDetails (GC logging)
// Kotlin/Native — uses tracing GC (since 1.7.20)
// Previously used reference counting with cycle detection Performance Profiling
JVM profiling tools: JMH for benchmarks, VisualVM/JFR for runtime profiling, Android Profiler for mobile.
// JMH (Java Microbenchmark Harness) via kotlinx-benchmark
// build.gradle.kts:
// plugins { id("org.jetbrains.kotlinx.benchmark") version "0.4.10" }
import org.openjdk.jmh.annotations.*
@State(Scope.Benchmark)
class SortBenchmark {
private val data = (0 until 1000).shuffled()
@Benchmark
fun sortList(): List<Int> = data.sorted()
@Benchmark
fun sortArray(): IntArray = data.toIntArray().also { it.sort() }
}
# JFR (Java Flight Recorder) — low-overhead profiling
java -XX:StartFlightRecording=duration=60s,filename=rec.jfr -jar app.jar
# Analyze with JDK Mission Control (jmc)
# VisualVM — GUI profiler
visualvm # CPU, memory, threads, heap dump
# async-profiler — sampling profiler (low overhead)
# https://github.com/async-profiler/async-profiler
./profiler.sh -d 30 -f flame.html <pid>
# IntelliJ IDEA Profiler
# Run > Profile (built-in CPU + memory profiler)
# Generates flame charts and call trees
# Android Profiler (Android Studio)
# CPU, Memory, Network, Energy profiling
# System Trace for detailed thread analysis
# Kotlin-specific: measure time
import kotlin.time.measureTime
val duration = measureTime {
doExpensiveWork()
}
println("Took $duration")
# Gradle build profiling
./gradlew build --scan # Build scan
./gradlew build --profile # HTML report Interop
100% Java interop — call any Java library from Kotlin and vice versa. Kotlin/Native has C interop. KMP shares code across platforms.
// Java interop — seamless, no wrappers needed
import java.util.ArrayList
import java.io.File
val list = ArrayList<String>() // Java class
list.add("hello")
val file = File("data.txt")
val content = file.readText() // Kotlin extension on Java class
// Kotlin is fully callable from Java
// @JvmStatic, @JvmField, @JvmOverloads for better Java API
class MyClass {
companion object {
@JvmStatic fun create(): MyClass = MyClass()
}
@JvmOverloads
fun greet(name: String, greeting: String = "Hello") =
"$greeting, $name!"
}
// Kotlin/Native — C interop
// Define in .def file:
// headers = mylib.h
// Build: generates Kotlin bindings from C headers
import mylib.*
val result = my_c_function(42)
// Kotlin Multiplatform — shared code
// expect/actual pattern
expect fun platformName(): String
// In JVM module:
actual fun platformName(): String = "JVM"
// In iOS module:
actual fun platformName(): String = "iOS"
// Platform-specific calls
import platform.Foundation.NSDate // iOS/macOS
import kotlinx.cinterop.* // C interop
// Process — call system commands (JVM)
val process = Runtime.getRuntime().exec("ls -la")
val output = process.inputStream.bufferedReader().readText() Packaging & Distribution
Libraries to Maven Central. Android apps to Google Play. Kotlin/Native produces standalone binaries.
// build.gradle.kts — library publishing
plugins {
`maven-publish`
signing
}
publishing {
publications {
create<MavenPublication>("release") {
groupId = "com.example"
artifactId = "my-lib"
version = "1.0.0"
from(components["kotlin"])
}
}
repositories {
maven {
url = uri("https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/")
credentials {
username = project.findProperty("ossrhUsername") as String
password = project.findProperty("ossrhPassword") as String
}
}
}
}
# Publish to Maven Central
./gradlew publish
# Fat JAR (all dependencies included)
# plugins { id("com.github.johnrengelman.shadow") }
./gradlew shadowJar
java -jar build/libs/app-all.jar
# Kotlin/Native binary
./gradlew linkReleaseExecutableNative
# Output: build/bin/native/releaseExecutable/app.kexe
# Android — Google Play
# Build > Generate Signed Bundle/APK in Android Studio
# Upload .aab to Play Console
# Docker (JVM)
FROM eclipse-temurin:21-jre-alpine
COPY build/libs/app-all.jar /app.jar
ENTRYPOINT ["java", "-jar", "/app.jar"]
# Kotlin scripting distribution
# .main.kts files can be run directly: kotlin script.main.kts