skills/standards-kotlin/SKILL.md
Kotlin coding standards for modern applications. Includes naming conventions, coroutines, flows, modern Kotlin 2.3.0 features, and recommended tooling.
npx skillsauth add b33eep/claude-code-setup standards-kotlinInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
val over var, immutable collectionsval over var: Immutability by default| 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.md
myproject/
├── pom.xml
├── src/
│ ├── main/
│ │ └── kotlin/... # Same structure as Gradle
│ └── test/
│ └── kotlin/... # Same structure as Gradle
└── README.md
Recommended: 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>> = _users
After:
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 empty
StateFlow/SharedFlow, keep MutableStateFlow/MutableSharedFlow privatecollectLatest for UI: Cancel previous work on new emissionsstateIn for cold-to-hot conversion:val 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 comment
Encouraged 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 default
import 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?
development
Guide users through creating, reviewing, and fixing custom skills for Claude — both command skills (invoked via /slash) and context skills (auto-loaded by tech stack). Use when the user asks to create a skill, build a skill, make a new slash command skill, add a coding standards skill, review an existing skill, update a skill, or fix a skill that doesn't trigger.
development
Build or edit Slidev (sli.dev) presentations for tech talks, workshops, conference sessions, and live-coding demos. Use when the user asks to create slides, a deck, a presentation, a workshop deck, a conference talk, or edit an existing slides.md.
documentation
Download YouTube video transcripts with automatic frame extraction for visual references. Use when analyzing YouTube videos, tutorials, or conference talks.
documentation
Write INVEST-compliant user stories with Given-When-Then acceptance criteria. Use when writing user stories, creating acceptance criteria, or during /design Step 4.