Why Kotlin Needs Specific AI Coding Rules
Kotlin was designed to be a better Java — more concise, safer, and more expressive. But AI assistants trained on vast amounts of Java code generate 'Java in Kotlin syntax': verbose POJOs instead of data classes, null-check pyramids instead of safe calls, Thread-based concurrency instead of coroutines, and explicit type declarations everywhere Kotlin could infer them.
The result is Kotlin code that's twice as long as it should be, misses Kotlin's safety features, and doesn't leverage the language's most powerful constructs. Your team chose Kotlin for a reason — AI rules ensure the AI writes the Kotlin you expect, not the Java you left behind.
These rules target both server-side Kotlin (Ktor, Spring Boot) and Android (Jetpack Compose). Adjust based on your platform — the core language rules apply everywhere.
Rule 1: Data Classes and Sealed Hierarchies
The rule: 'Use data class for all DTOs, events, and value objects — never write POJOs with manual equals/hashCode/toString/copy. Use sealed class or sealed interface for type hierarchies that represent a fixed set of variants. Use enum class for simple enumerations without associated data. Use value class for type-safe wrappers around primitives (UserId, Email).'
For state modeling: 'Model UI state and domain state with sealed hierarchies: sealed interface UiState { data object Loading : UiState; data class Success(val data: List<Item>) : UiState; data class Error(val message: String) : UiState }. Use exhaustive when expressions — the compiler ensures all variants are handled.'
This is the highest-impact Kotlin rule. A single data class replaces 30+ lines of Java boilerplate. AI assistants that don't use data classes are generating code at 3x the necessary length.
A single data class replaces 30+ lines of Java boilerplate (equals, hashCode, toString, copy, getters). If the AI generates a POJO in Kotlin, it's writing 3x more code than necessary.
Rule 2: Null Safety and Smart Casts
Kotlin's null safety system is one of its best features — every type is non-null by default, and nullable types (String?) require explicit handling. AI assistants sometimes bypass this with the !! operator (non-null assertion) which defeats the entire purpose.
The rule: 'Never use !! (non-null assertion) in production code — it throws NPE on null, defeating Kotlin's null safety. Use safe calls (?.), Elvis operator (?:), and let/also/run scope functions for null handling. Use requireNotNull() or checkNotNull() only at boundary validation points with descriptive messages. Prefer non-null types in function signatures — accept String?, return String (validate at the boundary).'
For smart casts: 'Use when with is for type checking — Kotlin smart-casts automatically. Use sealed hierarchies instead of instanceof chains. Never cast with as — use as? (safe cast) and handle the null case.'
The !! operator (non-null assertion) throws NPE on null — defeating Kotlin's null safety entirely. Use ?., ?:, and let instead. Reserve requireNotNull() for boundary validation only.
Rule 3: Coroutines and Structured Concurrency
Kotlin coroutines replace threads, callbacks, and RxJava for async operations. AI assistants frequently generate thread-based code or GlobalScope launches — both are anti-patterns in structured concurrency.
The rule: 'Use coroutines for all async operations. Never use GlobalScope — always launch coroutines in a structured scope (viewModelScope, lifecycleScope, or a custom CoroutineScope). Use suspend functions for sequential async operations. Use async/await for parallel operations within coroutineScope {}. Use Flow for reactive streams — never RxJava in new Kotlin code.'
For error handling in coroutines: 'Use try/catch inside coroutines for expected errors. Use CoroutineExceptionHandler for unexpected errors at the scope level. Use supervisorScope when child coroutine failures shouldn't cancel siblings.'
- Never GlobalScope — use viewModelScope, lifecycleScope, or custom scopes
- suspend functions for sequential async — async/await for parallel
- Flow for reactive streams — not RxJava in new code
- supervisorScope when child failures shouldn't cancel siblings
- withContext(Dispatchers.IO) for blocking I/O inside coroutines
- try/catch inside coroutines, CoroutineExceptionHandler at scope level
Rule 4: Jetpack Compose Patterns
If your project uses Jetpack Compose (most modern Android projects do), the AI needs specific rules to generate composable functions correctly. AI assistants frequently mix View-based XML layouts with Compose, mismanage state, and create composables that don't follow the unidirectional data flow pattern.
The rule: 'This is a Jetpack Compose project — never generate XML layouts or View-based code. Composable functions should be stateless where possible — hoist state to the caller. Use remember {} for composable-scoped state, rememberSaveable {} for state that survives configuration changes. Use derivedStateOf {} for computed state. Follow the state hoisting pattern: state down, events up.'
For architecture: 'Use ViewModel with StateFlow for screen-level state. Expose UI state as a single sealed class StateFlow, not multiple LiveData/StateFlow fields. Collect state in Compose with collectAsStateWithLifecycle(). Side effects use LaunchedEffect, DisposableEffect, or SideEffect — never launch coroutines directly in composable bodies.'
State down, events up. Composables should be stateless — hoist state to the caller. Use ViewModel + StateFlow for screen state, collectAsStateWithLifecycle() in Compose.
Rule 5: Kotlin Idioms and Extension Functions
Kotlin's standard library and language features enable concise, expressive code — but AI assistants often ignore these idioms, generating verbose alternatives that a Kotlin developer would never write.
The rule: 'Use scope functions appropriately: let for null-safe transformations, apply for object configuration, also for side effects, run for scoped computations, with for grouping calls on an object. Use extension functions for adding behavior to existing types without inheritance. Use destructuring declarations for data classes and Pairs. Use string templates ($variable and ${expression}) instead of concatenation.'
For collection operations: 'Use Kotlin's collection functions: map, filter, flatMap, groupBy, associate, partition, zip. Use firstOrNull() instead of find() when intent is clearer. Use buildList {}, buildMap {} for constructing collections conditionally. Use sequence {} for lazy evaluation of large collection chains.'
Complete Kotlin Rules Template
Consolidated template for Kotlin teams. Covers both server-side (Ktor/Spring) and Android (Compose) patterns.
- data class for DTOs/values — sealed class/interface for type hierarchies — value class for wrappers
- Never !! — use ?., ?:, let, requireNotNull() at boundaries only
- Coroutines in structured scopes — never GlobalScope, Flow over RxJava
- Compose: stateless composables, state hoisting, ViewModel + StateFlow
- collectAsStateWithLifecycle() — LaunchedEffect for side effects
- Scope functions: let for null-safe, apply for config, also for side effects
- Extension functions over utility classes — Kotlin idiom, not Java pattern
- Detekt for static analysis — ktlint for formatting — both in CI