Skip to content

kotlin-patterns

Replace all `!!` with safe alternatives. `!!` circumvents compile-time null safety.

From plugin
vexjoy-agent
413198 skills198 agents10 commands86 hooks
Install
$ npx -y skills add notque/vexjoy-agent --agent claude-code

How it fires

How this agent gets triggered: by you, by Claude, or both.

  • Fires itselfAuto-invocation. Claude auto-loads it when your prompt matches the work.Auto-invocation is when the right skill fires by itself at the right moment, driven by a FLOW.md router and a hook, instead of you invoking it by name. It is the difference between a skill being installed and a skill actually getting used.Read the full definition →
  • You can call itInvoke it directly when you want it.

Context preview

The summary Claude sees to decide when to auto-load this agent.

Replace all `!!` with safe alternatives. `!!` circumvents compile-time null safety.

Agent definition

kotlin-patterns.md

Kotlin Patterns Reference

Null Safety

Replace all `!!` with safe alternatives. `!!` circumvents compile-time null safety.

Safe Alternatives to `!!`

| Situation | Instead of `!!` | Use | |-----------|----------------|-----| | Value might be null, provide default | `value!!` | `value ?: defaultValue` | | Null means skip/return | `value!!.doSomething()` | `value?.doSomething()` | | Null is a programming error | `value!!` | `requireNotNull(value) { "value must not be null: reason" }` | | Null check in initialization | `lateinit var x: T; x!!` | `checkNotNull(x) { "x not initialized" }` | | Nullable transform chain | `list.find { ... }!!.name` | `list.find { ... }?.name ?: throw NoSuchElementException(...)` |

// BAD -- bypasses null safety
val account = accountRepository.findById(id)!!
val label = config["display_name"]!!

// GOOD -- explicit, descriptive failure
val account = accountRepository.findById(id)
    ?: throw AccountNotFoundException("Account $id not found")
val label = requireNotNull(config["display_name"]) {
    "display_name must be present in config"
}

Java Interop Boundaries

Platform types must be annotated or guarded at the boundary:

// BAD -- platform type passes through silently
fun getHeader(request: HttpServletRequest): String {
    return request.getHeader("X-Request-Id") // String! -- platform type
}

// GOOD -- explicit boundary handling
fun getHeader(request: HttpServletRequest): String? {
    return request.getHeader("X-Request-Id") // explicitly nullable
}

// GOOD -- assert non-null with context
fun getRequiredHeader(request: HttpServletRequest): String {
    return requireNotNull(request.getHeader("X-Request-Id")) {
        "X-Request-Id header is required"
    }
}

**Detection**: `grep -rn '!!' src/` -- any match is a violation requiring immediate review.

---

Coroutines and Flow

Structured Concurrency

Launch within structured scopes (`viewModelScope`, `lifecycleScope`, explicit scopes). Never `GlobalScope` in production.

// BAD -- GlobalScope leaks coroutines
GlobalScope.launch { fetchData() }

// GOOD -- scoped to ViewModel lifecycle
class ProductViewModel(private val repository: ProductRepository) : ViewModel() {
    fun loadProducts() {
        viewModelScope.launch {
            _state.value = repository.getProducts()
        }
    }
}

// GOOD -- scoped in Ktor
fun Application.configureRouting() {
    routing {
        get("/products") {
            val products = coroutineScope {
                async { productService.getAll() }.await()
            }
            call.respond(products)
        }
    }
}

Dispatcher Selection

| Task Type | Dispatcher | Reason | |-----------|-----------|--------| | CPU-intensive computation | `Dispatchers.Default` | Thread pool sized to CPU cores | | Blocking I/O (JDBC, file) | `Dispatchers.IO` | Expandable thread pool for blocking | | Android UI updates | `Dispatchers.Main` | Main thread only | | Ktor request handling | Ktor manages dispatcher | Use `withContext(Dispatchers.IO)` for blocking calls |

// BAD -- blocking JDBC call on Default dispatcher starves CPU threads
suspend fun fetchRecord(id: Long): DbRecord = withContext(Dispatchers.Default) {
    database.find(id) // blocking JDBC
}

// GOOD -- blocking call on IO dispatcher
suspend fun fetchRecord(id: Long): DbRecord = withContext(Dispatchers.IO) {
    database.find(id)
}

Flow Patterns

// StateFlow for UI state with debounced search
class SearchViewModel(private val repo: ProductRepository) : ViewModel() {
    private val _query = MutableStateFlow("")
    val results: StateFlow<List<Product>> = _query
        .debounce(300)
        .distinctUntilChanged()
        .flatMapLatest { query -> repo.search(query) }
        .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), emptyList())

    fun onQueryChanged(query: String) { _query.value = query }
}

Testing Coroutines

Use `runTest` from `kotlinx-coroutines-test`, not `runBlocking`.

// BAD -- runBlocking in tests masks timing issues
@Test
fun `should return products`() = runBlocking {
    val result = viewModel.loadProducts()
    assertEquals(expected, result)
}

// GOOD -- runTest with TestDispatcher provides virtual time control
@Test
fun `should debounce search queries`() = runTest {
    val vm = SearchViewModel(fakeRepo)
    vm.onQueryChanged("ki")
    advanceTimeBy(200) // under debounce threshold
    assertEquals(emptyList(), vm.results.value)
    advanceTimeBy(200) // crosses 300ms threshold
    assertEquals(listOf(product), vm.results.value)
}

---

Sealed Classes, Enums, and Data Classes

Decision Matrix

| Need | Use | Reason | |------|-----|--------| | Fixed set of named constants, no data | `enum class` | Serializable, ordinal, `values()`, simple | | Fixed set of states, each with different data | `sealed class` / `sealed interface` | Exhaustive `when`, each subtype carries its own fields | | Named constant with associated behavior | `enum class` with abstract function | Enum entries can override | | Pure value/record type with structural equality | `data class` | `copy()`, `equals()`, `hashCode()`, destructuring | | Inline wrapper to avoid primitive confusion | `@JvmInline value class` | Zero-overhead at runtime | | Open hierarchy for external extension | `abstract class` or `interface` | Sealed prevents external subclassing |

// Use enum for simple constants
enum class Direction { NORTH, SOUTH, EAST, WEST }

// Use sealed class when variants carry different data
sealed class LoadResult<out T> {
    data class Success<T>(val value: T) : LoadResult<T>()
    data class Failure(val error: Throwable) : LoadResult<Nothing>()
    data object Loading : LoadResult<Nothing>()
}

// Use data class for records
data class UserId(val value: Long)
data class AppUser(val id: UserId, val name: String, val email: String)

// Use value class to avoid primitive confu
Read more
Ships withvexjoy-agent

Essays and writing behind this toolkit live at vexjoy.com. AI agents skip steps. "Looks correct" replaces running tests. "Trivial change" replaces verification.

Get the whole plugin, auto-invoked