| name | standards-kotlin |
| description | Kotlin coding standards for modern applications. Includes naming conventions, coroutines, flows, modern Kotlin 2.3.0 features, and recommended tooling. |
| type | context |
| applies_to | ["kotlin","gradle","maven","junit","kotest","kotlinx-coroutines","ktor","spring","mockk","testcontainers"] |
| file_extensions | [".kt",".kts"] |
Kotlin Coding Standards
Core Principles
- Explicitness: Explicit code over implicit magic
- Readability: Readable code over clever tricks
- Null Safety: Embrace Kotlin's null safety system
- Immutability: Prefer
val over var, immutable collections
- Expressiveness: Use Kotlin's expressive features (data classes, sealed classes)
- DRY: Don't Repeat Yourself - but keep it simple
General Rules
- Prefer
val over var: Immutability by default
- Use data classes: For simple data holders
- Sealed classes/interfaces: For type-safe state modeling
- Early returns: Avoid deep nesting
- Descriptive names: Clear, meaningful names
- Minimal changes: Only change relevant code
- No over-engineering: Keep it simple
- Minimal comments: Self-explanatory code. Comments for "why", not "what"
Naming Conventions
| 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` |
Project Structure
Gradle Kotlin DSL Project (Recommended)
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
Maven Project (Alternative)
myproject/
├── pom.xml
├── src/
│ ├── main/
│ │ └── kotlin/... # Same structure as Gradle
│ └── test/
│ └── kotlin/... # Same structure as Gradle
└── README.md
Modern Kotlin Features
Recommended: Use Kotlin 2.3.0 (latest LTS) for new projects with K2 compiler enabled by default.
K2 Compiler (Stable since 2.0)
The K2 compiler brings significant performance improvements and faster compilation times.
Features:
- Faster compilation (up to 2x)
- Better smart casts
- Improved type inference
- Unified architecture for all platforms
Enabled by default in Kotlin 2.3.0 - no configuration needed.
Data Classes
Use data classes for immutable data holders.
data class User(
val id: String,
val name: String,
val email: String,
val age: Int
)
val user = User("1", "John Doe", "john@example.com", 30)
val updatedUser = user.copy(age = 31)
val (id, name, email, age) = user
println("User: $name ($email)")
Sealed Classes/Interfaces (Exhaustive When)
Use sealed classes for type-safe state modeling with exhaustive when expressions.
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>
}
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...")
}
}
val result: Result<User> = Result.Success(user)
handleResult(result)
Inline Value Classes (Zero-Cost Wrappers)
Use inline value classes for type-safe wrappers without runtime overhead.
@JvmInline
value class UserId(val value: String)
@JvmInline
value class Email(val value: String) {
init {
require(value.contains("@")) { "Invalid email" }
}
}
fun getUserById(id: UserId): User = TODO()
fun sendEmail(email: Email): Unit = TODO()
val userId = UserId("123")
val email = Email("user@example.com")
Context Receivers (Experimental, 2.2+)
Context receivers allow implicit parameters for cleaner DSLs.
Enable with:
kotlin {
compilerOptions {
freeCompilerArgs.add("-Xcontext-receivers")
}
}
Usage:
interface Logger {
fun log(message: String)
}
context(Logger)
fun processUser(user: User) {
log("Processing user: ${user.name}")
}
val logger = object : Logger {
override fun log(message: String) = println(message)
}
with(logger) {
processUser(user)
}
Explicit Backing Fields (Experimental, 2.3)
Simplifies backing property pattern - define implementation type within property scope.
Enable with:
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())
UUID API (Experimental, 2.3)
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)
}
@OptIn(ExperimentalUuidApi::class)
fun generateTimeBasedId(): Uuid {
return Uuid.generateV7()
}
Coroutines & Concurrency
Structured Concurrency
Always use structured concurrency - never use GlobalScope.
import kotlinx.coroutines.*
suspend fun fetchUserData(userId: String): UserData = coroutineScope {
val userDeferred = async { fetchUser(userId) }
val ordersDeferred = async { fetchOrders(userId) }
UserData(
user = userDeferred.await(),
orders = ordersDeferred.await()
)
}
fun fetchUserDataBad(userId: String) {
GlobalScope.launch {
}
}
Dispatchers
Use appropriate dispatchers for different workloads.
withContext(Dispatchers.Default) {
processLargeDataset(data)
}
withContext(Dispatchers.IO) {
apiClient.fetchData()
}
withContext(Dispatchers.Main) {
updateUI(data)
}
launch vs async
fun processInBackground() = coroutineScope {
launch {
sendNotification()
}
}
suspend fun fetchMultipleResources() = coroutineScope {
val users = async { fetchUsers() }
val orders = async { fetchOrders() }
CombinedData(
users = users.await(),
orders = orders.await()
)
}
Cancellation & Exception Handling
suspend fun longRunningTask() = coroutineScope {
repeat(100) { i ->
ensureActive()
delay(100)
println("Step $i")
}
}
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)
}
val handler = CoroutineExceptionHandler { _, exception ->
println("Caught exception: $exception")
}
val scope = CoroutineScope(SupervisorJob() + handler)
Flow API
StateFlow vs SharedFlow vs MutableStateFlow
class UserViewModel {
private val _userName = MutableStateFlow("")
val userName: StateFlow<String> = _userName.asStateFlow()
fun updateUserName(name: String) {
_userName.value = name
}
}
class EventBus {
private val _events = MutableSharedFlow<Event>(
replay = 0,
extraBufferCapacity = 64
)
val events: SharedFlow<Event> = _events.asSharedFlow()
suspend fun emit(event: Event) {
_events.emit(event)
}
}
Flow Operators
val userNames: Flow<String> = users
.map { it.name }
.filter { it.isNotEmpty() }
.distinctUntilChanged()
val combinedData = combine(users, orders) { users, orders ->
CombinedData(users, orders)
}
val allOrders: Flow<Order> = users
.flatMapConcat { user -> fetchOrders(user.id) }
val safeData: Flow<Data> = dataFlow
.catch { e -> emit(Data.Empty) }
.retry(3)
Flow Collection
lifecycleScope.launch {
userViewModel.userName.collect { name ->
updateUI(name)
}
}
lifecycleScope.launch {
searchQuery.collectLatest { query ->
val results = searchRepository.search(query)
updateResults(results)
}
}
val user = userFlow.first()
val user = userFlow.firstOrNull()
Flow Best Practices
- Single source of truth: Expose
StateFlow/SharedFlow, keep MutableStateFlow/MutableSharedFlow private
- Use
collectLatest for UI: Cancel previous work on new emissions
- Use
stateIn for cold-to-hot conversion:
val data: StateFlow<Data> = dataRepository.getData()
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5000),
initialValue = Data.Empty
)
Null Safety
Safe Calls, Elvis, and !! Operator
val length: Int? = name?.length
val length: Int = name?.length ?: 0
val length: Int = name!!.length
val user: User? = obj as? User
name?.let { n ->
println("Name: $n")
}
lateinit vs lazy
class MyClass {
lateinit var repository: Repository
fun init(repo: Repository) {
repository = repo
}
fun isInitialized() = ::repository.isInitialized
}
class MyClass {
val expensiveValue: String by lazy {
computeExpensiveValue()
}
}
Nullable Types vs Optional
fun findUser(id: String): User? {
return repository.findById(id)
}
fun findUserBad(id: String): Optional<User> {
return Optional.ofNullable(repository.findById(id))
}
fun getUserFromJava(id: String): User? {
return javaService.getUser(id).orElse(null)
}
Backing Properties (Standard Pattern)
Use underscore prefix for private mutable backing properties.
class UserRepository {
private val _users = mutableListOf<User>()
val users: List<User> get() = _users
fun addUser(user: User) {
_users.add(user)
}
}
class UserViewModel {
private val _state = MutableStateFlow<UiState>(UiState.Loading)
val state: StateFlow<UiState> = _state.asStateFlow()
fun updateState(newState: UiState) {
_state.value = newState
}
}
val users: StateFlow<List<User>> field = MutableStateFlow(emptyList())
Scope Functions
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 |
let - Null Checks & Transformations
val length: Int? = name?.let { it.length }
val result = value?.let { v ->
processValue(v)
}?.let { processed ->
saveResult(processed)
}
user?.let { u ->
println("User: ${u.name}")
sendWelcomeEmail(u)
}
val uppercaseName = name?.let { it.uppercase() }
apply - Object Initialization
val user = User().apply {
name = "John Doe"
email = "john@example.com"
age = 30
}
val intent = Intent(context, DetailActivity::class.java).apply {
putExtra("id", userId)
flags = Intent.FLAG_ACTIVITY_NEW_TASK
}
val dialog = AlertDialog.Builder(context).apply {
setTitle("Confirm")
setMessage("Are you sure?")
setPositiveButton("Yes") { _, _ -> confirm() }
}.create()
also - Side Effects
val numbers = mutableListOf(1, 2, 3).also {
println("List created with ${it.size} elements")
}
val result = processData(input)
.also { println("Intermediate result: $it") }
.transformData()
.also { println("Final result: $it") }
val user = createUser().also {
logUserCreation(it)
sendWelcomeEmail(it)
}
run - Computations
val result = run {
val x = computeX()
val y = computeY()
x + y
}
val result = service?.run {
fetchData()
processData()
}
val hexString = run {
val color = getColor()
Integer.toHexString(color)
}
with - Multiple Calls on Object
val result = with(canvas) {
drawCircle(centerX, centerY, radius)
drawLine(startX, startY, endX, endY)
drawText(text, x, y)
save()
}
with(sharedPreferences.edit()) {
putString("username", username)
putInt("age", age)
apply()
}
val user = getUser()
with(user) {
println("Name: $name")
println("Email: $email")
println("Age: $age")
}
Scope Function Selection Guide
value?.let { println(it) }
val config = Config().apply { timeout = 30 }
val list = getList().also { log("Size: ${it.size}") }
val result = run { compute() }
val formatted = with(user) { "$name ($email)" }
Collections & Sequences
List vs MutableList
val users: List<User> = listOf(user1, user2, user3)
val mutableUsers: MutableList<User> = mutableListOf()
mutableUsers.add(user4)
val readOnlyView: List<User> = mutableUsers
val javaList = java.util.List.of(user1, user2)
Sequence for Lazy Evaluation
Use Sequence for large collections or chained operations.
val result = users
.filter { it.age > 18 }
.map { it.name }
.take(10)
val result = users.asSequence()
.filter { it.age > 18 }
.map { it.name }
.take(10)
.toList()
val fibonacci = generateSequence(1 to 1) { (a, b) -> b to a + b }
.map { it.first }
.take(10)
.toList()
Collection Operations
val names = users.map { it.name }
val adults = users.filter { it.age >= 18 }
val groups = users.groupBy { it.department }
val totalAge = users.sumOf { it.age }
val oldest = users.maxByOrNull { it.age }
val names = users.fold("") { acc, user -> "$acc, ${user.name}" }
val (adults, minors) = users.partition { it.age >= 18 }
val userById = users.associateBy { it.id }
val nameToUser = users.associateWith { it.name }
Loops on Ranges
for (i in 0..n - 1) { }
for (i in 0 until n) { }
for (i in 0..<n) { }
for (i in 1..10) { }
for (i in 1..<10) { }
for (i in 10 downTo 1) { }
for (i in 1..10 step 2) { }
val adults = users.filter { it.age >= 18 }
val adults = mutableListOf<User>()
for (user in users) {
if (user.age >= 18) {
adults.add(user)
}
}
String Templates
Use string templates instead of concatenation.
val message = "Hello, $name!"
val message = "User $name has ${children.size} children"
val message = "${user.name} (${user.age} years old)"
val message = "Length: ${text.length}"
val message = "Result: ${compute()}"
val message = "Hello, " + name + "!"
val json = """
{
"name": "$name",
"age": $age,
"email": "$email"
}
""".trimIndent()
val jsonSchema = $$"""
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "$${title ?: "unknown"}"
}
"""
Extension Functions
Extension functions add functionality to existing classes without inheritance.
When to Use Extensions
fun String.isValidEmail(): Boolean {
return this.contains("@") && this.contains(".")
}
val email = "user@example.com"
if (email.isValidEmail()) { }
fun User.toDisplayString(): String {
return "$name ($email)"
}
fun List<User>.activeUsers(): List<User> {
return filter { it.isActive }
}
Extension Best Practices
private fun String.sanitize(): String {
return this.trim().lowercase()
}
data class User(val name: String)
fun User.greet() = "Hello, $name"
fun String.isValidEmail(): Boolean { }
fun String.isValidUrl(): Boolean { }
class User {
fun activate() { }
}
fun User.deactivate() { }
Extension Properties
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()
}
}
println("Hello".lastChar)
println(listOf(1, 3, 2).median)
When Expressions Advanced
When with Guards (Kotlin 1.7+)
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}"
}
}
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"
}
}
When as Expression
val result = when (x) {
0 -> "zero"
in 1..10 -> "small"
else -> "large"
}
var result: String
when (x) {
0 -> result = "zero"
in 1..10 -> result = "small"
else -> result = "large"
}
Generics Basics
Generic Functions
fun <T> singletonList(item: T): List<T> {
return listOf(item)
}
val list = singletonList(42)
val names = singletonList("Alice")
fun <K, V> mapOf(key: K, value: V): Map<K, V> {
return mapOf(key to value)
}
fun <T : Comparable<T>> max(a: T, b: T): T {
return if (a > b) a else b
}
Generic Classes
class Box<T>(val value: T) {
fun get(): T = value
}
val intBox = Box(42)
val stringBox = Box("hello")
class Pair<A, B>(val first: A, val second: B)
val pair = Pair(1, "one")
Variance (in, out)
interface Producer<out T> {
fun produce(): T
}
interface Consumer<in T> {
fun consume(item: T)
}
class ListWrapper<out T>(private val list: List<T>) {
fun get(index: Int): T = list[index]
}
val strings: Producer<String> = object : Producer<String> {
override fun produce() = "Hello"
}
val anys: Producer<Any> = strings
Reified Type Parameters
inline fun <reified T> isInstance(value: Any): Boolean {
return value is T
}
val result = isInstance<String>("hello")
val result2 = isInstance<Int>("hello")
inline fun <reified T> Any.asOrNull(): T? {
return this as? T
}
val str: String? = obj.asOrNull<String>()
Formatting & Code Style
Follow Kotlin official coding conventions for consistent code formatting.
Indentation & Braces
if (condition) {
doSomething()
}
if (condition) return
if (!component.isSyncing &&
!hasErrors()
) {
proceed()
}
Whitespace Rules
val sum = a + b
val result = x * y
for (i in 0..10) { }
val x = -5
val y = a++
if (condition) { }
when (x) { }
for (i in list) { }
foo(1, 2)
User(name, age)
user.name
user?.email
Trailing Commas
Encouraged at declaration/call sites (makes diffs cleaner):
fun process(
name: String,
age: Int,
email: String,
) { }
process(
name = "John",
age = 30,
email = "john@example.com",
)
val list = listOf(
"apple",
"banana",
"cherry",
)
Expression Bodies
Prefer expression bodies for simple functions:
fun double(x: Int) = x * 2
fun double(x: Int): Int {
return x * 2
}
fun longFunctionName(
arg1: String,
arg2: String
) = processArguments(arg1, arg2)
Named Arguments
Use named arguments for clarity:
drawSquare(x = 10, y = 10, width = 100, height = 100)
setVisibility(visible = true, animated = false)
createUser(name = "John")
Testing Fundamentals
JUnit 5 + kotlin.test
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`() {
val user = User("1", "John", "john@example.com", 30)
repository.save(user)
val result = userService.findById("1")
assertNotNull(result)
assertEquals("John", result.name)
}
@Test
fun `should return null when user not found`() {
val result = userService.findById("999")
assertNull(result)
}
@ParameterizedTest
@ValueSource(strings = ["", " ", "\t"])
fun `should throw when id is blank`(id: String) {
assertFailsWith<IllegalArgumentException> {
userService.findById(id)
}
}
}
Mockk for Mocking
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`() {
val user = User("1", "John", "john@example.com", 30)
every { repository.findById("1") } returns user
val result = userService.findById("1")
verify { repository.findById("1") }
assertEquals("John", result?.name)
}
@Test
fun `should handle repository exception`() {
every { repository.findById(any()) } throws RuntimeException("DB error")
assertFailsWith<RuntimeException> {
userService.findById("1")
}
}
}
Coroutines Testing
import kotlinx.coroutines.test.*
import kotlin.test.*
class UserViewModelTest {
@Test
fun `should fetch user on init`() = runTest {
val repository = FakeUserRepository()
val viewModel = UserViewModel(repository)
advanceUntilIdle()
assertEquals(UserState.Success(user), viewModel.state.value)
}
@Test
fun `should emit loading state while fetching`() = runTest {
val repository = SlowUserRepository()
val viewModel = UserViewModel(repository)
val states = mutableListOf<UserState>()
backgroundScope.launch {
viewModel.state.toList(states)
}
advanceUntilIdle()
assertEquals(
listOf(UserState.Loading, UserState.Success(user)),
states
)
}
}
Build Tool Awareness
Gradle Kotlin DSL (build.gradle.kts) - Recommended
plugins {
kotlin("jvm") version "2.3.10"
application
}
group = "com.example"
version = "1.0.0"
repositories {
mavenCentral()
}
dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.0")
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)
compilerOptions {
freeCompilerArgs.add("-Xcontext-receivers")
freeCompilerArgs.add("-Xexplicit-backing-fields")
}
}
tasks.test {
useJUnitPlatform()
}
application {
mainClass.set("com.example.ApplicationKt")
}
Gradle Dependencies: implementation vs api
dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.0")
api("com.example:shared-models:1.0.0")
compileOnly("org.jetbrains:annotations:24.0.0")
runtimeOnly("com.h2database:h2:2.2.220")
}
Maven (pom.xml) - Alternative
<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>
Recommended Tooling
| 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) |
ktlint Usage
brew install ktlint
ktlint -F "src/**/*.kt"
ktlint "src/**/*.kt"
plugins {
id("org.jlleitschuh.gradle.ktlint") version "12.0.3"
}
detekt Usage
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")
}
Production Best Practices
- Immutability by default - Use
val over var, immutable collections
- Null safety - Embrace nullable types, avoid
!! operator
- Sealed classes for state - Type-safe state modeling with exhaustive
when
- Structured concurrency - Always use
coroutineScope, never GlobalScope
- Flow for streams - Use
StateFlow/SharedFlow for reactive state
- Data classes for DTOs - Automatic
equals, hashCode, toString, copy
- Inline value classes - Type-safe wrappers without runtime cost
- Explicit over implicit - Clear, readable code over clever tricks
- Early returns - Reduce nesting, improve readability
- Descriptive naming - Functions and properties should explain intent
- Prefer expressions - Use
when, if as expressions when possible
- Use extension functions - Add functionality without inheritance
- Coroutine cancellation - Always handle cancellation properly
- Test coroutines properly - Use
runTest and TestDispatcher
- ktlint + detekt - Automate code quality checks in CI
Comments - Less is More
val user = repository.findById(id)
val user = repository.findById(id)
rateLimiter.acquire()
fun findUserById(id: String): User?
References