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.
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)
Multiple inheritance: Subtype can implement other interfaces
Variance: Supports out/in modifiers 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 data
sealed 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.
No mutable collections: Use ImmutableList, Array, not MutableList
Deep immutability: Nested objects also immutable
Compose optimization: Skips recomposition if reference equals
Why it matters:
// Without @Immutable@ComposablefunNoteCard(note: TextNoteEvent) { // Recomposes every time parent recomposes
Text(note.content)
}
// With @Immutable@ComposablefunNoteCard(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:
@ImmutabledataclassRelayStatus(
val url: NormalizedRelayUrl,
val connected: Boolean,
val error: String? = null
) {
// Implicit: equals(), hashCode(), copy(), toString()
}
// Usageval 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 instanceval 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.
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 inlinefun<T>measureTime(block: () -> T): T {
val start = System.currentTimeMillis()
val result = block() // Lambda object allocated
println("Time: ${System.currentTimeMillis() - start}ms")
return result
}
// With inlineinlinefun<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 return from 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:48expectobject OptimizedJsonMapper {
inlinefun<reified T : OptimizedSerializable>fromJsonTo(json: String): T
}
// Usageval event: TextNoteEvent = OptimizedJsonMapper.fromJsonTo(jsonString)
// Compiler inlines and passes TextNoteEvent::class info
Without reified:
// Would need to pass class explicitlyfun<T>fromJson(json: String, clazz: KClass<T>): T {
returnwhen (clazz) {
TextNoteEvent::class -> parseTextNote(json) as T
// ...
}
}
val event = fromJson(json, TextNoteEvent::class) // Verbose
With reified:
inlinefun<reified T>fromJson(json: String): T {
returnwhen (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
inlinefunfoo(
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
inlinefunfoo(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 classEventId(val hex: String)
@JvmInline
value classPubKey(val hex: String)
// Type safety without runtime costfunfetchEvent(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 init block with logic
Inline at compile-time, may box in some cases
Amethyst opportunity:
// Current (String everywhere, no type safety)funfetchEvent(id: String): Event // Could pass wrong string// With value class@JvmInline value classEventId(val hex: String)
@JvmInline value classPubKeyHex(val hex: String)
@JvmInline value classBech32(val encoded: String)
funfetchEvent(id: EventId): Event // Type safe, zero cost
Common Patterns
Pattern: StateFlow State Management
classMyViewModel {
privateval _state = MutableStateFlow(State.Initial)
val state: StateFlow<State> = _state.asStateFlow()
funloadData() {
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)
}
}
}
}
sealedclassState {
dataobject Initial : State()
dataobject Loading : State()
dataclassSuccess(valdata: List<Item>) : State()
dataclassError(val message: String) : State()
}
Pattern: Sealed Result with Generics
sealedinterfaceResult<out T> {
dataclassSuccess<T>(val value: T) : Result<T>
dataclassError(val exception: Exception) : Result<Nothing>
dataobject Loading : Result<Nothing>
}
// Use with variancefun<T>fetchData(): Result<T> = ...
val userResult: Result<User> = fetchData()
val itemResult: Result<List<Item>> = fetchData()
@ImmutabledataclassEvent(
var content: String // BAD: var breaks immutability
)
✅ All val:
@ImmutabledataclassEvent(
val content: String
)
❌ Passing class explicitly when reified available:
inlinefun<T>parse(json: String, clazz: KClass<T>): T // BAD
✅ Use reified:
inlinefun<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)