kotlin-expert
Advanced Kotlin patterns for AmethystMultiplatform. Flow state management (StateFlow/SharedFlow), sealed hierarchies (classes vs interfaces), immutability (@Immutable, data classes), DSL builders (type-safe fluent APIs), inline functions (reified generics, performance). Use when working with: (1) State management patterns (StateFlow/SharedFlow/MutableStateFlow), (2) Sealed classes or sealed interfaces, (3) @Immutable annotations for Compose, (4) DSL builders with lambda receivers, (5) inline/reified functions, (6) Kotlin performance optimization. Complements kotlin-coroutines agent (async patterns) - this skill focuses on Amethyst-specific Kotlin idioms.
Install
mkdir -p .claude/skills/kotlin-expert && curl -L -o skill.zip "https://mcp.directory/api/skills/download/1141" && unzip -o skill.zip -d .claude/skills/kotlin-expert && rm skill.zipInstalls to .claude/skills/kotlin-expert
About this skill
Kotlin Expert
Advanced Kotlin patterns for AmethystMultiplatform. Covers Flow state management, sealed hierarchies, immutability, DSL builders, and inline functions with real codebase examples.
Mental Model
Kotlin in Amethyst:
State Management (Hot Flows)
├── StateFlow<T> # Single value, always has value, replays to new subscribers
├── SharedFlow<T> # Event stream, configurable replay, multiple subscribers
└── MutableStateFlow<T> # Private mutable, public via .asStateFlow()
Type Safety (Sealed Hierarchies)
├── sealed class # State variants with data (AccountState.LoggedIn/LoggedOut)
└── sealed interface # Generic result types (SignerResult<T>)
Compose Performance (@Immutable)
├── @Immutable # 173+ event classes - prevents recomposition
└── data class # Structural equality, copy(), immutable by convention
DSL Patterns
├── Builder classes # Fluent APIs (TagArrayBuilder)
├── Lambda receivers # inline fun tagArray { ... }
└── Method chaining # return this
Performance
├── inline fun # Eliminate lambda overhead
├── reified type params # Runtime type info (OptimizedJsonMapper)
└── value class # Zero-cost wrappers (NOT USED yet in Amethyst)
Delegation:
- kotlin-coroutines agent: Deep async (structured concurrency, channels, operators)
- kotlin-multiplatform skill: expect/actual, source sets
- This skill: Amethyst Kotlin idioms, state patterns, type safety
1. Flow State Management
StateFlow: State that Changes
Mental model: StateFlow is a "hot" observable state holder. Always has a value, new collectors immediately get current state.
Amethyst pattern:
// AccountManager.kt:48-50
class AccountManager {
private val _accountState = MutableStateFlow<AccountState>(AccountState.LoggedOut)
val accountState: StateFlow<AccountState> = _accountState.asStateFlow()
fun login(key: String) {
_accountState.value = AccountState.LoggedIn(...)
}
}
Key principles:
- Private mutable, public immutable:
_accountState(MutableStateFlow) private,accountState(StateFlow) public - Always has value: Initial value required (
LoggedOut) - Single value: Replays ONE most recent value to new subscribers
- Hot: Stays in memory, all collectors share same instance
See: AccountManager.kt:48-50, RelayConnectionManager.kt:49-52
SharedFlow: Event Streams
Mental model: SharedFlow is a "hot" broadcast stream for events. Configurable replay buffer, doesn't require initial value.
Amethyst pattern:
// RelayConnectionManager.kt:52-53
val connectedRelays: StateFlow<Set<NormalizedRelayUrl>> = client.connectedRelaysFlow()
val availableRelays: StateFlow<Set<NormalizedRelayUrl>> = client.availableRelaysFlow()
When to use StateFlow vs SharedFlow:
| Scenario | Use StateFlow | Use SharedFlow |
|---|---|---|
| UI state | ✅ Current screen data, login status | ❌ |
| One-time events | ❌ | ✅ Navigation, snackbars, toasts |
| Always has value | ✅ | ❌ Optional |
| Replay count | 1 (latest only) | Configurable (0, 1, n) |
| Backpressure | Conflates (drops old) | Configurable buffer |
Best practice:
// State: Use StateFlow
private val _uiState = MutableStateFlow(UiState.Loading)
val uiState: StateFlow<UiState> = _uiState.asStateFlow()
// Events: Use SharedFlow
private val _navigationEvents = MutableSharedFlow<NavEvent>(replay = 0)
val navigationEvents: SharedFlow<NavEvent> = _navigationEvents.asSharedFlow()
Flow Anti-Patterns
❌ Exposing mutable state:
val accountState: MutableStateFlow<AccountState> // BAD: Can be mutated externally
✅ Expose immutable:
val accountState: StateFlow<AccountState> = _accountState.asStateFlow() // GOOD
❌ SharedFlow for state:
val loginState = MutableSharedFlow<LoginState>() // BAD: State might get lost
✅ StateFlow for state:
val loginState = MutableStateFlow(LoginState.LoggedOut) // GOOD: Always has value
See: references/flow-patterns.md for comprehensive examples.
2. Sealed Hierarchies
Sealed Classes: State Variants
Mental model: Sealed classes represent a closed set of variants that share common data/behavior.
Amethyst pattern:
// AccountManager.kt:36-46
sealed class AccountState {
data object LoggedOut : AccountState()
data class LoggedIn(
val signer: NostrSigner,
val pubKeyHex: String,
val npub: String,
val nsec: String?,
val isReadOnly: Boolean
) : AccountState()
}
// Usage
when (state) {
is AccountState.LoggedOut -> showLogin()
is AccountState.LoggedIn -> showFeed(state.pubKeyHex)
} // Exhaustive - compiler enforces all cases
Key principles:
- Closed hierarchy: All subclasses known at compile-time
- Exhaustive when: Compiler ensures all cases handled
- Shared data: Sealed class can hold common properties
- Single inheritance: Subclass can't extend another class
When to use:
- Modeling UI states (Loading, Success, Error)
- Login states (LoggedOut, LoggedIn)
- Result types with different data per variant
Sealed Interfaces: Generic Result Types
Mental model: Sealed interfaces for contracts with multiple implementations that need generics or multiple inheritance.
Amethyst pattern:
// SignerResult.kt:25-46
sealed interface SignerResult<T : IResult> {
sealed interface RequestAddressed<T : IResult> : SignerResult<T> {
class Successful<T : IResult>(val result: T) : RequestAddressed<T>
class Rejected<T : IResult> : RequestAddressed<T>
class TimedOut<T : IResult> : RequestAddressed<T>
class ReceivedButCouldNotPerform<T : IResult>(
val message: String?
) : RequestAddressed<T>
}
}
// Usage with generics
fun handleResult(result: SignerResult<SignResult>) {
when (result) {
is SignerResult.RequestAddressed.Successful -> processEvent(result.result.event)
is SignerResult.RequestAddressed.Rejected -> showRejected()
is SignerResult.RequestAddressed.TimedOut -> showTimeout()
}
}
Key principles:
- Multiple inheritance: Subtype can implement other interfaces
- Variance: Supports
out/inmodifiers for generics - No constructor: Can't hold state directly (subtypes can)
- Nested hierarchies: Can create sub-sealed hierarchies
Sealed Class vs Sealed Interface
| Feature | Sealed Class | Sealed Interface |
|---|---|---|
| Constructor | ✅ Can hold common state | ❌ No constructor |
| Inheritance | ❌ Single parent only | ✅ Multiple interfaces |
| Generics | ❌ No variance | ✅ Covariance/contravariance |
| Use case | State variants | Result types, contracts |
Decision tree:
Need to hold common data in base?
YES → sealed class
NO → sealed interface
Need generics with variance (out/in)?
YES → sealed interface
NO → Either works
Subtypes need multiple inheritance?
YES → sealed interface
NO → Either works
Amethyst examples:
sealed class AccountState- state variants with different datasealed interface SignerResult<T>- generic result types with variance
See: references/sealed-class-catalog.md for all sealed types in quartz.
3. Immutability & Compose Performance
@Immutable Annotation
Mental model: @Immutable tells Compose "this value never changes after construction." Compose can skip recomposition if @Immutable object reference doesn't change.
Amethyst pattern:
// TextNoteEvent.kt:51-63
@Immutable
class TextNoteEvent(
id: HexKey,
pubKey: HexKey,
createdAt: Long,
tags: Array<Array<String>>,
content: String,
sig: HexKey
) : BaseThreadedEvent(id, pubKey, createdAt, KIND, tags, content, sig) {
// All properties immutable (val), no mutable state
}
Key principles:
- All properties immutable: Only
val, nevervar - No mutable collections: Use
ImmutableList,Array, notMutableList - Deep immutability: Nested objects also immutable
- Compose optimization: Skips recomposition if reference equals
Why it matters:
// Without @Immutable
@Composable
fun NoteCard(note: TextNoteEvent) { // Recomposes every time parent recomposes
Text(note.content)
}
// With @Immutable
@Composable
fun NoteCard(note: TextNoteEvent) { // Only recomposes if note reference changes
Text(note.content)
}
173+ @Immutable classes in quartz - all events immutable for Compose performance.
Data Classes & Immutability
Pattern:
@Immutable
data class RelayStatus(
val url: NormalizedRelayUrl,
val connected: Boolean,
val error: String? = null
) {
// Implicit: equals(), hashCode(), copy(), toString()
}
// Usage
val oldStatus = RelayStatus(url, connected = false)
val newStatus = oldStatus.copy(connected = true) // Immutable update
Key principles:
- Structural equality:
equals()compares properties, not reference - copy(): Create modified copies without mutating
- All properties in constructor: For proper
equals()/hashCode() - Prefer val: Make properties immutable
kotlinx.collections.immutable
Pattern:
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
// Instead of List (which could be mutable internally)
val relays: ImmutableList<String> = persistentListOf("wss://relay1.com", "wss://relay2.com")
// Add returns new instance
val updated = relays.add("wss://relay3.com") // relays unchanged, updated has 3 items
When to use:
- Compose state that needs collection
- Publicly exposed collections
- Shared state across threads
See: references/immutability-patterns.md
4. DSL Builders
Type-Safe Fluent APIs
Mental model: DSL (Domain-Specific Language) builders use lambda receivers and method chaining to create readable, type-safe APIs.
Amethyst pattern:
// TagArrayBuilder.kt:23-90
class TagArrayBuilder<T : IEvent> {
private val tagList = mutableMapOf<String, MutableList<Tag>>()
fun add(tag: Array<String>): TagArrayBuilder<T> {
if (tag.isEmpty() || tag[0].isEmpty()) return this
tagList.getOrPut(tag[0], ::mutableListOf).add(tag)
return this // Method chaining
}
fun remove(tagName: String): TagArrayBuilder<T> {
tagList.remove(tagName)
return this // Method chaining
}
fun build() = tagList.flatMap { it.value }.toTypedArray()
}
// Inline function with lambda receiver (line 90)
inline fun <T : Event> tagArray(initializer: TagArrayBuilder<T>.() -> Unit = {}): TagArray =
TagArrayBuilder<T>().apply(initializer).build()
Usage:
val tags = tagArray<TextNoteEvent> {
add(arrayOf("e", eventId, relay, "reply"))
add(arrayOf("p", pubkey))
remove("a") // Remove address tags
}
Key patterns:
- Method chaining: Return
thisfrom mutator methods - Lambda receiver:
TagArrayBuilder<T>.() -> Unit- lambda hasthis: TagArrayBuilder<T> - inline function: Eliminates lambda overhead
- apply(): Executes lambda with receiver, returns receiver
DSL Pattern Template
class MyBuilder {
private val items = mutableListOf<Item>()
fun add(item: Item): MyBuilder {
items.add(item)
return this
}
fun build(): Result = Result(items.toList())
}
inline fun myDsl(init: MyBuilder.() -> Unit): Result =
MyBuilder().apply(init).build()
// Usage
val result = myDsl {
add(Item("foo"))
add(Item("bar"))
}
Why inline?
- Eliminates lambda object allocation
- Enables
reifiedtype parameters - Better performance for frequently-called DSLs
See: references/dsl-builder-examples.md for more patterns.
5. Inline Functions & reified
inline fun: Eliminate Overhead
Mental model: inline copies function body to call site. No lambda object created, direct code insertion.
Pattern:
// Without inline
fun <T> measureTime(block: () -> T): T {
val start = System.currentTimeMillis()
val result = block() // Lambda object allocated
println("Time: ${System.currentTimeMillis() - start}ms")
return result
}
// With inline
inline fun <T> measureTime(block: () -> T): T {
val start = System.currentTimeMillis()
val result = block() // No allocation, code inlined
println("Time: ${System.currentTimeMillis() - start}ms")
return result
}
Benefits:
- Zero overhead: No lambda object allocation
- Non-local returns: Can
returnfrom outer function inside lambda - reified enabled: Access to type parameter at runtime
reified: Runtime Type Access
Mental model: reified makes generic type T available at runtime. Only works with inline.
Amethyst pattern:
// OptimizedJsonMapper.kt:48
expect object OptimizedJsonMapper {
inline fun <reified T : OptimizedSerializable> fromJsonTo(json: String): T
}
// Usage
val event: TextNoteEvent = OptimizedJsonMapper.fromJsonTo(jsonString)
// Compiler inlines and passes TextNoteEvent::class info
Without reified:
// Would need to pass class explicitly
fun <T> fromJson(json: String, clazz: KClass<T>): T {
return when (clazz) {
TextNoteEvent::class -> parseTextNote(json) as T
// ...
}
}
val event = fromJson(json, TextNoteEvent::class) // Verbose
With reified:
inline fun <reified T> fromJson(json: String): T {
return when (T::class) { // Can access T::class!
TextNoteEvent::class -> parseTextNote(json) as T
// ...
}
}
val event = fromJson<TextNoteEvent>(json) // Clean
noinline & crossinline
noinline: Prevent specific lambda from being inlined
inline fun foo(
inlined: () -> Unit,
noinline notInlined: () -> Unit // Can be stored, passed around
) {
inlined()
someFunction(notInlined) // Can pass to non-inline function
}
crossinline: Lambda can't do non-local returns
inline fun foo(crossinline block: () -> Unit) {
launch {
block() // OK: crossinline allows lambda in different context
}
}
6. Value Classes (Opportunity)
Mental model: value class is a compile-time wrapper with zero runtime overhead. Single property, no boxing.
Not currently used in Amethyst - potential optimization.
Pattern:
@JvmInline
value class EventId(val hex: String)
@JvmInline
value class PubKey(val hex: String)
// Type safety without runtime cost
fun fetchEvent(eventId: EventId): Event {
// eventId.hex accessed without wrapper object
}
val id = EventId("abc123")
fetchEvent(id) // Type safe
// fetchEvent(PubKey("xyz")) // Compile error!
When to use:
- Type safety for primitives (IDs, hex strings, timestamps)
- High-frequency allocations (event processing)
- Clear domain types without overhead
Restrictions:
- Single property only
- Must be
val - Can't have
initblock with logic - Inline at compile-time, may box in some cases
Amethyst opportunity:
// Current (String everywhere, no type safety)
fun fetchEvent(id: String): Event // Could pass wrong string
// With value class
@JvmInline value class EventId(val hex: String)
@JvmInline value class PubKeyHex(val hex: String)
@JvmInline value class Bech32(val encoded: String)
fun fetchEvent(id: EventId): Event // Type safe, zero cost
Common Patterns
Pattern: StateFlow State Management
class MyViewModel {
private val _state = MutableStateFlow(State.Initial)
val state: StateFlow<State> = _state.asStateFlow()
fun loadData() {
viewModelScope.launch {
_state.value = State.Loading
val result = repository.getData()
_state.value = when (result) {
is Success -> State.Success(result.data)
is Error -> State.Error(result.message)
}
}
}
}
sealed class State {
data object Initial : State()
data object Loading : State()
data class Success(val data: List<Item>) : State()
data class Error(val message: String) : State()
}
Pattern: Sealed Result with Generics
sealed interface Result<out T> {
data class Success<T>(val value: T) : Result<T>
data class Error(val exception: Exception) : Result<Nothing>
data object Loading : Result<Nothing>
}
// Use with variance
fun <T> fetchData(): Result<T> = ...
val userResult: Result<User> = fetchData()
val itemResult: Result<List<Item>> = fetchData()
Pattern: Immutable Event Builder
@Immutable
data class Event(
val id: String,
val kind: Int,
val content: String,
val tags: ImmutableList<Tag>
) {
companion object {
fun builder() = EventBuilder()
}
}
class EventBuilder {
private var id: String = ""
private var kind: Int = 1
private var content: String = ""
private val tags = mutableListOf<Tag>()
fun id(value: String) = apply { id = value }
fun kind(value: Int) = apply { kind = value }
fun content(value: String) = apply { content = value }
fun tag(tag: Tag) = apply { tags.add(tag) }
fun build() = Event(id, kind, content, tags.toImmutableList())
}
// Usage
val event = Event.builder()
.id("abc")
.kind(1)
.content("Hello")
.tag(Tag.P("pubkey"))
.build()
Delegation Guide
When to delegate:
| Topic | Delegate To | This Skill Covers |
|---|---|---|
| Structured concurrency, channels | kotlin-coroutines agent | Flow state patterns only |
| expect/actual, source sets | kotlin-multiplatform skill | Platform-agnostic Kotlin |
| General Compose patterns | compose-expert skill | @Immutable for performance |
| Build configuration | gradle-expert skill | - |
Ask kotlin-coroutines agent for:
- Advanced Flow operators (flatMapLatest, combine, zip)
- Channel patterns
- Structured concurrency (supervisorScope, coroutineScope)
- Error handling in coroutines
This skill teaches:
- StateFlow/SharedFlow state management
- Sealed hierarchies
- @Immutable for Compose
- DSL builders
- Inline/reified patterns
Anti-Patterns
❌ Mutable public state:
val accountState: MutableStateFlow<AccountState> // BAD
✅ Immutable public interface:
val accountState: StateFlow<AccountState> = _accountState.asStateFlow()
❌ Sealed class for generic results:
sealed class Result<T> { // BAD: Can't use variance
data class Success<T>(val value: T) : Result<T>()
}
✅ Sealed interface for generics:
sealed interface Result<out T> { // GOOD: Covariance
data class Success<T>(val value: T) : Result<T>
}
❌ Mutable properties in @Immutable class:
@Immutable
data class Event(
var content: String // BAD: var breaks immutability
)
✅ All val:
@Immutable
data class Event(
val content: String
)
❌ Passing class explicitly when reified available:
inline fun <T> parse(json: String, clazz: KClass<T>): T // BAD
✅ Use reified:
inline fun <reified T> parse(json: String): T // GOOD
Quick Reference
Flow Decision Tree
Need to expose state?
YES → StateFlow (always has value, single latest)
NO → Need events? → SharedFlow (optional replay, broadcast)
Need to mutate?
Internal only → MutableStateFlow (private)
Expose publicly → StateFlow via .asStateFlow()
Sealed Decision Tree
Need common data in base type?
YES → sealed class
NO → sealed interface
Need generics with variance?
YES → sealed interface
NO → Either works
Need multiple inheritance?
YES → sealed interface
NO → Either works
Inline Decision Tree
Passing lambda to function?
Called frequently? → inline (performance)
Need reified? → inline (required)
Need to store/pass lambda? → regular fun (can't inline)
Resources
Official Docs
- StateFlow and SharedFlow | Android Developers
- Sealed Classes | Kotlin Docs
- Inline Functions | Kotlin Docs
Bundled References
references/flow-patterns.md- StateFlow/SharedFlow examples from AccountManager, RelayManagerreferences/sealed-class-catalog.md- All sealed types in quartzreferences/dsl-builder-examples.md- TagArrayBuilder, other DSL patternsreferences/immutability-patterns.md- @Immutable usage, data classes, collections
Codebase Examples
- AccountManager.kt:36-50 - sealed class AccountState, StateFlow pattern
- RelayConnectionManager.kt:44-52 - StateFlow state management
- SignerResult.kt:25-46 - sealed interface with generics
- TextNoteEvent.kt:51-63 - @Immutable event class
- TagArrayBuilder.kt:23-90 - DSL builder pattern, inline function
- OptimizedJsonMapper.kt:48 - inline fun with reified
Version: 1.0.0 Last Updated: 2025-12-30 Codebase Reference: AmethystMultiplatform commit 258c4e011
More by vitorpamplona
View all →You might also like
flutter-development
aj-geddes
Build beautiful cross-platform mobile apps with Flutter and Dart. Covers widgets, state management with Provider/BLoC, navigation, API integration, and material design.
drawio-diagrams-enhanced
jgtolentino
Create professional draw.io (diagrams.net) diagrams in XML format (.drawio files) with integrated PMP/PMBOK methodologies, extensive visual asset libraries, and industry-standard professional templates. Use this skill when users ask to create flowcharts, swimlane diagrams, cross-functional flowcharts, org charts, network diagrams, UML diagrams, BPMN, project management diagrams (WBS, Gantt, PERT, RACI), risk matrices, stakeholder maps, or any other visual diagram in draw.io format. This skill includes access to custom shape libraries for icons, clipart, and professional symbols.
godot
bfollington
This skill should be used when working on Godot Engine projects. It provides specialized knowledge of Godot's file formats (.gd, .tscn, .tres), architecture patterns (component-based, signal-driven, resource-based), common pitfalls, validation tools, code templates, and CLI workflows. The `godot` command is available for running the game, validating scripts, importing resources, and exporting builds. Use this skill for tasks involving Godot game development, debugging scene/resource files, implementing game systems, or creating new Godot components.
nano-banana-pro
garg-aayush
Generate and edit images using Google's Nano Banana Pro (Gemini 3 Pro Image) API. Use when the user asks to generate, create, edit, modify, change, alter, or update images. Also use when user references an existing image file and asks to modify it in any way (e.g., "modify this image", "change the background", "replace X with Y"). Supports both text-to-image generation and image-to-image editing with configurable resolution (1K default, 2K, or 4K for high resolution). DO NOT read the image file first - use this skill directly with the --input-image parameter.
ui-ux-pro-max
nextlevelbuilder
"UI/UX design intelligence. 50 styles, 21 palettes, 50 font pairings, 20 charts, 8 stacks (React, Next.js, Vue, Svelte, SwiftUI, React Native, Flutter, Tailwind). Actions: plan, build, create, design, implement, review, fix, improve, optimize, enhance, refactor, check UI/UX code. Projects: website, landing page, dashboard, admin panel, e-commerce, SaaS, portfolio, blog, mobile app, .html, .tsx, .vue, .svelte. Elements: button, modal, navbar, sidebar, card, table, form, chart. Styles: glassmorphism, claymorphism, minimalism, brutalism, neumorphism, bento grid, dark mode, responsive, skeuomorphism, flat design. Topics: color palette, accessibility, animation, layout, typography, font pairing, spacing, hover, shadow, gradient."
rust-coding-skill
UtakataKyosui
Guides Claude in writing idiomatic, efficient, well-structured Rust code using proper data modeling, traits, impl organization, macros, and build-speed best practices.
Stay ahead of the MCP ecosystem
Get weekly updates on new skills and servers.