kotlin-patterns
Replace all `!!` with safe alternatives. `!!` circumvents compile-time null safety.
$ npx -y skills add notque/vexjoy-agent --agent claude-codeHow 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.mdKotlin 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 confuRead more
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 confuEssays and writing behind this toolkit live at vexjoy.com. AI agents skip steps. "Looks correct" replaces running tests. "Trivial change" replaces verification.
Repo: notque/vexjoy-agent
Other agents on vexjoy-agent.
- ansible-automation-engineer
Ansible automation: playbooks, roles, collections, Molecule testing, Vault security.
Open agent - modules
**Scope**: Module selection patterns, builtin vs command/shell decisions, collection modules, and version-specific module changes **Version range**: ansible-core 2.14+ / Ansible Collections (community.general 7.0+) **Generated**: 2026-04-04 — verify against current Ansible
Open agent - testing
**Scope**: Molecule test scenarios, ansible-lint rules, idempotency validation, and check-mode patterns **Version range**: Molecule 6.0+ / ansible-lint 6.0+ / ansible-core 2.14+ **Generated**: 2026-04-04 — verify against current Molecule and ansible-lint documentation
Open agent - base-instructions
Universal operational rules injected by /do at agent dispatch. Domain-specific rules live in each agent's .md file.
Open agent - communication-patterns
**Scope**: Failure modes in agent output style — over-reporting, self-congratulation, verbose narration, and hedging. Covers what to detect and how to fix each. **Version range**: all versions **Generated**: 2026-05-11
Open agent - combat-effects-upgrade
Zero-dependency combat visual upgrades: CSS particle replacement, Framer Motion combat juice, CSS 3D card transforms.
Open agent

