initial skills
This commit is contained in:
@@ -0,0 +1,811 @@
|
||||
---
|
||||
name: kotlin-expert
|
||||
description: 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)
|
||||
- **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:**
|
||||
|
||||
```kotlin
|
||||
// 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:**
|
||||
1. **Private mutable, public immutable**: `_accountState` (MutableStateFlow) private, `accountState` (StateFlow) public
|
||||
2. **Always has value**: Initial value required (`LoggedOut`)
|
||||
3. **Single value**: Replays ONE most recent value to new subscribers
|
||||
4. **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:**
|
||||
|
||||
```kotlin
|
||||
// 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:**
|
||||
```kotlin
|
||||
// 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:**
|
||||
```kotlin
|
||||
val accountState: MutableStateFlow<AccountState> // BAD: Can be mutated externally
|
||||
```
|
||||
|
||||
✅ **Expose immutable:**
|
||||
```kotlin
|
||||
val accountState: StateFlow<AccountState> = _accountState.asStateFlow() // GOOD
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
❌ **SharedFlow for state:**
|
||||
```kotlin
|
||||
val loginState = MutableSharedFlow<LoginState>() // BAD: State might get lost
|
||||
```
|
||||
|
||||
✅ **StateFlow for state:**
|
||||
```kotlin
|
||||
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:**
|
||||
|
||||
```kotlin
|
||||
// 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:**
|
||||
1. **Closed hierarchy**: All subclasses known at compile-time
|
||||
2. **Exhaustive when**: Compiler ensures all cases handled
|
||||
3. **Shared data**: Sealed class can hold common properties
|
||||
4. **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:**
|
||||
|
||||
```kotlin
|
||||
// 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:**
|
||||
1. **Multiple inheritance**: Subtype can implement other interfaces
|
||||
2. **Variance**: Supports `out`/`in` modifiers for generics
|
||||
3. **No constructor**: Can't hold state directly (subtypes can)
|
||||
4. **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.
|
||||
|
||||
**Amethyst pattern:**
|
||||
|
||||
```kotlin
|
||||
// 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:**
|
||||
1. **All properties immutable**: Only `val`, never `var`
|
||||
2. **No mutable collections**: Use `ImmutableList`, `Array`, not `MutableList`
|
||||
3. **Deep immutability**: Nested objects also immutable
|
||||
4. **Compose optimization**: Skips recomposition if reference equals
|
||||
|
||||
**Why it matters:**
|
||||
|
||||
```kotlin
|
||||
// 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:**
|
||||
|
||||
```kotlin
|
||||
@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:**
|
||||
1. **Structural equality**: `equals()` compares properties, not reference
|
||||
2. **copy()**: Create modified copies without mutating
|
||||
3. **All properties in constructor**: For proper `equals()`/`hashCode()`
|
||||
4. **Prefer val**: Make properties immutable
|
||||
|
||||
### kotlinx.collections.immutable
|
||||
|
||||
**Pattern:**
|
||||
|
||||
```kotlin
|
||||
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:**
|
||||
|
||||
```kotlin
|
||||
// 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:**
|
||||
|
||||
```kotlin
|
||||
val tags = tagArray<TextNoteEvent> {
|
||||
add(arrayOf("e", eventId, relay, "reply"))
|
||||
add(arrayOf("p", pubkey))
|
||||
remove("a") // Remove address tags
|
||||
}
|
||||
```
|
||||
|
||||
**Key patterns:**
|
||||
1. **Method chaining**: Return `this` from mutator methods
|
||||
2. **Lambda receiver**: `TagArrayBuilder<T>.() -> Unit` - lambda has `this: TagArrayBuilder<T>`
|
||||
3. **inline function**: Eliminates lambda overhead
|
||||
4. **apply()**: Executes lambda with receiver, returns receiver
|
||||
|
||||
### DSL Pattern Template
|
||||
|
||||
```kotlin
|
||||
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 `reified` type 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:**
|
||||
|
||||
```kotlin
|
||||
// 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:**
|
||||
1. **Zero overhead**: No lambda object allocation
|
||||
2. **Non-local returns**: Can `return` from outer function inside lambda
|
||||
3. **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:**
|
||||
|
||||
```kotlin
|
||||
// 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:**
|
||||
|
||||
```kotlin
|
||||
// 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:**
|
||||
|
||||
```kotlin
|
||||
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
|
||||
|
||||
```kotlin
|
||||
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
|
||||
|
||||
```kotlin
|
||||
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:**
|
||||
|
||||
```kotlin
|
||||
@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 `init` block with logic
|
||||
- Inline at compile-time, may box in some cases
|
||||
|
||||
**Amethyst opportunity:**
|
||||
|
||||
```kotlin
|
||||
// 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
|
||||
|
||||
```kotlin
|
||||
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
|
||||
|
||||
```kotlin
|
||||
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
|
||||
|
||||
```kotlin
|
||||
@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:**
|
||||
```kotlin
|
||||
val accountState: MutableStateFlow<AccountState> // BAD
|
||||
```
|
||||
|
||||
✅ **Immutable public interface:**
|
||||
```kotlin
|
||||
val accountState: StateFlow<AccountState> = _accountState.asStateFlow()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
❌ **Sealed class for generic results:**
|
||||
```kotlin
|
||||
sealed class Result<T> { // BAD: Can't use variance
|
||||
data class Success<T>(val value: T) : Result<T>()
|
||||
}
|
||||
```
|
||||
|
||||
✅ **Sealed interface for generics:**
|
||||
```kotlin
|
||||
sealed interface Result<out T> { // GOOD: Covariance
|
||||
data class Success<T>(val value: T) : Result<T>
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
❌ **Mutable properties in @Immutable class:**
|
||||
```kotlin
|
||||
@Immutable
|
||||
data class Event(
|
||||
var content: String // BAD: var breaks immutability
|
||||
)
|
||||
```
|
||||
|
||||
✅ **All val:**
|
||||
```kotlin
|
||||
@Immutable
|
||||
data class Event(
|
||||
val content: String
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
❌ **Passing class explicitly when reified available:**
|
||||
```kotlin
|
||||
inline fun <T> parse(json: String, clazz: KClass<T>): T // BAD
|
||||
```
|
||||
|
||||
✅ **Use reified:**
|
||||
```kotlin
|
||||
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](https://developer.android.com/kotlin/flow/stateflow-and-sharedflow)
|
||||
- [Sealed Classes | Kotlin Docs](https://kotlinlang.org/docs/sealed-classes.html)
|
||||
- [Inline Functions | Kotlin Docs](https://kotlinlang.org/docs/inline-functions.html)
|
||||
|
||||
### Bundled References
|
||||
- `references/flow-patterns.md` - StateFlow/SharedFlow examples from AccountManager, RelayManager
|
||||
- `references/sealed-class-catalog.md` - All sealed types in quartz
|
||||
- `references/dsl-builder-examples.md` - TagArrayBuilder, other DSL patterns
|
||||
- `references/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
|
||||
@@ -0,0 +1,602 @@
|
||||
# DSL Builder Examples
|
||||
|
||||
Type-safe fluent APIs and DSL patterns from the codebase.
|
||||
|
||||
## Table of Contents
|
||||
- [TagArrayBuilder Pattern](#tagarraybuilder-pattern)
|
||||
- [Builder Variations](#builder-variations)
|
||||
- [DSL Principles](#dsl-principles)
|
||||
- [Creating Custom DSLs](#creating-custom-dsls)
|
||||
|
||||
---
|
||||
|
||||
## TagArrayBuilder Pattern
|
||||
|
||||
### Core Implementation
|
||||
|
||||
**File:** `quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/core/TagArrayBuilder.kt:23-91`
|
||||
|
||||
```kotlin
|
||||
class TagArrayBuilder<T : IEvent> {
|
||||
private val tagList = mutableMapOf<String, MutableList<Tag>>()
|
||||
|
||||
fun remove(tagName: String): TagArrayBuilder<T> {
|
||||
tagList.remove(tagName)
|
||||
return this // Method chaining
|
||||
}
|
||||
|
||||
fun remove(tagName: String, tagValue: String): TagArrayBuilder<T> {
|
||||
tagList[tagName]?.removeAll { it.valueOrNull() == tagValue }
|
||||
if (tagList[tagName]?.isEmpty() == true) {
|
||||
tagList.remove(tagName)
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
fun removeIf(
|
||||
predicate: (Tag, Tag) -> Boolean,
|
||||
toCompare: Tag
|
||||
): TagArrayBuilder<T> {
|
||||
val tagName = toCompare.nameOrNull() ?: return this
|
||||
tagList[tagName]?.removeAll { predicate(it, toCompare) }
|
||||
if (tagList[tagName]?.isEmpty() == true) {
|
||||
tagList.remove(tagName)
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
fun add(tag: Array<String>): TagArrayBuilder<T> {
|
||||
if (tag.isEmpty() || tag[0].isEmpty()) return this
|
||||
tagList.getOrPut(tag[0], ::mutableListOf).add(tag)
|
||||
return this
|
||||
}
|
||||
|
||||
fun addFirst(tag: Array<String>): TagArrayBuilder<T> {
|
||||
if (tag.isEmpty() || tag[0].isEmpty()) return this
|
||||
tagList.getOrPut(tag[0], ::mutableListOf).add(0, tag)
|
||||
return this
|
||||
}
|
||||
|
||||
fun addUnique(tag: Array<String>): TagArrayBuilder<T> {
|
||||
if (tag.isEmpty() || tag[0].isEmpty()) return this
|
||||
tagList[tag[0]] = mutableListOf(tag) // Replace existing
|
||||
return this
|
||||
}
|
||||
|
||||
fun addAll(tag: List<Array<String>>): TagArrayBuilder<T> {
|
||||
tag.forEach(::add)
|
||||
return this
|
||||
}
|
||||
|
||||
fun toTypedArray() = tagList.flatMap { it.value }.toTypedArray()
|
||||
|
||||
fun build() = toTypedArray()
|
||||
}
|
||||
|
||||
// Inline DSL function with lambda receiver
|
||||
inline fun <T : Event> tagArray(
|
||||
initializer: TagArrayBuilder<T>.() -> Unit = {}
|
||||
): TagArray = TagArrayBuilder<T>().apply(initializer).build()
|
||||
```
|
||||
|
||||
### Usage Examples
|
||||
|
||||
**Basic usage:**
|
||||
|
||||
```kotlin
|
||||
val tags = tagArray<TextNoteEvent> {
|
||||
add(arrayOf("e", eventId, relay, "reply"))
|
||||
add(arrayOf("p", pubkey))
|
||||
add(arrayOf("t", "bitcoin"))
|
||||
}
|
||||
```
|
||||
|
||||
**Advanced patterns:**
|
||||
|
||||
```kotlin
|
||||
// Remove and add
|
||||
val tags = tagArray<TextNoteEvent> {
|
||||
addAll(existingTags)
|
||||
remove("a") // Remove all address tags
|
||||
addUnique(arrayOf("client", "Amethyst")) // Replace client tag
|
||||
}
|
||||
|
||||
// Conditional building
|
||||
val tags = tagArray<TextNoteEvent> {
|
||||
add(arrayOf("e", rootId, "", "root"))
|
||||
|
||||
if (replyToId != null) {
|
||||
add(arrayOf("e", replyToId, "", "reply"))
|
||||
}
|
||||
|
||||
mentionedPubkeys.forEach { pubkey ->
|
||||
add(arrayOf("p", pubkey))
|
||||
}
|
||||
|
||||
hashtags.forEach { tag ->
|
||||
add(arrayOf("t", tag.lowercase()))
|
||||
}
|
||||
}
|
||||
|
||||
// Custom predicate removal
|
||||
val tags = tagArray<TextNoteEvent> {
|
||||
addAll(originalTags)
|
||||
removeIf(
|
||||
predicate = { tag, compare -> tag[1] == compare[1] },
|
||||
toCompare = arrayOf("e", eventIdToRemove)
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Builder Variations
|
||||
|
||||
### PrivateTagArrayBuilder
|
||||
|
||||
**File:** `quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/PrivateTagArrayBuilder.kt`
|
||||
|
||||
```kotlin
|
||||
class PrivateTagArrayBuilder {
|
||||
private val builder = TagArrayBuilder<Event>()
|
||||
|
||||
fun add(tag: PrivateTag): PrivateTagArrayBuilder {
|
||||
builder.add(tag.toArray())
|
||||
return this
|
||||
}
|
||||
|
||||
fun addAll(tags: List<PrivateTag>): PrivateTagArrayBuilder {
|
||||
tags.forEach { add(it) }
|
||||
return this
|
||||
}
|
||||
|
||||
fun build(): Array<Array<String>> = builder.build()
|
||||
}
|
||||
|
||||
// DSL function
|
||||
inline fun privateTagArray(
|
||||
initializer: PrivateTagArrayBuilder.() -> Unit
|
||||
): Array<Array<String>> = PrivateTagArrayBuilder().apply(initializer).build()
|
||||
```
|
||||
|
||||
**Usage:**
|
||||
|
||||
```kotlin
|
||||
val privateTags = privateTagArray {
|
||||
add(PrivateTag.Event(eventId, marker = "bookmark"))
|
||||
add(PrivateTag.Profile(pubkey))
|
||||
addAll(existingPrivateTags)
|
||||
}
|
||||
```
|
||||
|
||||
### TlvBuilder
|
||||
|
||||
**File:** `quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip19Bech32/tlv/TlvBuilder.kt`
|
||||
|
||||
```kotlin
|
||||
class TlvBuilder {
|
||||
private val entries = mutableListOf<TlvEntry>()
|
||||
|
||||
fun add(type: TlvType, value: ByteArray): TlvBuilder {
|
||||
entries.add(TlvEntry(type, value))
|
||||
return this
|
||||
}
|
||||
|
||||
fun addRelay(relay: String): TlvBuilder {
|
||||
add(TlvType.Relay, relay.encodeToByteArray())
|
||||
return this
|
||||
}
|
||||
|
||||
fun addAuthor(pubkey: ByteArray): TlvBuilder {
|
||||
add(TlvType.Author, pubkey)
|
||||
return this
|
||||
}
|
||||
|
||||
fun addKind(kind: Int): TlvBuilder {
|
||||
add(TlvType.Kind, kind.toByteArray())
|
||||
return this
|
||||
}
|
||||
|
||||
fun build(): ByteArray {
|
||||
return entries.flatMap { it.encode() }.toByteArray()
|
||||
}
|
||||
}
|
||||
|
||||
fun tlv(init: TlvBuilder.() -> Unit): ByteArray =
|
||||
TlvBuilder().apply(init).build()
|
||||
```
|
||||
|
||||
**Usage:**
|
||||
|
||||
```kotlin
|
||||
val tlvData = tlv {
|
||||
addAuthor(pubkeyBytes)
|
||||
addRelay("wss://relay.damus.io")
|
||||
addKind(1)
|
||||
}
|
||||
```
|
||||
|
||||
### MapOfSetBuilder
|
||||
|
||||
**File:** `quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/MapOfSetBuilder.kt`
|
||||
|
||||
```kotlin
|
||||
class MapOfSetBuilder<K, V> {
|
||||
private val map = mutableMapOf<K, MutableSet<V>>()
|
||||
|
||||
fun add(key: K, value: V): MapOfSetBuilder<K, V> {
|
||||
map.getOrPut(key) { mutableSetOf() }.add(value)
|
||||
return this
|
||||
}
|
||||
|
||||
fun addAll(key: K, values: Collection<V>): MapOfSetBuilder<K, V> {
|
||||
map.getOrPut(key) { mutableSetOf() }.addAll(values)
|
||||
return this
|
||||
}
|
||||
|
||||
fun remove(key: K, value: V): MapOfSetBuilder<K, V> {
|
||||
map[key]?.remove(value)
|
||||
if (map[key]?.isEmpty() == true) {
|
||||
map.remove(key)
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
fun build(): Map<K, Set<V>> = map.mapValues { it.value.toSet() }
|
||||
}
|
||||
|
||||
inline fun <K, V> mapOfSets(
|
||||
init: MapOfSetBuilder<K, V>.() -> Unit
|
||||
): Map<K, Set<V>> = MapOfSetBuilder<K, V>().apply(init).build()
|
||||
```
|
||||
|
||||
**Usage:**
|
||||
|
||||
```kotlin
|
||||
val relayMap = mapOfSets<String, EventId> {
|
||||
add("wss://relay1.com", eventId1)
|
||||
add("wss://relay1.com", eventId2)
|
||||
add("wss://relay2.com", eventId3)
|
||||
}
|
||||
// Result: {"wss://relay1.com": [eventId1, eventId2], "wss://relay2.com": [eventId3]}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## DSL Principles
|
||||
|
||||
### 1. Lambda with Receiver
|
||||
|
||||
**Mental model:** Lambda receiver makes `this` refer to builder instance inside lambda.
|
||||
|
||||
```kotlin
|
||||
// Without receiver
|
||||
fun buildTags(config: (TagArrayBuilder<Event>) -> Unit) {
|
||||
val builder = TagArrayBuilder<Event>()
|
||||
config(builder) // Must pass builder explicitly
|
||||
builder.build()
|
||||
}
|
||||
|
||||
buildTags { builder ->
|
||||
builder.add(...) // Verbose
|
||||
}
|
||||
|
||||
// With receiver
|
||||
inline fun buildTags(config: TagArrayBuilder<Event>.() -> Unit) {
|
||||
TagArrayBuilder<Event>().apply(config).build()
|
||||
}
|
||||
|
||||
buildTags {
|
||||
add(...) // Clean - 'this' is builder
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Method Chaining
|
||||
|
||||
**Pattern:** Return `this` from mutator methods.
|
||||
|
||||
```kotlin
|
||||
class Builder {
|
||||
private var value: String = ""
|
||||
|
||||
fun setValue(v: String): Builder {
|
||||
value = v
|
||||
return this // Enable chaining
|
||||
}
|
||||
|
||||
fun append(s: String): Builder {
|
||||
value += s
|
||||
return this
|
||||
}
|
||||
|
||||
fun build(): String = value
|
||||
}
|
||||
|
||||
// Usage
|
||||
val result = Builder()
|
||||
.setValue("Hello")
|
||||
.append(" ")
|
||||
.append("World")
|
||||
.build()
|
||||
```
|
||||
|
||||
### 3. Inline for Performance
|
||||
|
||||
**Why inline:**
|
||||
- Eliminates lambda allocation
|
||||
- Allows `reified` type parameters
|
||||
- Better for hot paths (frequently called)
|
||||
|
||||
```kotlin
|
||||
// NOT inline - lambda object created each call
|
||||
fun <T> myDsl(init: Builder<T>.() -> Unit): Result<T> {
|
||||
return Builder<T>().apply(init).build()
|
||||
}
|
||||
|
||||
// Inline - lambda code inlined at call site
|
||||
inline fun <T> myDsl(init: Builder<T>.() -> Unit): Result<T> {
|
||||
return Builder<T>().apply(init).build()
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Type Safety
|
||||
|
||||
**Use generics for compile-time safety:**
|
||||
|
||||
```kotlin
|
||||
// Type-safe builder
|
||||
class EventBuilder<T : Event> {
|
||||
fun addTag(tag: Tag<T>): EventBuilder<T> { // Only accepts tags for this event type
|
||||
tags.add(tag)
|
||||
return this
|
||||
}
|
||||
}
|
||||
|
||||
// Usage
|
||||
val textNote = EventBuilder<TextNoteEvent>()
|
||||
.addTag(TextNoteTag.Subject("Hello")) // OK
|
||||
// .addTag(ChannelTag.Name("test")) // Compile error!
|
||||
.build()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Creating Custom DSLs
|
||||
|
||||
### Pattern: Simple Builder DSL
|
||||
|
||||
```kotlin
|
||||
class QueryBuilder {
|
||||
private val filters = mutableListOf<String>()
|
||||
private var limit: Int? = null
|
||||
private var offset: Int? = null
|
||||
|
||||
fun filter(field: String, value: String): QueryBuilder {
|
||||
filters.add("$field:$value")
|
||||
return this
|
||||
}
|
||||
|
||||
fun limit(n: Int): QueryBuilder {
|
||||
limit = n
|
||||
return this
|
||||
}
|
||||
|
||||
fun offset(n: Int): QueryBuilder {
|
||||
offset = n
|
||||
return this
|
||||
}
|
||||
|
||||
fun build(): String {
|
||||
val parts = mutableListOf<String>()
|
||||
if (filters.isNotEmpty()) {
|
||||
parts.add(filters.joinToString(" AND "))
|
||||
}
|
||||
if (limit != null) {
|
||||
parts.add("LIMIT $limit")
|
||||
}
|
||||
if (offset != null) {
|
||||
parts.add("OFFSET $offset")
|
||||
}
|
||||
return parts.joinToString(" ")
|
||||
}
|
||||
}
|
||||
|
||||
inline fun query(init: QueryBuilder.() -> Unit): String =
|
||||
QueryBuilder().apply(init).build()
|
||||
|
||||
// Usage
|
||||
val sql = query {
|
||||
filter("status", "active")
|
||||
filter("age", ">18")
|
||||
limit(10)
|
||||
offset(20)
|
||||
}
|
||||
// Result: "status:active AND age:>18 LIMIT 10 OFFSET 20"
|
||||
```
|
||||
|
||||
### Pattern: Nested Builders
|
||||
|
||||
```kotlin
|
||||
class FilterBuilder {
|
||||
private val conditions = mutableListOf<String>()
|
||||
|
||||
fun equals(field: String, value: String) {
|
||||
conditions.add("$field = '$value'")
|
||||
}
|
||||
|
||||
fun greaterThan(field: String, value: Int) {
|
||||
conditions.add("$field > $value")
|
||||
}
|
||||
|
||||
fun build(): String = conditions.joinToString(" AND ")
|
||||
}
|
||||
|
||||
class QueryBuilder {
|
||||
private var filterClause: String = ""
|
||||
private var selectClause: String = "*"
|
||||
|
||||
fun select(vararg fields: String): QueryBuilder {
|
||||
selectClause = fields.joinToString(", ")
|
||||
return this
|
||||
}
|
||||
|
||||
fun where(init: FilterBuilder.() -> Unit): QueryBuilder {
|
||||
filterClause = FilterBuilder().apply(init).build()
|
||||
return this
|
||||
}
|
||||
|
||||
fun build(): String {
|
||||
return "SELECT $selectClause WHERE $filterClause"
|
||||
}
|
||||
}
|
||||
|
||||
inline fun query(init: QueryBuilder.() -> Unit): String =
|
||||
QueryBuilder().apply(init).build()
|
||||
|
||||
// Usage
|
||||
val sql = query {
|
||||
select("id", "name", "age")
|
||||
where {
|
||||
equals("status", "active")
|
||||
greaterThan("age", 18)
|
||||
}
|
||||
}
|
||||
// Result: "SELECT id, name, age WHERE status = 'active' AND age > 18"
|
||||
```
|
||||
|
||||
### Pattern: Type-Safe HTML DSL
|
||||
|
||||
```kotlin
|
||||
abstract class Tag(val name: String) {
|
||||
private val children = mutableListOf<Tag>()
|
||||
private val attributes = mutableMapOf<String, String>()
|
||||
|
||||
fun <T : Tag> tag(tag: T, init: T.() -> Unit): T {
|
||||
tag.init()
|
||||
children.add(tag)
|
||||
return tag
|
||||
}
|
||||
|
||||
fun attr(name: String, value: String) {
|
||||
attributes[name] = value
|
||||
}
|
||||
|
||||
fun render(builder: StringBuilder, indent: String) {
|
||||
builder.append("$indent<$name")
|
||||
attributes.forEach { (k, v) -> builder.append(" $k=\"$v\"") }
|
||||
if (children.isEmpty()) {
|
||||
builder.append("/>\n")
|
||||
} else {
|
||||
builder.append(">\n")
|
||||
children.forEach { it.render(builder, "$indent ") }
|
||||
builder.append("$indent</$name>\n")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class HTML : Tag("html")
|
||||
class Head : Tag("head")
|
||||
class Body : Tag("body")
|
||||
class Div : Tag("div")
|
||||
class P : Tag("p")
|
||||
class A : Tag("a")
|
||||
|
||||
fun HTML.head(init: Head.() -> Unit) = tag(Head(), init)
|
||||
fun HTML.body(init: Body.() -> Unit) = tag(Body(), init)
|
||||
fun Body.div(init: Div.() -> Unit) = tag(Div(), init)
|
||||
fun Div.p(init: P.() -> Unit) = tag(P(), init)
|
||||
fun Div.a(init: A.() -> Unit) = tag(A(), init)
|
||||
|
||||
fun html(init: HTML.() -> Unit): HTML = HTML().apply(init)
|
||||
|
||||
// Usage
|
||||
val page = html {
|
||||
head {
|
||||
// ...
|
||||
}
|
||||
body {
|
||||
div {
|
||||
attr("class", "container")
|
||||
p {
|
||||
attr("id", "intro")
|
||||
}
|
||||
a {
|
||||
attr("href", "https://example.com")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Best Practices
|
||||
|
||||
### ✅ DO
|
||||
|
||||
1. **Return `this` for chaining:**
|
||||
```kotlin
|
||||
fun add(item: Item): Builder {
|
||||
items.add(item)
|
||||
return this
|
||||
}
|
||||
```
|
||||
|
||||
2. **Use `inline` for DSL functions:**
|
||||
```kotlin
|
||||
inline fun myDsl(init: Builder.() -> Unit) = Builder().apply(init).build()
|
||||
```
|
||||
|
||||
3. **Provide sensible defaults:**
|
||||
```kotlin
|
||||
inline fun query(
|
||||
init: QueryBuilder.() -> Unit = {} // Empty lambda as default
|
||||
) = QueryBuilder().apply(init).build()
|
||||
```
|
||||
|
||||
4. **Validate in `build()`:**
|
||||
```kotlin
|
||||
fun build(): Result {
|
||||
require(fields.isNotEmpty()) { "Must specify at least one field" }
|
||||
return Result(fields)
|
||||
}
|
||||
```
|
||||
|
||||
### ❌ DON'T
|
||||
|
||||
1. **Forget to return `this`:**
|
||||
```kotlin
|
||||
fun add(item: Item) { // BAD: Can't chain
|
||||
items.add(item)
|
||||
}
|
||||
```
|
||||
|
||||
2. **Mutate after build:**
|
||||
```kotlin
|
||||
val builder = Builder()
|
||||
builder.add("foo")
|
||||
val result = builder.build()
|
||||
builder.add("bar") // BAD: Confusing state
|
||||
```
|
||||
|
||||
3. **Expose mutable state:**
|
||||
```kotlin
|
||||
class Builder {
|
||||
val items = mutableListOf<Item>() // BAD: Can be mutated externally
|
||||
}
|
||||
```
|
||||
|
||||
4. **Make DSL functions non-inline unnecessarily:**
|
||||
```kotlin
|
||||
fun myDsl(init: Builder.() -> Unit) = ... // BAD: Lambda allocation overhead
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## References
|
||||
|
||||
- TagArrayBuilder.kt:23-91
|
||||
- PrivateTagArrayBuilder.kt
|
||||
- TlvBuilder.kt
|
||||
- [Type-Safe Builders | Kotlin Docs](https://kotlinlang.org/docs/type-safe-builders.html)
|
||||
- [DSLs with Kotlin](https://kt.academy/article/dsl-intro)
|
||||
@@ -0,0 +1,405 @@
|
||||
# Flow Patterns in Amethyst
|
||||
|
||||
StateFlow and SharedFlow usage patterns from the codebase.
|
||||
|
||||
## Table of Contents
|
||||
- [StateFlow for State Management](#stateflow-for-state-management)
|
||||
- [Flow Composition](#flow-composition)
|
||||
- [Common Patterns](#common-patterns)
|
||||
- [Anti-Patterns](#anti-patterns)
|
||||
|
||||
---
|
||||
|
||||
## StateFlow for State Management
|
||||
|
||||
### AccountManager Pattern
|
||||
|
||||
**File:** `commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/account/AccountManager.kt:36-115`
|
||||
|
||||
```kotlin
|
||||
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()
|
||||
}
|
||||
|
||||
class AccountManager {
|
||||
private val _accountState = MutableStateFlow<AccountState>(AccountState.LoggedOut)
|
||||
val accountState: StateFlow<AccountState> = _accountState.asStateFlow()
|
||||
|
||||
fun generateNewAccount(): AccountState.LoggedIn {
|
||||
val keyPair = KeyPair()
|
||||
val signer = NostrSignerInternal(keyPair)
|
||||
|
||||
val state = AccountState.LoggedIn(
|
||||
signer = signer,
|
||||
pubKeyHex = keyPair.pubKey.toHexKey(),
|
||||
npub = keyPair.pubKey.toNpub(),
|
||||
nsec = keyPair.privKey?.toNsec(),
|
||||
isReadOnly = false
|
||||
)
|
||||
_accountState.value = state // Update state
|
||||
return state
|
||||
}
|
||||
|
||||
fun loginWithKey(keyInput: String): Result<AccountState.LoggedIn> {
|
||||
// ... validation ...
|
||||
|
||||
val state = AccountState.LoggedIn(...)
|
||||
_accountState.value = state
|
||||
return Result.success(state)
|
||||
}
|
||||
|
||||
fun logout() {
|
||||
_accountState.value = AccountState.LoggedOut
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Pattern highlights:**
|
||||
- Private `MutableStateFlow` for internal mutations
|
||||
- Public `StateFlow` via `.asStateFlow()` for read-only access
|
||||
- Sealed class for type-safe state variants
|
||||
- Initial value required (`AccountState.LoggedOut`)
|
||||
|
||||
### RelayConnectionManager Pattern
|
||||
|
||||
**File:** `commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/network/RelayConnectionManager.kt:44-80`
|
||||
|
||||
```kotlin
|
||||
data class RelayStatus(
|
||||
val url: NormalizedRelayUrl,
|
||||
val connected: Boolean,
|
||||
val error: String? = null,
|
||||
val messageCount: Int = 0
|
||||
)
|
||||
|
||||
open class RelayConnectionManager(
|
||||
websocketBuilder: WebsocketBuilder
|
||||
) : IRelayClientListener {
|
||||
private val client = NostrClient(websocketBuilder)
|
||||
|
||||
// Map of relay URLs to their status
|
||||
private val _relayStatuses = MutableStateFlow<Map<NormalizedRelayUrl, RelayStatus>>(emptyMap())
|
||||
val relayStatuses: StateFlow<Map<NormalizedRelayUrl, RelayStatus>> = _relayStatuses.asStateFlow()
|
||||
|
||||
// Delegated StateFlows from client
|
||||
val connectedRelays: StateFlow<Set<NormalizedRelayUrl>> = client.connectedRelaysFlow()
|
||||
val availableRelays: StateFlow<Set<NormalizedRelayUrl>> = client.availableRelaysFlow()
|
||||
|
||||
fun addRelay(url: String): NormalizedRelayUrl? {
|
||||
val normalized = RelayUrlNormalizer.normalizeOrNull(url) ?: return null
|
||||
updateRelayStatus(normalized) { it.copy(connected = false, error = null) }
|
||||
return normalized
|
||||
}
|
||||
|
||||
fun removeRelay(url: NormalizedRelayUrl) {
|
||||
_relayStatuses.value = _relayStatuses.value - url // Immutable update (remove from map)
|
||||
}
|
||||
|
||||
private fun updateRelayStatus(
|
||||
relay: NormalizedRelayUrl,
|
||||
update: (RelayStatus) -> RelayStatus
|
||||
) {
|
||||
_relayStatuses.value = _relayStatuses.value.toMutableMap().apply {
|
||||
val current = get(relay) ?: RelayStatus(relay, false)
|
||||
put(relay, update(current))
|
||||
}
|
||||
}
|
||||
|
||||
// IRelayClientListener implementation
|
||||
override fun onConnect(relay: NormalizedRelayUrl) {
|
||||
updateRelayStatus(relay) { it.copy(connected = true, error = null) }
|
||||
}
|
||||
|
||||
override fun onError(relay: NormalizedRelayUrl, error: String) {
|
||||
updateRelayStatus(relay) { it.copy(connected = false, error = error) }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Pattern highlights:**
|
||||
- `Map` as state value for collection tracking
|
||||
- Immutable map updates (copy with modifications)
|
||||
- Helper function `updateRelayStatus` for consistent updates
|
||||
- Delegation pattern (client exposes its own StateFlows)
|
||||
|
||||
---
|
||||
|
||||
## Flow Composition
|
||||
|
||||
### Multiple StateFlows in UI
|
||||
|
||||
**Pattern:**
|
||||
|
||||
```kotlin
|
||||
@Composable
|
||||
fun LoginScreen(accountManager: AccountManager) {
|
||||
val accountState by accountManager.accountState.collectAsState()
|
||||
|
||||
when (accountState) {
|
||||
is AccountState.LoggedOut -> {
|
||||
LoginForm(onLogin = { key -> accountManager.loginWithKey(key) })
|
||||
}
|
||||
is AccountState.LoggedIn -> {
|
||||
MainApp(account = accountState as AccountState.LoggedIn)
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Observing Multiple Flows
|
||||
|
||||
**Pattern:**
|
||||
|
||||
```kotlin
|
||||
@Composable
|
||||
fun RelayStatusCard(relayManager: RelayConnectionManager) {
|
||||
val relayStatuses by relayManager.relayStatuses.collectAsState()
|
||||
val connectedRelays by relayManager.connectedRelays.collectAsState()
|
||||
|
||||
Column {
|
||||
Text("${connectedRelays.size} of ${relayStatuses.size} relays connected")
|
||||
|
||||
relayStatuses.forEach { (url, status) ->
|
||||
RelayRow(
|
||||
url = url,
|
||||
connected = status.connected,
|
||||
error = status.error
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Pattern: Immutable State Updates
|
||||
|
||||
```kotlin
|
||||
// Map updates
|
||||
_relayStatuses.value = _relayStatuses.value + (url to newStatus) // Add
|
||||
_relayStatuses.value = _relayStatuses.value - url // Remove
|
||||
_relayStatuses.value = _relayStatuses.value.mapValues { (key, value) ->
|
||||
if (key == targetUrl) value.copy(connected = true) else value
|
||||
}
|
||||
|
||||
// List updates
|
||||
_items.value = _items.value + newItem // Append
|
||||
_items.value = _items.value.filter { it.id != removedId } // Remove
|
||||
_items.value = _items.value.map { if (it.id == id) it.copy(name = newName) else it } // Update
|
||||
|
||||
// Object updates
|
||||
_user.value = _user.value.copy(name = newName)
|
||||
```
|
||||
|
||||
### Pattern: Conditional State Transitions
|
||||
|
||||
```kotlin
|
||||
fun attemptLogin(credentials: Credentials) {
|
||||
if (_loginState.value is LoginState.LoggingIn) {
|
||||
return // Already logging in, ignore
|
||||
}
|
||||
|
||||
_loginState.value = LoginState.LoggingIn
|
||||
viewModelScope.launch {
|
||||
try {
|
||||
val user = repository.login(credentials)
|
||||
_loginState.value = LoginState.Success(user)
|
||||
} catch (e: Exception) {
|
||||
_loginState.value = LoginState.Error(e.message ?: "Login failed")
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Pattern: Derived State
|
||||
|
||||
```kotlin
|
||||
class MyViewModel {
|
||||
private val _items = MutableStateFlow<List<Item>>(emptyList())
|
||||
val items: StateFlow<List<Item>> = _items.asStateFlow()
|
||||
|
||||
// Derived state (computed from items)
|
||||
val itemCount: StateFlow<Int> = items.map { it.size }
|
||||
.stateIn(viewModelScope, SharingStarted.Lazily, 0)
|
||||
|
||||
val hasItems: StateFlow<Boolean> = items.map { it.isNotEmpty() }
|
||||
.stateIn(viewModelScope, SharingStarted.Lazily, false)
|
||||
}
|
||||
|
||||
// Usage in Compose
|
||||
@Composable
|
||||
fun ItemList(viewModel: MyViewModel) {
|
||||
val itemCount by viewModel.itemCount.collectAsState()
|
||||
val hasItems by viewModel.hasItems.collectAsState()
|
||||
|
||||
if (hasItems) {
|
||||
Text("$itemCount items")
|
||||
} else {
|
||||
Text("No items")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Pattern: State with Loading/Error
|
||||
|
||||
```kotlin
|
||||
sealed class UiState<out T> {
|
||||
data object Loading : UiState<Nothing>()
|
||||
data class Success<T>(val data: T) : UiState<T>()
|
||||
data class Error(val message: String) : UiState<Nothing>()
|
||||
}
|
||||
|
||||
class FeedViewModel {
|
||||
private val _feedState = MutableStateFlow<UiState<List<Event>>>(UiState.Loading)
|
||||
val feedState: StateFlow<UiState<List<Event>>> = _feedState.asStateFlow()
|
||||
|
||||
fun loadFeed() {
|
||||
viewModelScope.launch {
|
||||
_feedState.value = UiState.Loading
|
||||
try {
|
||||
val events = repository.getEvents()
|
||||
_feedState.value = UiState.Success(events)
|
||||
} catch (e: Exception) {
|
||||
_feedState.value = UiState.Error(e.message ?: "Unknown error")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// UI
|
||||
@Composable
|
||||
fun FeedScreen(viewModel: FeedViewModel) {
|
||||
val state by viewModel.feedState.collectAsState()
|
||||
|
||||
when (state) {
|
||||
is UiState.Loading -> LoadingSpinner()
|
||||
is UiState.Success -> EventList((state as UiState.Success).data)
|
||||
is UiState.Error -> ErrorMessage((state as UiState.Error).message)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Anti-Patterns
|
||||
|
||||
### ❌ Exposing Mutable State
|
||||
|
||||
```kotlin
|
||||
// BAD: External code can mutate
|
||||
class BadViewModel {
|
||||
val state: MutableStateFlow<State> = MutableStateFlow(State.Initial)
|
||||
}
|
||||
|
||||
// Caller can do:
|
||||
viewModel.state.value = State.Hacked // Bypass internal logic!
|
||||
```
|
||||
|
||||
### ✅ Expose Immutable
|
||||
|
||||
```kotlin
|
||||
// GOOD: Only ViewModel can mutate
|
||||
class GoodViewModel {
|
||||
private val _state = MutableStateFlow(State.Initial)
|
||||
val state: StateFlow<State> = _state.asStateFlow()
|
||||
|
||||
fun updateState(newState: State) {
|
||||
// Controlled mutation with validation
|
||||
_state.value = newState
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### ❌ Not Using Immutable Updates
|
||||
|
||||
```kotlin
|
||||
// BAD: Mutating collection doesn't trigger StateFlow update
|
||||
val list = mutableListOf<Item>()
|
||||
list.add(newItem)
|
||||
_items.value = list // Same reference, no update emitted!
|
||||
```
|
||||
|
||||
### ✅ Create New Instance
|
||||
|
||||
```kotlin
|
||||
// GOOD: New list instance
|
||||
_items.value = _items.value + newItem // New list created, update emitted
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### ❌ StateFlow for Events
|
||||
|
||||
```kotlin
|
||||
// BAD: Events get lost if no collector
|
||||
class BadViewModel {
|
||||
val navigationEvent: StateFlow<NavEvent?> = MutableStateFlow(null)
|
||||
|
||||
fun navigate(event: NavEvent) {
|
||||
_navigationEvent.value = event // Lost if UI not observing!
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### ✅ SharedFlow for Events
|
||||
|
||||
```kotlin
|
||||
// GOOD: Events queued
|
||||
class GoodViewModel {
|
||||
private val _navigationEvent = MutableSharedFlow<NavEvent>(replay = 0)
|
||||
val navigationEvent: SharedFlow<NavEvent> = _navigationEvent.asSharedFlow()
|
||||
|
||||
fun navigate(event: NavEvent) {
|
||||
viewModelScope.launch {
|
||||
_navigationEvent.emit(event) // Queued for collector
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### ❌ Blocking Operations in State Update
|
||||
|
||||
```kotlin
|
||||
// BAD: Blocking main thread
|
||||
fun loadData() {
|
||||
_state.value = fetchDataFromNetwork() // Blocks!
|
||||
}
|
||||
```
|
||||
|
||||
### ✅ Async Updates
|
||||
|
||||
```kotlin
|
||||
// GOOD: Use coroutines
|
||||
fun loadData() {
|
||||
viewModelScope.launch {
|
||||
_state.value = UiState.Loading
|
||||
val data = withContext(Dispatchers.IO) {
|
||||
fetchDataFromNetwork()
|
||||
}
|
||||
_state.value = UiState.Success(data)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## References
|
||||
|
||||
- AccountManager.kt:36-115
|
||||
- RelayConnectionManager.kt:44-80
|
||||
- [StateFlow and SharedFlow | Android Developers](https://developer.android.com/kotlin/flow/stateflow-and-sharedflow)
|
||||
- [Hot vs Cold Flows](https://carrion.dev/en/posts/kotlin-flows-hot-cold/)
|
||||
@@ -0,0 +1,641 @@
|
||||
# Immutability Patterns
|
||||
|
||||
@Immutable annotation, data classes, and immutable collections for Compose performance.
|
||||
|
||||
## Table of Contents
|
||||
- [Why Immutability Matters](#why-immutability-matters)
|
||||
- [@Immutable Annotation](#immutable-annotation)
|
||||
- [Data Classes](#data-classes)
|
||||
- [Immutable Collections](#immutable-collections)
|
||||
- [Common Patterns](#common-patterns)
|
||||
- [Performance Impact](#performance-impact)
|
||||
|
||||
---
|
||||
|
||||
## Why Immutability Matters
|
||||
|
||||
### Compose Recomposition
|
||||
|
||||
**Mental model:** Compose tracks state changes by comparing references. If an `@Immutable` object reference doesn't change, Compose skips recomposition.
|
||||
|
||||
```kotlin
|
||||
// Without @Immutable - Recomposes on every parent recomposition
|
||||
data class User(val name: String, val age: Int)
|
||||
|
||||
@Composable
|
||||
fun UserCard(user: User) { // Recomposes unnecessarily
|
||||
Text(user.name)
|
||||
}
|
||||
|
||||
// With @Immutable - Only recomposes when user reference changes
|
||||
@Immutable
|
||||
data class User(val name: String, val age: Int)
|
||||
|
||||
@Composable
|
||||
fun UserCard(user: User) { // Smart recomposition
|
||||
Text(user.name)
|
||||
}
|
||||
```
|
||||
|
||||
**Performance difference:**
|
||||
- Without `@Immutable`: 1000 `UserCard` recompositions per screen update
|
||||
- With `@Immutable`: 10 `UserCard` recompositions (only changed users)
|
||||
|
||||
### Thread Safety
|
||||
|
||||
Immutable objects are inherently thread-safe:
|
||||
|
||||
```kotlin
|
||||
@Immutable
|
||||
data class Event(
|
||||
val id: String,
|
||||
val content: String,
|
||||
val createdAt: Long
|
||||
)
|
||||
|
||||
// Safe to share across coroutines without synchronization
|
||||
val sharedEvent: Event = fetchEvent()
|
||||
launch { processEvent(sharedEvent) } // Safe
|
||||
launch { saveEvent(sharedEvent) } // Safe
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## @Immutable Annotation
|
||||
|
||||
### Basic Usage
|
||||
|
||||
**Pattern from Amethyst:**
|
||||
|
||||
```kotlin
|
||||
// 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 are val (immutable)
|
||||
// No var properties
|
||||
// No mutable collections
|
||||
}
|
||||
```
|
||||
|
||||
**Requirements for @Immutable:**
|
||||
1. All properties must be `val` (no `var`)
|
||||
2. All property types must be immutable or primitives
|
||||
3. No mutable collections (`MutableList`, `MutableMap`)
|
||||
4. Arrays are allowed (treated as immutable by contract)
|
||||
5. No public mutable state
|
||||
|
||||
### @Immutable vs @Stable
|
||||
|
||||
**@Immutable:** Value never changes after construction
|
||||
|
||||
```kotlin
|
||||
@Immutable
|
||||
data class User(val name: String, val age: Int)
|
||||
// Once created, user.name and user.age never change
|
||||
```
|
||||
|
||||
**@Stable:** Value can change, but changes are tracked
|
||||
|
||||
```kotlin
|
||||
@Stable
|
||||
class MutableCounter {
|
||||
var count by mutableStateOf(0) // Changes tracked by Compose
|
||||
}
|
||||
```
|
||||
|
||||
**Amethyst uses @Immutable extensively:**
|
||||
- 173+ event classes annotated with `@Immutable`
|
||||
- All Nostr events immutable by design
|
||||
- Critical for feed performance (thousands of events)
|
||||
|
||||
---
|
||||
|
||||
## Data Classes
|
||||
|
||||
### Immutable Data Classes
|
||||
|
||||
**Pattern:**
|
||||
|
||||
```kotlin
|
||||
@Immutable
|
||||
data class RelayStatus(
|
||||
val url: NormalizedRelayUrl,
|
||||
val connected: Boolean,
|
||||
val error: String? = null,
|
||||
val messageCount: Int = 0
|
||||
) {
|
||||
// Immutable properties only (val)
|
||||
// Default values allowed
|
||||
}
|
||||
```
|
||||
|
||||
**Benefits:**
|
||||
1. **Structural equality:** `equals()` compares values, not references
|
||||
2. **copy():** Create modified copies without mutation
|
||||
3. **toString():** Debugging-friendly output
|
||||
4. **hashCode():** Consistent hashing for collections
|
||||
5. **componentN():** Destructuring support
|
||||
|
||||
### copy() for Updates
|
||||
|
||||
**Mental model:** Instead of mutating, create modified copies.
|
||||
|
||||
```kotlin
|
||||
val status = RelayStatus(
|
||||
url = "wss://relay.damus.io",
|
||||
connected = false,
|
||||
error = null
|
||||
)
|
||||
|
||||
// Immutable update
|
||||
val updatedStatus = status.copy(connected = true)
|
||||
|
||||
// Original unchanged
|
||||
assert(status.connected == false)
|
||||
assert(updatedStatus.connected == true)
|
||||
```
|
||||
|
||||
**StateFlow pattern:**
|
||||
|
||||
```kotlin
|
||||
private val _relayStatuses = MutableStateFlow<Map<String, RelayStatus>>(emptyMap())
|
||||
|
||||
fun updateRelay(url: String, connected: Boolean) {
|
||||
_relayStatuses.value = _relayStatuses.value.mapValues { (key, status) ->
|
||||
if (key == url) {
|
||||
status.copy(connected = connected) // Immutable update
|
||||
} else {
|
||||
status
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### All Properties in Constructor
|
||||
|
||||
**Why important for data classes:**
|
||||
|
||||
```kotlin
|
||||
// BAD: Properties outside constructor not included in equals/hashCode
|
||||
data class User(val name: String) {
|
||||
var age: Int = 0 // NOT in equals/hashCode/copy!
|
||||
}
|
||||
|
||||
val user1 = User("Alice")
|
||||
val user2 = User("Alice")
|
||||
user1.age = 25
|
||||
user2.age = 30
|
||||
|
||||
assert(user1 == user2) // TRUE! age not compared
|
||||
assert(user1.copy() == user1) // TRUE! age not copied
|
||||
|
||||
// GOOD: All properties in constructor
|
||||
@Immutable
|
||||
data class User(
|
||||
val name: String,
|
||||
val age: Int // Included in equals/hashCode/copy
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Immutable Collections
|
||||
|
||||
### kotlinx.collections.immutable
|
||||
|
||||
**Installation:**
|
||||
|
||||
```kotlin
|
||||
// build.gradle.kts
|
||||
dependencies {
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-collections-immutable:0.3.7")
|
||||
}
|
||||
```
|
||||
|
||||
**Why use:**
|
||||
- Structural sharing (efficient copies)
|
||||
- Explicit immutability (compiler enforced)
|
||||
- Safe for Compose state
|
||||
|
||||
### ImmutableList
|
||||
|
||||
```kotlin
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
||||
// Create immutable list
|
||||
val relays: ImmutableList<String> = persistentListOf(
|
||||
"wss://relay1.com",
|
||||
"wss://relay2.com"
|
||||
)
|
||||
|
||||
// Add returns NEW list
|
||||
val updated = relays.add("wss://relay3.com")
|
||||
assert(relays.size == 2) // Original unchanged
|
||||
assert(updated.size == 3) // New list has 3 items
|
||||
|
||||
// Convert from regular list
|
||||
val mutableList = mutableListOf("a", "b", "c")
|
||||
val immutable = mutableList.toImmutableList()
|
||||
```
|
||||
|
||||
### ImmutableMap
|
||||
|
||||
```kotlin
|
||||
import kotlinx.collections.immutable.ImmutableMap
|
||||
import kotlinx.collections.immutable.persistentMapOf
|
||||
import kotlinx.collections.immutable.toImmutableMap
|
||||
|
||||
// Create immutable map
|
||||
val relayStatuses: ImmutableMap<String, RelayStatus> = persistentMapOf(
|
||||
"wss://relay1.com" to RelayStatus(...),
|
||||
"wss://relay2.com" to RelayStatus(...)
|
||||
)
|
||||
|
||||
// Put returns NEW map
|
||||
val updated = relayStatuses.put("wss://relay3.com", RelayStatus(...))
|
||||
|
||||
// Remove returns NEW map
|
||||
val removed = relayStatuses.remove("wss://relay1.com")
|
||||
```
|
||||
|
||||
### ImmutableSet
|
||||
|
||||
```kotlin
|
||||
import kotlinx.collections.immutable.ImmutableSet
|
||||
import kotlinx.collections.immutable.persistentSetOf
|
||||
|
||||
val connectedRelays: ImmutableSet<String> = persistentSetOf(
|
||||
"wss://relay1.com",
|
||||
"wss://relay2.com"
|
||||
)
|
||||
|
||||
val updated = connectedRelays.add("wss://relay3.com")
|
||||
```
|
||||
|
||||
### Structural Sharing
|
||||
|
||||
**Mental model:** Immutable collections reuse internal structure for efficiency.
|
||||
|
||||
```kotlin
|
||||
val list1 = persistentListOf(1, 2, 3, 4, 5) // 5 items
|
||||
val list2 = list1.add(6) // Shares structure with list1
|
||||
|
||||
// Internally:
|
||||
// list1 and list2 share nodes for items 1-5
|
||||
// list2 has one additional node for item 6
|
||||
// O(1) time, O(1) space for add operation
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Pattern: Immutable State Updates
|
||||
|
||||
```kotlin
|
||||
@Immutable
|
||||
data class FeedState(
|
||||
val events: ImmutableList<Event>,
|
||||
val loading: Boolean,
|
||||
val error: String?
|
||||
)
|
||||
|
||||
class FeedViewModel {
|
||||
private val _state = MutableStateFlow(
|
||||
FeedState(
|
||||
events = persistentListOf(),
|
||||
loading = false,
|
||||
error = null
|
||||
)
|
||||
)
|
||||
val state: StateFlow<FeedState> = _state.asStateFlow()
|
||||
|
||||
fun loadEvents() {
|
||||
_state.value = _state.value.copy(loading = true, error = null)
|
||||
|
||||
viewModelScope.launch {
|
||||
try {
|
||||
val events = repository.getEvents()
|
||||
_state.value = _state.value.copy(
|
||||
events = events.toImmutableList(),
|
||||
loading = false
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
_state.value = _state.value.copy(
|
||||
loading = false,
|
||||
error = e.message
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun addEvent(event: Event) {
|
||||
_state.value = _state.value.copy(
|
||||
events = _state.value.events.add(event) // Immutable add
|
||||
)
|
||||
}
|
||||
|
||||
fun removeEvent(eventId: String) {
|
||||
_state.value = _state.value.copy(
|
||||
events = _state.value.events.filter { it.id != eventId }.toImmutableList()
|
||||
)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Pattern: Deep Immutability
|
||||
|
||||
```kotlin
|
||||
// Nested immutable structures
|
||||
@Immutable
|
||||
data class User(
|
||||
val name: String,
|
||||
val profile: Profile // Also immutable
|
||||
)
|
||||
|
||||
@Immutable
|
||||
data class Profile(
|
||||
val bio: String,
|
||||
val avatar: String,
|
||||
val relays: ImmutableList<String> // Immutable collection
|
||||
)
|
||||
|
||||
// Safe deep copy
|
||||
val user = User(
|
||||
name = "Alice",
|
||||
profile = Profile(
|
||||
bio = "Nostr enthusiast",
|
||||
avatar = "https://...",
|
||||
relays = persistentListOf("wss://relay1.com")
|
||||
)
|
||||
)
|
||||
|
||||
val updatedUser = user.copy(
|
||||
profile = user.profile.copy(
|
||||
bio = "Bitcoin & Nostr enthusiast" // Deep update
|
||||
)
|
||||
)
|
||||
```
|
||||
|
||||
### Pattern: Collection Builder to Immutable
|
||||
|
||||
```kotlin
|
||||
// Build mutable, convert to immutable
|
||||
fun processEvents(input: List<Event>): ImmutableList<Event> {
|
||||
val processed = mutableListOf<Event>()
|
||||
|
||||
for (event in input) {
|
||||
if (event.isValid()) {
|
||||
processed.add(event.normalize())
|
||||
}
|
||||
}
|
||||
|
||||
return processed.toImmutableList() // Convert once at end
|
||||
}
|
||||
```
|
||||
|
||||
### Pattern: Immutable Map Updates
|
||||
|
||||
```kotlin
|
||||
private val _relayStatuses = MutableStateFlow<ImmutableMap<String, RelayStatus>>(
|
||||
persistentMapOf()
|
||||
)
|
||||
|
||||
fun updateRelay(url: String, connected: Boolean) {
|
||||
val currentStatuses = _relayStatuses.value
|
||||
val currentStatus = currentStatuses[url] ?: RelayStatus(url, false)
|
||||
|
||||
_relayStatuses.value = currentStatuses.put(
|
||||
url,
|
||||
currentStatus.copy(connected = connected)
|
||||
)
|
||||
}
|
||||
|
||||
fun removeRelay(url: String) {
|
||||
_relayStatuses.value = _relayStatuses.value.remove(url)
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Performance Impact
|
||||
|
||||
### Benchmarks (Approximate)
|
||||
|
||||
**Recomposition cost:**
|
||||
|
||||
```kotlin
|
||||
// 1000 items in LazyColumn
|
||||
// Without @Immutable: ~100ms per frame (skipped frames)
|
||||
// With @Immutable: ~16ms per frame (smooth 60fps)
|
||||
|
||||
@Immutable
|
||||
data class Item(val id: String, val name: String)
|
||||
|
||||
@Composable
|
||||
fun ItemList(items: ImmutableList<Item>) {
|
||||
LazyColumn {
|
||||
items(items, key = { it.id }) { item ->
|
||||
ItemRow(item) // Only recomposes when item changes
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Structural sharing efficiency:**
|
||||
|
||||
```kotlin
|
||||
val list1 = persistentListOf(1..10000)
|
||||
val list2 = list1.add(10001) // O(log n) time, shares structure
|
||||
|
||||
// Regular list (copy on modification):
|
||||
val mutableList = (1..10000).toMutableList()
|
||||
val copy = mutableList.toList() + 10001 // O(n) time, full copy
|
||||
```
|
||||
|
||||
### When to Use Immutable Collections
|
||||
|
||||
**Use ImmutableList/Map/Set when:**
|
||||
- Storing in Compose state (@Immutable class)
|
||||
- Sharing across coroutines
|
||||
- Frequent modifications (structural sharing efficient)
|
||||
- Need compile-time immutability guarantee
|
||||
|
||||
**Use Array when:**
|
||||
- Fixed size, no modifications
|
||||
- Nostr protocol (tags are `Array<Array<String>>`)
|
||||
- Performance-critical (array access is fastest)
|
||||
|
||||
**Use regular List/Map/Set when:**
|
||||
- Local scope only
|
||||
- Build once, read many times
|
||||
- Converting to immutable at boundary
|
||||
|
||||
---
|
||||
|
||||
## Anti-Patterns
|
||||
|
||||
### ❌ Mutable Properties in @Immutable Class
|
||||
|
||||
```kotlin
|
||||
@Immutable
|
||||
data class BadEvent(
|
||||
val id: String,
|
||||
var content: String // BAD: var breaks immutability
|
||||
)
|
||||
```
|
||||
|
||||
### ✅ All val Properties
|
||||
|
||||
```kotlin
|
||||
@Immutable
|
||||
data class GoodEvent(
|
||||
val id: String,
|
||||
val content: String
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### ❌ Mutable Collections in @Immutable Class
|
||||
|
||||
```kotlin
|
||||
@Immutable
|
||||
data class BadState(
|
||||
val items: MutableList<Item> // BAD: Can mutate items
|
||||
)
|
||||
|
||||
// Caller can mutate:
|
||||
val state = BadState(mutableListOf())
|
||||
state.items.add(newItem) // Breaks immutability!
|
||||
```
|
||||
|
||||
### ✅ Immutable Collections
|
||||
|
||||
```kotlin
|
||||
@Immutable
|
||||
data class GoodState(
|
||||
val items: ImmutableList<Item>
|
||||
)
|
||||
|
||||
// Caller must create new state:
|
||||
val updated = state.copy(items = state.items.add(newItem))
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### ❌ Direct Mutation
|
||||
|
||||
```kotlin
|
||||
val status = RelayStatus(url, connected = false)
|
||||
status.connected = true // Compile error (val)
|
||||
|
||||
// But could happen with mutable nested objects:
|
||||
@Immutable
|
||||
data class Config(
|
||||
val settings: Settings // If Settings is mutable...
|
||||
)
|
||||
|
||||
class Settings {
|
||||
var theme: String = "dark" // BAD
|
||||
}
|
||||
|
||||
val config = Config(Settings())
|
||||
config.settings.theme = "light" // Mutates "immutable" config!
|
||||
```
|
||||
|
||||
### ✅ Deep Immutability
|
||||
|
||||
```kotlin
|
||||
@Immutable
|
||||
data class Config(
|
||||
val settings: Settings
|
||||
)
|
||||
|
||||
@Immutable
|
||||
data class Settings(
|
||||
val theme: String // val only
|
||||
)
|
||||
|
||||
val config = Config(Settings("dark"))
|
||||
val updated = config.copy(
|
||||
settings = config.settings.copy(theme = "light")
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### ❌ Exposing Mutable Internal State
|
||||
|
||||
```kotlin
|
||||
@Immutable
|
||||
class BadViewModel {
|
||||
private val _items = mutableListOf<Item>()
|
||||
val items: List<Item> = _items // BAD: Exposes mutable list
|
||||
|
||||
fun addItem(item: Item) {
|
||||
_items.add(item)
|
||||
}
|
||||
}
|
||||
|
||||
// Caller can cast and mutate:
|
||||
val vm = BadViewModel()
|
||||
(vm.items as MutableList).clear() // Breaks encapsulation!
|
||||
```
|
||||
|
||||
### ✅ Convert to Immutable at Boundary
|
||||
|
||||
```kotlin
|
||||
@Immutable
|
||||
class GoodViewModel {
|
||||
private val _items = mutableListOf<Item>()
|
||||
val items: ImmutableList<Item>
|
||||
get() = _items.toImmutableList() // GOOD: Copy to immutable
|
||||
|
||||
fun addItem(item: Item) {
|
||||
_items.add(item)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Checklist for Immutability
|
||||
|
||||
**For @Immutable classes:**
|
||||
- [ ] All properties are `val`, never `var`
|
||||
- [ ] No mutable collections (`MutableList`, `MutableMap`, `MutableSet`)
|
||||
- [ ] Nested objects are also `@Immutable` or primitives
|
||||
- [ ] No public mutable state
|
||||
- [ ] Use `copy()` for updates, never mutation
|
||||
- [ ] Arrays used only when truly immutable by contract
|
||||
|
||||
**For StateFlow state:**
|
||||
- [ ] State class is `@Immutable`
|
||||
- [ ] Use immutable collections (ImmutableList, ImmutableMap)
|
||||
- [ ] Create new instances for updates (`copy()`, `.add()`, `.put()`)
|
||||
- [ ] Never mutate state in-place
|
||||
|
||||
**For Compose performance:**
|
||||
- [ ] All `@Composable` parameters are `@Immutable` or `@Stable`
|
||||
- [ ] Lists use `ImmutableList` and `key` parameter in `items()`
|
||||
- [ ] Heavy objects (events, profiles) cached and reused
|
||||
|
||||
---
|
||||
|
||||
## References
|
||||
|
||||
- TextNoteEvent.kt:51-63 - @Immutable event example
|
||||
- RelayConnectionManager.kt - Immutable map updates
|
||||
- [Compose Performance | Android Developers](https://developer.android.com/jetpack/compose/performance/stability)
|
||||
- [kotlinx.collections.immutable | GitHub](https://github.com/Kotlin/kotlinx.collections.immutable)
|
||||
- [@Stable and @Immutable | Compose Docs](https://developer.android.com/jetpack/compose/performance/stability/fix)
|
||||
@@ -0,0 +1,482 @@
|
||||
# Sealed Class Catalog
|
||||
|
||||
Comprehensive list of sealed types in AmethystMultiplatform with usage patterns.
|
||||
|
||||
## Table of Contents
|
||||
- [State Management](#state-management)
|
||||
- [Result Types](#result-types)
|
||||
- [Tag Variants](#tag-variants)
|
||||
- [Sealed Class vs Sealed Interface](#sealed-class-vs-sealed-interface)
|
||||
- [Patterns](#patterns)
|
||||
|
||||
---
|
||||
|
||||
## State Management
|
||||
|
||||
### AccountState (Sealed Class)
|
||||
|
||||
**File:** `commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/account/AccountManager.kt:36-46`
|
||||
|
||||
```kotlin
|
||||
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()
|
||||
}
|
||||
```
|
||||
|
||||
**Why sealed class:**
|
||||
- Two distinct states with different data
|
||||
- `LoggedIn` holds data, `LoggedOut` doesn't
|
||||
- No need for generics or multiple inheritance
|
||||
|
||||
**Usage:**
|
||||
|
||||
```kotlin
|
||||
fun handleAccountState(state: AccountState) {
|
||||
when (state) {
|
||||
is AccountState.LoggedOut -> showLogin()
|
||||
is AccountState.LoggedIn -> {
|
||||
showFeed(
|
||||
pubkey = state.pubKeyHex,
|
||||
canSign = !state.isReadOnly
|
||||
)
|
||||
}
|
||||
} // Exhaustive - compiler enforces
|
||||
}
|
||||
```
|
||||
|
||||
### VerificationState (Sealed Class)
|
||||
|
||||
**File:** `quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip03Timestamp/VerificationState.kt`
|
||||
|
||||
```kotlin
|
||||
sealed class VerificationState {
|
||||
data object NotStarted : VerificationState()
|
||||
data object Started : VerificationState()
|
||||
data class Failed(val reason: String) : VerificationState()
|
||||
data object Verified : VerificationState()
|
||||
}
|
||||
```
|
||||
|
||||
**Pattern:**
|
||||
- State machine (NotStarted → Started → Failed/Verified)
|
||||
- Only `Failed` carries data (reason)
|
||||
- Rest are singletons (`data object`)
|
||||
|
||||
---
|
||||
|
||||
## Result Types
|
||||
|
||||
### SignerResult (Sealed Interface with Generics)
|
||||
|
||||
**File:** `quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip46RemoteSigner/signer/SignerResult.kt:25-46`
|
||||
|
||||
```kotlin
|
||||
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? = null
|
||||
) : RequestAddressed<T>
|
||||
class ReceivedButCouldNotParseEventFromResult<T : IResult>(
|
||||
val eventJson: String
|
||||
) : RequestAddressed<T>
|
||||
class ReceivedButCouldNotVerifyResultingEvent<T : IResult>(
|
||||
val invalidEvent: Event
|
||||
) : RequestAddressed<T>
|
||||
}
|
||||
}
|
||||
|
||||
interface IResult
|
||||
|
||||
data class SignResult(val event: Event) : IResult
|
||||
data class EncryptionResult(val ciphertext: String) : IResult
|
||||
data class DecryptionResult(val plaintext: String) : IResult
|
||||
```
|
||||
|
||||
**Why sealed interface:**
|
||||
- Generic result type `<T : IResult>`
|
||||
- Nested sealed hierarchy (RequestAddressed)
|
||||
- Need covariance for flexible result types
|
||||
|
||||
**Usage:**
|
||||
|
||||
```kotlin
|
||||
suspend fun signEvent(event: Event): SignerResult<SignResult> {
|
||||
return when (val result = remoteSigner.sign(event)) {
|
||||
is SignerResult.RequestAddressed.Successful -> result
|
||||
is SignerResult.RequestAddressed.Rejected -> {
|
||||
logger.warn("Signing rejected")
|
||||
result
|
||||
}
|
||||
is SignerResult.RequestAddressed.TimedOut -> {
|
||||
logger.error("Signing timed out")
|
||||
result
|
||||
}
|
||||
is SignerResult.RequestAddressed.ReceivedButCouldNotPerform -> {
|
||||
logger.error("Signer error: ${result.message}")
|
||||
result
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### CacheResults (Sealed Class with Generics)
|
||||
|
||||
**File:** `quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/signers/caches/CacheResults.kt`
|
||||
|
||||
```kotlin
|
||||
sealed class CacheResults<T> {
|
||||
data class Found<T>(val value: T) : CacheResults<T>()
|
||||
class NotFound<T> : CacheResults<T>()
|
||||
}
|
||||
```
|
||||
|
||||
**Pattern:**
|
||||
- Simple binary result (found/not found)
|
||||
- `Found` carries data, `NotFound` doesn't
|
||||
- Generic for reusability
|
||||
|
||||
---
|
||||
|
||||
## Tag Variants
|
||||
|
||||
### MuteTag (Sealed Class)
|
||||
|
||||
**File:** `quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/muteList/tags/MuteTag.kt`
|
||||
|
||||
```kotlin
|
||||
sealed class MuteTag(
|
||||
val nameOrNull: String?,
|
||||
val valueOrNull: String?
|
||||
) {
|
||||
class Event(eventId: String) : MuteTag("e", eventId)
|
||||
class Profile(pubkey: String) : MuteTag("p", pubkey)
|
||||
class Word(word: String) : MuteTag("word", word)
|
||||
class Thread(threadId: String) : MuteTag("thread", threadId)
|
||||
|
||||
companion object {
|
||||
fun parse(tag: Array<String>): MuteTag? {
|
||||
return when (tag.getOrNull(0)) {
|
||||
"e" -> tag.getOrNull(1)?.let { Event(it) }
|
||||
"p" -> tag.getOrNull(1)?.let { Profile(it) }
|
||||
"word" -> tag.getOrNull(1)?.let { Word(it) }
|
||||
"thread" -> tag.getOrNull(1)?.let { Thread(it) }
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun toArray(): Array<String> {
|
||||
return arrayOf(nameOrNull ?: "", valueOrNull ?: "")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Pattern:**
|
||||
- Common base class with shared properties
|
||||
- Each variant represents different tag type
|
||||
- Factory method `parse()` for parsing
|
||||
- `toArray()` for serialization
|
||||
|
||||
### BookmarkIdTag (Sealed Class)
|
||||
|
||||
**File:** `quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/bookmarkList/tags/BookmarkIdTag.kt`
|
||||
|
||||
```kotlin
|
||||
sealed class BookmarkIdTag {
|
||||
abstract val id: String
|
||||
abstract val marker: String?
|
||||
|
||||
data class Event(override val id: String, override val marker: String?) : BookmarkIdTag()
|
||||
data class Profile(override val id: String, override val marker: String?) : BookmarkIdTag()
|
||||
data class Address(override val id: String, override val marker: String?) : BookmarkIdTag()
|
||||
|
||||
companion object {
|
||||
fun parse(tag: Array<String>): BookmarkIdTag? {
|
||||
val marker = tag.getOrNull(3)
|
||||
return when (tag.getOrNull(0)) {
|
||||
"e" -> tag.getOrNull(1)?.let { Event(it, marker) }
|
||||
"p" -> tag.getOrNull(1)?.let { Profile(it, marker) }
|
||||
"a" -> tag.getOrNull(1)?.let { Address(it, marker) }
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Pattern:**
|
||||
- Abstract properties in sealed class
|
||||
- Data classes implement abstract properties
|
||||
- Parse factory returns sealed variant
|
||||
|
||||
---
|
||||
|
||||
## Exception Hierarchies
|
||||
|
||||
### SignerExceptions (Sealed Class)
|
||||
|
||||
**File:** `quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/signers/SignerExceptions.kt`
|
||||
|
||||
```kotlin
|
||||
sealed class SignerExceptions(message: String) : Exception(message) {
|
||||
class UnableToSign(message: String) : SignerExceptions(message)
|
||||
class UnableToDecrypt(message: String) : SignerExceptions(message)
|
||||
class UnableToEncrypt(message: String) : SignerExceptions(message)
|
||||
class UnableToGetPublicKey(message: String) : SignerExceptions(message)
|
||||
}
|
||||
```
|
||||
|
||||
**Pattern:**
|
||||
- Sealed exception hierarchy
|
||||
- Extends `Exception` base class
|
||||
- Type-safe error handling
|
||||
|
||||
**Usage:**
|
||||
|
||||
```kotlin
|
||||
try {
|
||||
signer.sign(event)
|
||||
} catch (e: SignerExceptions) {
|
||||
when (e) {
|
||||
is SignerExceptions.UnableToSign -> logger.error("Signing failed: ${e.message}")
|
||||
is SignerExceptions.UnableToDecrypt -> logger.error("Decryption failed: ${e.message}")
|
||||
is SignerExceptions.UnableToEncrypt -> logger.error("Encryption failed: ${e.message}")
|
||||
is SignerExceptions.UnableToGetPublicKey -> logger.error("No public key: ${e.message}")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Sealed Class vs Sealed Interface
|
||||
|
||||
### When to Use Sealed Class
|
||||
|
||||
**Examples from codebase:**
|
||||
|
||||
1. **AccountState** - State variants with different data
|
||||
2. **VerificationState** - State machine
|
||||
3. **MuteTag** - Tag variants with common base properties
|
||||
4. **SignerExceptions** - Exception hierarchy
|
||||
|
||||
**Characteristics:**
|
||||
- Need common constructor parameters
|
||||
- Single inheritance only
|
||||
- State variants
|
||||
- Exception hierarchies
|
||||
|
||||
### When to Use Sealed Interface
|
||||
|
||||
**Examples from codebase:**
|
||||
|
||||
1. **SignerResult<T>** - Generic result types needing variance
|
||||
2. **RelayUrlNormalizer.Result** - Binary result with no shared state
|
||||
|
||||
**Characteristics:**
|
||||
- Need generics with variance (`out`, `in`)
|
||||
- No common state needed
|
||||
- Multiple inheritance possible
|
||||
- Contract/capability representation
|
||||
|
||||
---
|
||||
|
||||
## Patterns
|
||||
|
||||
### Pattern: State Machine
|
||||
|
||||
```kotlin
|
||||
sealed class ConnectionState {
|
||||
data object Disconnected : ConnectionState()
|
||||
data object Connecting : ConnectionState()
|
||||
data class Connected(val relay: String) : ConnectionState()
|
||||
data class Failed(val error: String) : ConnectionState()
|
||||
}
|
||||
|
||||
// Allowed transitions
|
||||
fun transition(from: ConnectionState, event: Event): ConnectionState {
|
||||
return when (from) {
|
||||
is ConnectionState.Disconnected -> {
|
||||
when (event) {
|
||||
is Event.Connect -> ConnectionState.Connecting
|
||||
else -> from
|
||||
}
|
||||
}
|
||||
is ConnectionState.Connecting -> {
|
||||
when (event) {
|
||||
is Event.Success -> ConnectionState.Connected(event.relay)
|
||||
is Event.Error -> ConnectionState.Failed(event.message)
|
||||
is Event.Cancel -> ConnectionState.Disconnected
|
||||
else -> from
|
||||
}
|
||||
}
|
||||
is ConnectionState.Connected -> {
|
||||
when (event) {
|
||||
is Event.Disconnect -> ConnectionState.Disconnected
|
||||
is Event.Error -> ConnectionState.Failed(event.message)
|
||||
else -> from
|
||||
}
|
||||
}
|
||||
is ConnectionState.Failed -> {
|
||||
when (event) {
|
||||
is Event.Retry -> ConnectionState.Connecting
|
||||
is Event.Cancel -> ConnectionState.Disconnected
|
||||
else -> from
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Pattern: Result Type
|
||||
|
||||
```kotlin
|
||||
sealed interface Result<out T> {
|
||||
data class Success<T>(val data: T) : Result<T>
|
||||
data class Error(val exception: Exception) : Result<Nothing>
|
||||
data object Loading : Result<Nothing>
|
||||
}
|
||||
|
||||
// Extension functions
|
||||
fun <T> Result<T>.getOrNull(): T? = when (this) {
|
||||
is Result.Success -> data
|
||||
else -> null
|
||||
}
|
||||
|
||||
fun <T> Result<T>.getOrThrow(): T = when (this) {
|
||||
is Result.Success -> data
|
||||
is Result.Error -> throw exception
|
||||
is Result.Loading -> error("Still loading")
|
||||
}
|
||||
|
||||
fun <T, R> Result<T>.map(transform: (T) -> R): Result<R> = when (this) {
|
||||
is Result.Success -> Result.Success(transform(data))
|
||||
is Result.Error -> this
|
||||
is Result.Loading -> Result.Loading
|
||||
}
|
||||
```
|
||||
|
||||
### Pattern: Tagged Union (Discriminated Union)
|
||||
|
||||
```kotlin
|
||||
sealed class Command {
|
||||
data class SendEvent(val event: Event) : Command()
|
||||
data class Subscribe(val filters: List<Filter>) : Command()
|
||||
data class Unsubscribe(val subId: String) : Command()
|
||||
data object Close : Command()
|
||||
|
||||
fun toJson(): String = when (this) {
|
||||
is SendEvent -> """["EVENT",${event.toJson()}]"""
|
||||
is Subscribe -> """["REQ","sub",${filters.joinToString { it.toJson() }}]"""
|
||||
is Unsubscribe -> """["CLOSE","$subId"]"""
|
||||
is Close -> """["CLOSE"]"""
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Pattern: Nested Sealed Hierarchies
|
||||
|
||||
```kotlin
|
||||
sealed interface UiState {
|
||||
sealed interface Loading : UiState {
|
||||
data object Initial : Loading
|
||||
data class Refreshing(val currentData: List<Item>) : Loading
|
||||
}
|
||||
|
||||
sealed interface Content : UiState {
|
||||
data class Success(val data: List<Item>) : Content
|
||||
data object Empty : Content
|
||||
}
|
||||
|
||||
sealed interface Error : UiState {
|
||||
data class Network(val message: String) : Error
|
||||
data class Server(val code: Int, val message: String) : Error
|
||||
}
|
||||
}
|
||||
|
||||
// Usage
|
||||
fun renderUi(state: UiState) {
|
||||
when (state) {
|
||||
is UiState.Loading.Initial -> showFullScreenLoader()
|
||||
is UiState.Loading.Refreshing -> showRefreshIndicator(state.currentData)
|
||||
is UiState.Content.Success -> showList(state.data)
|
||||
is UiState.Content.Empty -> showEmptyState()
|
||||
is UiState.Error.Network -> showNetworkError(state.message)
|
||||
is UiState.Error.Server -> showServerError(state.code, state.message)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## All Sealed Types in Quartz
|
||||
|
||||
**Complete list of sealed types found in codebase:**
|
||||
|
||||
### Commons
|
||||
- AccountState (class)
|
||||
|
||||
### Quartz
|
||||
- BaseZapSplitSetup (class)
|
||||
- MuteTag (class)
|
||||
- BookmarkIdTag (class)
|
||||
- SignerResult (interface)
|
||||
- VerificationState (class)
|
||||
- CacheResults (class)
|
||||
- SignerExceptions (class)
|
||||
- RelayUrlNormalizer.Result (interface)
|
||||
|
||||
**Total:** 8 sealed types (7 classes, 1 interface)
|
||||
|
||||
---
|
||||
|
||||
## Decision Tree
|
||||
|
||||
```
|
||||
Need to represent variants of a concept?
|
||||
YES → Use sealed type
|
||||
NO → Regular class/interface
|
||||
|
||||
Variants have different data?
|
||||
YES → sealed class or sealed interface
|
||||
NO → enum (if simple constants)
|
||||
|
||||
Need generics with variance (out/in)?
|
||||
YES → sealed interface
|
||||
NO → sealed class (simpler)
|
||||
|
||||
Need common constructor/properties?
|
||||
YES → sealed class
|
||||
NO → sealed interface
|
||||
|
||||
Need multiple inheritance?
|
||||
YES → sealed interface
|
||||
NO → Either works
|
||||
|
||||
Representing state machine?
|
||||
→ sealed class (state transitions)
|
||||
|
||||
Representing result/error types?
|
||||
→ sealed interface (if generic, else class)
|
||||
|
||||
Representing tag/command variants?
|
||||
→ sealed class (common structure)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## References
|
||||
|
||||
- [Sealed Classes | Kotlin Docs](https://kotlinlang.org/docs/sealed-classes.html)
|
||||
- [Effective Kotlin: Sealed Classes](https://kt.academy/article/ek-sealed-classes)
|
||||
- [Complete Guide: Sealed Classes & Interfaces 2025](https://proandroiddev.com/complete-technical-guide-sealed-classes-sealed-interfaces-enums-in-kotlin-28ffc39116df)
|
||||
Reference in New Issue
Block a user