standards-kotlin — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited standards-kotlin (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
val over var, immutable collections| Element | Convention | Example |
|---|---|---|
| Classes | PascalCase | UserService, OrderRepository |
| Interfaces | PascalCase | UserRepository, PaymentProcessor |
| Functions | camelCase | getUserById, calculateTotal |
| Properties | camelCase | firstName, totalAmount |
| Constants | UPPER_SNAKE_CASE | MAX_RETRY_COUNT, DEFAULT_TIMEOUT |
| Packages | lowercase.dot.separated | com.example.service, com.example.repository |
| Files | PascalCase.kt | UserService.kt, OrderRepository.kt |
| Test Classes | ClassNameTest | UserServiceTest, OrderRepositoryTest |
| Test Functions | backtick names | ` should return user when id exists ` |
myproject/
├── build.gradle.kts
├── settings.gradle.kts
├── gradle.properties
├── src/
│ ├── main/
│ │ ├── kotlin/
│ │ │ └── com/example/myapp/
│ │ │ ├── Application.kt # Main entry point
│ │ │ ├── config/
│ │ │ │ └── AppConfig.kt # Configuration
│ │ │ ├── domain/
│ │ │ │ └── User.kt # Domain models
│ │ │ ├── repository/
│ │ │ │ └── UserRepository.kt # Data access
│ │ │ ├── service/
│ │ │ │ └── UserService.kt # Business logic
│ │ │ └── api/
│ │ │ └── UserController.kt # REST endpoints
│ │ └── resources/
│ │ ├── application.conf
│ │ └── logback.xml
│ └── test/
│ ├── kotlin/
│ │ └── com/example/myapp/
│ │ ├── service/
│ │ │ └── UserServiceTest.kt
│ │ └── repository/
│ │ └── UserRepositoryTest.kt
│ └── resources/
│ └── application-test.conf
└── README.mdmyproject/
├── pom.xml
├── src/
│ ├── main/
│ │ └── kotlin/... # Same structure as Gradle
│ └── test/
│ └── kotlin/... # Same structure as Gradle
└── README.mdRecommended: Use Kotlin 2.3.0 (latest LTS) for new projects with K2 compiler enabled by default.
The K2 compiler brings significant performance improvements and faster compilation times.
Features:
Enabled by default in Kotlin 2.3.0 - no configuration needed.
Use data classes for immutable data holders.
// Data class - automatic equals, hashCode, toString, copy, componentN
data class User(
val id: String,
val name: String,
val email: String,
val age: Int
)
// Usage
val user = User("1", "John Doe", "[email protected]", 30)
// Copy with changes
val updatedUser = user.copy(age = 31)
// Destructuring
val (id, name, email, age) = user
println("User: $name ($email)")Use sealed classes for type-safe state modeling with exhaustive when expressions.
// Sealed interface for result types
sealed interface Result<out T> {
data class Success<T>(val data: T) : Result<T>
data class Error(val message: String, val cause: Throwable? = null) : Result<Nothing>
data object Loading : Result<Nothing>
}
// Exhaustive when - compiler ensures all cases are handled
fun <T> handleResult(result: Result<T>) {
when (result) {
is Result.Success -> println("Success: ${result.data}")
is Result.Error -> println("Error: ${result.message}")
Result.Loading -> println("Loading...")
// No else needed - compiler knows all cases
}
}
// Usage
val result: Result<User> = Result.Success(user)
handleResult(result)Use inline value classes for type-safe wrappers without runtime overhead.
// Inline value class - no boxing overhead
@JvmInline
value class UserId(val value: String)
@JvmInline
value class Email(val value: String) {
init {
require(value.contains("@")) { "Invalid email" }
}
}
// Usage - type-safe, no runtime cost
fun getUserById(id: UserId): User = TODO()
fun sendEmail(email: Email): Unit = TODO()
val userId = UserId("123")
val email = Email("[email protected]")Context receivers allow implicit parameters for cleaner DSLs.
Enable with:
// build.gradle.kts
kotlin {
compilerOptions {
freeCompilerArgs.add("-Xcontext-receivers")
}
}Usage:
interface Logger {
fun log(message: String)
}
// Function with context receiver
context(Logger)
fun processUser(user: User) {
log("Processing user: ${user.name}")
// ...
}
// Call with context
val logger = object : Logger {
override fun log(message: String) = println(message)
}
with(logger) {
processUser(user)
}Simplifies backing property pattern - define implementation type within property scope.
Enable with:
// build.gradle.kts
kotlin {
compilerOptions {
freeCompilerArgs.add("-Xexplicit-backing-fields")
}
}Before:
private val _users = MutableStateFlow<List<User>>(emptyList())
val users: StateFlow<List<User>> = _usersAfter:
val users: StateFlow<List<User>> field = MutableStateFlow(emptyList())Built-in UUID support without external dependencies.
Enable with:
@OptIn(ExperimentalUuidApi::class)Usage:
import kotlin.uuid.Uuid
import kotlin.uuid.ExperimentalUuidApi
@OptIn(ExperimentalUuidApi::class)
fun generateUserId(): Uuid {
return Uuid.generateV4()
}
@OptIn(ExperimentalUuidApi::class)
fun parseUserId(id: String): Uuid? {
return Uuid.parseOrNull(id)
}
// V7 UUIDs (time-based, sortable)
@OptIn(ExperimentalUuidApi::class)
fun generateTimeBasedId(): Uuid {
return Uuid.generateV7()
}Always use structured concurrency - never use GlobalScope.
import kotlinx.coroutines.*
// GOOD - Structured concurrency
suspend fun fetchUserData(userId: String): UserData = coroutineScope {
val userDeferred = async { fetchUser(userId) }
val ordersDeferred = async { fetchOrders(userId) }
UserData(
user = userDeferred.await(),
orders = ordersDeferred.await()
)
}
// BAD - GlobalScope leaks
fun fetchUserDataBad(userId: String) {
GlobalScope.launch { // Don't use GlobalScope!
// ...
}
}Use appropriate dispatchers for different workloads.
// Dispatchers.Default - CPU-intensive work
withContext(Dispatchers.Default) {
// Heavy computation
processLargeDataset(data)
}
// Dispatchers.IO - I/O operations (network, disk)
withContext(Dispatchers.IO) {
// Network call
apiClient.fetchData()
}
// Dispatchers.Main - UI updates (Android/Desktop)
withContext(Dispatchers.Main) {
updateUI(data)
}
// Dispatchers.Unconfined - Advanced use cases only// launch - fire and forget, returns Job
fun processInBackground() = coroutineScope {
launch {
// No result needed
sendNotification()
}
}
// async - returns Deferred<T>, await for result
suspend fun fetchMultipleResources() = coroutineScope {
val users = async { fetchUsers() }
val orders = async { fetchOrders() }
CombinedData(
users = users.await(),
orders = orders.await()
)
}// Cancellation-aware code
suspend fun longRunningTask() = coroutineScope {
repeat(100) { i ->
ensureActive() // Check for cancellation
delay(100)
println("Step $i")
}
}
// Exception handling with supervisorScope
suspend fun fetchDataSafely(): Result<Data> = try {
supervisorScope {
val data = async { fetchData() }
Result.Success(data.await())
}
} catch (e: Exception) {
Result.Error("Failed to fetch data", e)
}
// CoroutineExceptionHandler
val handler = CoroutineExceptionHandler { _, exception ->
println("Caught exception: $exception")
}
val scope = CoroutineScope(SupervisorJob() + handler)// StateFlow - single value, always has current value, conflates updates
class UserViewModel {
private val _userName = MutableStateFlow("")
val userName: StateFlow<String> = _userName.asStateFlow()
fun updateUserName(name: String) {
_userName.value = name
}
}
// SharedFlow - event stream, can replay, doesn't conflate
class EventBus {
private val _events = MutableSharedFlow<Event>(
replay = 0, // Don't replay events
extraBufferCapacity = 64
)
val events: SharedFlow<Event> = _events.asSharedFlow()
suspend fun emit(event: Event) {
_events.emit(event)
}
}
// Hot vs Cold flows
// StateFlow/SharedFlow = Hot (emit regardless of collectors)
// flow { } = Cold (only emit when collected)// Transform flows
val userNames: Flow<String> = users
.map { it.name }
.filter { it.isNotEmpty() }
.distinctUntilChanged()
// Combine flows
val combinedData = combine(users, orders) { users, orders ->
CombinedData(users, orders)
}
// FlatMap variants
val allOrders: Flow<Order> = users
.flatMapConcat { user -> fetchOrders(user.id) } // Sequential
// .flatMapMerge { user -> fetchOrders(user.id) } // Concurrent
// .flatMapLatest { user -> fetchOrders(user.id) } // Cancel previous
// Error handling
val safeData: Flow<Data> = dataFlow
.catch { e -> emit(Data.Empty) }
.retry(3)// Collect in coroutine
lifecycleScope.launch {
userViewModel.userName.collect { name ->
updateUI(name)
}
}
// collectLatest - cancel previous collection on new emission
lifecycleScope.launch {
searchQuery.collectLatest { query ->
// Cancelled if new query arrives
val results = searchRepository.search(query)
updateResults(results)
}
}
// Single value
val user = userFlow.first() // First value
val user = userFlow.firstOrNull() // Or null if emptyStateFlow/SharedFlow, keep MutableStateFlow/MutableSharedFlow privateval data: StateFlow<Data> = dataRepository.getData()
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5000),
initialValue = Data.Empty
)// Safe call (?.) - returns null if receiver is null
val length: Int? = name?.length
// Elvis operator (?:) - default value
val length: Int = name?.length ?: 0
// !! operator - throws NPE if null (use sparingly!)
val length: Int = name!!.length // Only if you're 100% sure it's not null
// Safe cast (as?)
val user: User? = obj as? User
// let for null checks
name?.let { n ->
println("Name: $n")
}// lateinit - non-null var, initialized later (must be var)
class MyClass {
lateinit var repository: Repository
fun init(repo: Repository) {
repository = repo
}
// Check if initialized
fun isInitialized() = ::repository.isInitialized
}
// lazy - initialized on first access (must be val)
class MyClass {
val expensiveValue: String by lazy {
// Computed only once, on first access
computeExpensiveValue()
}
}// GOOD - Use nullable types (Kotlin-native)
fun findUser(id: String): User? {
return repository.findById(id)
}
// BAD - Don't use Java's Optional in Kotlin
fun findUserBad(id: String): Optional<User> { // Avoid
return Optional.ofNullable(repository.findById(id))
}
// When interoping with Java, convert at boundary
fun getUserFromJava(id: String): User? {
return javaService.getUser(id).orElse(null)
}Use underscore prefix for private mutable backing properties.
// Standard pattern for exposing read-only property
class UserRepository {
private val _users = mutableListOf<User>()
val users: List<User> get() = _users
fun addUser(user: User) {
_users.add(user)
}
}
// StateFlow pattern
class UserViewModel {
private val _state = MutableStateFlow<UiState>(UiState.Loading)
val state: StateFlow<UiState> = _state.asStateFlow()
fun updateState(newState: UiState) {
_state.value = newState
}
}
// Alternative: explicit backing fields (experimental, Kotlin 2.3+)
// Requires: -Xexplicit-backing-fields compiler flag
val users: StateFlow<List<User>> field = MutableStateFlow(emptyList())Kotlin provides five scope functions: let, run, with, apply, also. Choose based on context object reference and return value.
| Function | Object Reference | Return Value | Use Case |
|---|---|---|---|
let | it | Lambda result | Null checks, transformations |
run | this | Lambda result | Object configuration + computation |
with | this | Lambda result | Group function calls on object |
apply | this | Context object | Object initialization |
also | it | Context object | Side effects |
// Null check with let
val length: Int? = name?.let { it.length }
// Chain with let
val result = value?.let { v ->
processValue(v)
}?.let { processed ->
saveResult(processed)
}
// Execute block only if non-null
user?.let { u ->
println("User: ${u.name}")
sendWelcomeEmail(u)
}
// Transform value
val uppercaseName = name?.let { it.uppercase() }// Object initialization (returns `this`)
val user = User().apply {
name = "John Doe"
email = "[email protected]"
age = 30
}
// Configure and return
val intent = Intent(context, DetailActivity::class.java).apply {
putExtra("id", userId)
flags = Intent.FLAG_ACTIVITY_NEW_TASK
}
// Builder pattern style
val dialog = AlertDialog.Builder(context).apply {
setTitle("Confirm")
setMessage("Are you sure?")
setPositiveButton("Yes") { _, _ -> confirm() }
}.create()// Side effects (returns `this`)
val numbers = mutableListOf(1, 2, 3).also {
println("List created with ${it.size} elements")
}
// Debug chain
val result = processData(input)
.also { println("Intermediate result: $it") }
.transformData()
.also { println("Final result: $it") }
// Multiple operations
val user = createUser().also {
logUserCreation(it)
sendWelcomeEmail(it)
}// Compute value (returns lambda result)
val result = run {
val x = computeX()
val y = computeY()
x + y
}
// Null check with run
val result = service?.run {
fetchData()
processData()
}
// Replace multiple statements
val hexString = run {
val color = getColor()
Integer.toHexString(color)
}// Group operations on object (returns lambda result)
val result = with(canvas) {
drawCircle(centerX, centerY, radius)
drawLine(startX, startY, endX, endY)
drawText(text, x, y)
save()
}
// Configure object
with(sharedPreferences.edit()) {
putString("username", username)
putInt("age", age)
apply()
}
// Avoid repetition
val user = getUser()
with(user) {
println("Name: $name")
println("Email: $email")
println("Age: $age")
}// Use let for null checks
value?.let { println(it) }
// Use apply for object initialization (returns object)
val config = Config().apply { timeout = 30 }
// Use also for side effects (returns object)
val list = getList().also { log("Size: ${it.size}") }
// Use run for computations (returns result)
val result = run { compute() }
// Use with for grouping calls (returns result)
val formatted = with(user) { "$name ($email)" }// Prefer immutable collections
val users: List<User> = listOf(user1, user2, user3)
// Mutable only when needed
val mutableUsers: MutableList<User> = mutableListOf()
mutableUsers.add(user4)
// Read-only view of mutable collection
val readOnlyView: List<User> = mutableUsers
// List.of() for Java interop (immutable)
val javaList = java.util.List.of(user1, user2)Use Sequence for large collections or chained operations.
// List - eager evaluation (creates intermediate lists)
val result = users
.filter { it.age > 18 }
.map { it.name }
.take(10)
// Sequence - lazy evaluation (no intermediate collections)
val result = users.asSequence()
.filter { it.age > 18 }
.map { it.name }
.take(10)
.toList() // Terminal operation
// Generate infinite sequence
val fibonacci = generateSequence(1 to 1) { (a, b) -> b to a + b }
.map { it.first }
.take(10)
.toList()// Transform
val names = users.map { it.name }
val adults = users.filter { it.age >= 18 }
val groups = users.groupBy { it.department }
// Reduce/Fold
val totalAge = users.sumOf { it.age }
val oldest = users.maxByOrNull { it.age }
val names = users.fold("") { acc, user -> "$acc, ${user.name}" }
// Partition
val (adults, minors) = users.partition { it.age >= 18 }
// Associate
val userById = users.associateBy { it.id }
val nameToUser = users.associateWith { it.name }// BAD - off-by-one prone
for (i in 0..n - 1) { }
for (i in 0 until n) { } // Better but verbose
// GOOD - use ..< (Kotlin 1.7+)
for (i in 0..<n) { }
// Ranges
for (i in 1..10) { } // 1 to 10 (inclusive)
for (i in 1..<10) { } // 1 to 9 (exclusive end)
for (i in 10 downTo 1) { } // 10 to 1 (descending)
for (i in 1..10 step 2) { } // 1, 3, 5, 7, 9
// Prefer higher-order functions over loops
// GOOD
val adults = users.filter { it.age >= 18 }
// Less idiomatic
val adults = mutableListOf<User>()
for (user in users) {
if (user.age >= 18) {
adults.add(user)
}
}Use string templates instead of concatenation.
// Simple variable - no braces needed
val message = "Hello, $name!"
// Expression - use braces
val message = "User $name has ${children.size} children"
val message = "${user.name} (${user.age} years old)"
// Property access
val message = "Length: ${text.length}"
// Function call
val message = "Result: ${compute()}"
// BAD - concatenation
val message = "Hello, " + name + "!"
// Multiline strings with templates
val json = """
{
"name": "$name",
"age": $age,
"email": "$email"
}
""".trimIndent()
// Multi-dollar strings (Kotlin 2.0+) - escape $ in raw strings
val jsonSchema = $$"""
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "$${title ?: "unknown"}"
}
"""Extension functions add functionality to existing classes without inheritance.
// GOOD - Add functionality to existing type
fun String.isValidEmail(): Boolean {
return this.contains("@") && this.contains(".")
}
// Usage
val email = "[email protected]"
if (email.isValidEmail()) { }
// GOOD - Domain-specific operations
fun User.toDisplayString(): String {
return "$name ($email)"
}
// GOOD - Collection transformations
fun List<User>.activeUsers(): List<User> {
return filter { it.isActive }
}// 1. Restrict visibility (prefer private/internal)
private fun String.sanitize(): String {
return this.trim().lowercase()
}
// 2. Place extensions in same file as class (if you own it)
// User.kt
data class User(val name: String)
fun User.greet() = "Hello, $name"
// 3. Group related extensions in separate file
// StringExtensions.kt
fun String.isValidEmail(): Boolean { }
fun String.isValidUrl(): Boolean { }
// 4. Don't overuse - prefer member functions when appropriate
class User {
// GOOD - core behavior as member
fun activate() { }
}
// Extension for convenience
fun User.deactivate() { }// Extension property (no backing field allowed)
val String.lastChar: Char?
get() = this.lastOrNull()
val List<Int>.median: Double
get() {
val sorted = this.sorted()
val middle = sorted.size / 2
return if (sorted.size % 2 == 0) {
(sorted[middle - 1] + sorted[middle]) / 2.0
} else {
sorted[middle].toDouble()
}
}
// Usage
println("Hello".lastChar) // 'o'
println(listOf(1, 3, 2).median) // 2.0// Guard conditions in when
sealed interface Status {
data class Ok(val info: Info) : Status
data class Error(val code: Int) : Status
}
fun handleStatus(status: Status): String {
return when (status) {
is Status.Ok if (status.info.isEmpty()) -> "no information"
is Status.Ok -> "success: ${status.info}"
is Status.Error if (status.code >= 500) -> "server error"
is Status.Error -> "client error: ${status.code}"
}
}
// Guard with multiple conditions
fun classify(value: Int): String {
return when (value) {
in 0..10 if (value % 2 == 0) -> "small even"
in 0..10 -> "small odd"
in 11..100 -> "medium"
else -> "large"
}
}// Prefer when as expression (not statement)
// GOOD
val result = when (x) {
0 -> "zero"
in 1..10 -> "small"
else -> "large"
}
// BAD
var result: String
when (x) {
0 -> result = "zero"
in 1..10 -> result = "small"
else -> result = "large"
}// Generic function
fun <T> singletonList(item: T): List<T> {
return listOf(item)
}
val list = singletonList(42) // List<Int>
val names = singletonList("Alice") // List<String>
// Multiple type parameters
fun <K, V> mapOf(key: K, value: V): Map<K, V> {
return mapOf(key to value)
}
// Type constraints
fun <T : Comparable<T>> max(a: T, b: T): T {
return if (a > b) a else b
}// Generic class
class Box<T>(val value: T) {
fun get(): T = value
}
val intBox = Box(42)
val stringBox = Box("hello")
// Multiple type parameters
class Pair<A, B>(val first: A, val second: B)
val pair = Pair(1, "one")// out - covariant (producer)
interface Producer<out T> {
fun produce(): T
}
// in - contravariant (consumer)
interface Consumer<in T> {
fun consume(item: T)
}
// Example
class ListWrapper<out T>(private val list: List<T>) {
fun get(index: Int): T = list[index] // OK - produces T
// fun add(item: T) { } // Error - cannot consume T
}
// Covariance allows
val strings: Producer<String> = object : Producer<String> {
override fun produce() = "Hello"
}
val anys: Producer<Any> = strings // OK - String is subtype of Any// inline + reified for runtime type access
inline fun <reified T> isInstance(value: Any): Boolean {
return value is T
}
val result = isInstance<String>("hello") // true
val result2 = isInstance<Int>("hello") // false
// Useful for type-safe casts
inline fun <reified T> Any.asOrNull(): T? {
return this as? T
}
val str: String? = obj.asOrNull<String>()Follow Kotlin official coding conventions for consistent code formatting.
// 4 spaces (not tabs)
// Opening brace at line end (Java-style)
if (condition) {
doSomething()
}
// Single-line if can omit braces
if (condition) return
// Multi-line conditions: indent by 4 spaces
if (!component.isSyncing &&
!hasErrors()
) {
proceed()
}// Space around binary operators
val sum = a + b
val result = x * y
// NO space for range operator
for (i in 0..10) { }
// NO space for unary operators
val x = -5
val y = a++
// Space after control flow keywords
if (condition) { }
when (x) { }
for (i in list) { }
// NO space before ( in calls/constructors
foo(1, 2)
User(name, age)
// NO spaces around . or ?.
user.name
user?.email
// Space after //
// This is a commentEncouraged at declaration/call sites (makes diffs cleaner):
// Function parameters
fun process(
name: String,
age: Int,
email: String, // trailing comma
) { }
// Arguments
process(
name = "John",
age = 30,
email = "[email protected]", // trailing comma
)
// Collections
val list = listOf(
"apple",
"banana",
"cherry", // trailing comma
)Prefer expression bodies for simple functions:
// GOOD - expression body
fun double(x: Int) = x * 2
// BAD - block body for simple function
fun double(x: Int): Int {
return x * 2
}
// Expression body with line break
fun longFunctionName(
arg1: String,
arg2: String
) = processArguments(arg1, arg2)Use named arguments for clarity:
// Multiple parameters of same type
drawSquare(x = 10, y = 10, width = 100, height = 100)
// Boolean parameters
setVisibility(visible = true, animated = false)
// Skip default parameters
createUser(name = "John") // age has defaultimport kotlin.test.*
import org.junit.jupiter.api.*
class UserServiceTest {
private lateinit var userService: UserService
private lateinit var repository: UserRepository
@BeforeEach
fun setup() {
repository = InMemoryUserRepository()
userService = UserService(repository)
}
@Test
fun `should return user when id exists`() {
// Given
val user = User("1", "John", "[email protected]", 30)
repository.save(user)
// When
val result = userService.findById("1")
// Then
assertNotNull(result)
assertEquals("John", result.name)
}
@Test
fun `should return null when user not found`() {
// When
val result = userService.findById("999")
// Then
assertNull(result)
}
@ParameterizedTest
@ValueSource(strings = ["", " ", "\t"])
fun `should throw when id is blank`(id: String) {
assertFailsWith<IllegalArgumentException> {
userService.findById(id)
}
}
}import io.mockk.*
import kotlin.test.*
class UserServiceTest {
private val repository = mockk<UserRepository>()
private val userService = UserService(repository)
@Test
fun `should call repository when finding user`() {
// Given
val user = User("1", "John", "[email protected]", 30)
every { repository.findById("1") } returns user
// When
val result = userService.findById("1")
// Then
verify { repository.findById("1") }
assertEquals("John", result?.name)
}
@Test
fun `should handle repository exception`() {
// Given
every { repository.findById(any()) } throws RuntimeException("DB error")
// When/Then
assertFailsWith<RuntimeException> {
userService.findById("1")
}
}
}import kotlinx.coroutines.test.*
import kotlin.test.*
class UserViewModelTest {
@Test
fun `should fetch user on init`() = runTest {
// Given
val repository = FakeUserRepository()
val viewModel = UserViewModel(repository)
// When
advanceUntilIdle()
// Then
assertEquals(UserState.Success(user), viewModel.state.value)
}
@Test
fun `should emit loading state while fetching`() = runTest {
// Given
val repository = SlowUserRepository()
val viewModel = UserViewModel(repository)
val states = mutableListOf<UserState>()
// Collect states
backgroundScope.launch {
viewModel.state.toList(states)
}
// When
advanceUntilIdle()
// Then
assertEquals(
listOf(UserState.Loading, UserState.Success(user)),
states
)
}
}// build.gradle.kts
plugins {
kotlin("jvm") version "2.3.10"
application
}
group = "com.example"
version = "1.0.0"
repositories {
mavenCentral()
}
dependencies {
// Kotlin standard library (automatically added in 2.3+)
// implementation(kotlin("stdlib"))
// Coroutines
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.0")
// Testing
testImplementation(kotlin("test"))
testImplementation("org.junit.jupiter:junit-jupiter:5.10.0")
testImplementation("io.mockk:mockk:1.13.8")
testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.8.0")
}
kotlin {
jvmToolchain(21) // Use Java 21
compilerOptions {
freeCompilerArgs.add("-Xcontext-receivers") // Enable experimental features
freeCompilerArgs.add("-Xexplicit-backing-fields")
}
}
tasks.test {
useJUnitPlatform()
}
application {
mainClass.set("com.example.ApplicationKt")
}dependencies {
// implementation - internal dependency, not exposed to consumers
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.0")
// api - exposed to consumers (only in library modules)
api("com.example:shared-models:1.0.0")
// compileOnly - needed only at compile time
compileOnly("org.jetbrains:annotations:24.0.0")
// runtimeOnly - needed only at runtime
runtimeOnly("com.h2database:h2:2.2.220")
}<project>
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>myapp</artifactId>
<version>1.0.0</version>
<properties>
<kotlin.version>2.3.10</kotlin.version>
<kotlin.compiler.jvmTarget>21</kotlin.compiler.jvmTarget>
</properties>
<dependencies>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-stdlib</artifactId>
<version>${kotlin.version}</version>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlinx</groupId>
<artifactId>kotlinx-coroutines-core</artifactId>
<version>1.8.0</version>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-test-junit5</artifactId>
<version>${kotlin.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<sourceDirectory>src/main/kotlin</sourceDirectory>
<testSourceDirectory>src/test/kotlin</testSourceDirectory>
<plugins>
<plugin>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-maven-plugin</artifactId>
<version>${kotlin.version}</version>
</plugin>
</plugins>
</build>
</project>| Tool | Purpose |
|---|---|
gradle | Build automation (Kotlin DSL recommended) |
kotlin-test / junit-jupiter | Testing framework |
mockk | Mocking library (Kotlin-native) |
ktlint | Code formatting & linting |
detekt | Static code analysis, code smells |
kotlinx-coroutines-test | Coroutines testing utilities |
kotlinx-serialization | JSON/protobuf serialization |
testcontainers | Integration testing with Docker |
kotest | Alternative testing framework (BDD-style) |
# Install ktlint
brew install ktlint # macOS
# Or download from: https://github.com/pinterest/ktlint
# Format code
ktlint -F "src/**/*.kt"
# Check code
ktlint "src/**/*.kt"
# Gradle plugin
# build.gradle.kts
plugins {
id("org.jlleitschuh.gradle.ktlint") version "12.0.3"
}// build.gradle.kts
plugins {
id("io.gitlab.arturbosch.detekt") version "1.23.0"
}
detekt {
buildUponDefaultConfig = true
allRules = false
config.setFrom(files("$projectDir/config/detekt.yml"))
}
dependencies {
detektPlugins("io.gitlab.arturbosch.detekt:detekt-formatting:1.23.0")
}val over var, immutable collections!! operatorwhencoroutineScope, never GlobalScopeStateFlow/SharedFlow for reactive stateequals, hashCode, toString, copywhen, if as expressions when possiblerunTest and TestDispatcher// BAD - redundant comment
// Get user from repository
val user = repository.findById(id)
// GOOD - self-explanatory code, no comment needed
val user = repository.findById(id)
// GOOD - comment explains WHY (not obvious)
// Rate limit: API allows max 100 requests per minute per client
rateLimiter.acquire()
// GOOD - KDoc for public API
/**
* Fetches user by ID. Returns null if user not found.
*
* @param id User ID (non-blank)
* @throws IllegalArgumentException if id is blank
*/
fun findUserById(id: String): User?~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.