KO

kotlin-coroutines

Provides advanced patterns for Kotlin coroutines in Amethyst, including Flow operators, structured concurrency, and async testing.

Install

mkdir -p .claude/skills/kotlin-coroutines && curl -L -o skill.zip "https://agentskills.codes/api/skills/download/3062" && unzip -o skill.zip -d .claude/skills/kotlin-coroutines && rm skill.zip

Installs to .claude/skills/kotlin-coroutines

Activation

This is the description your AI agent reads to decide when to run this skill — the better it matches your request, the more reliably it fires.

Advanced Kotlin coroutines patterns for AmethystMultiplatform. Use when working with: (1) Structured concurrency (supervisorScope, coroutineScope), (2) Advanced Flow operators (flatMapLatest, combine, merge, shareIn, stateIn), (3) Channels and callbackFlow, (4) Dispatcher management and context switching, (5) Exception handling (CoroutineExceptionHandler, SupervisorJob), (6) Testing async code (runTest, Turbine), (7) Nostr relay connection pools and subscriptions, (8) Backpressure handling in event streams. Delegates to kotlin-expert for basic StateFlow/SharedFlow patterns. Complements nostr-expert for relay communication.
630 chars✓ has a “when” triggerlonger than Claude Code's old 250-char listing cap (fine on current versions)
Advanced

Key capabilities

  • Manage multi-relay subscriptions using supervisorScope
  • Implement callbackFlow for Android callbacks
  • Handle backpressure in high-frequency event streams
  • Manage exceptions with CoroutineExceptionHandler
  • Test asynchronous code with runTest and Turbine

How it works

This skill provides advanced Kotlin coroutines patterns for managing complex asynchronous operations, including structured concurrency, Flow operators, channels, and exception handling, specifically for the AmethystMultiplatform project.

Inputs & outputs

You give it
Relay lists, filters, Android callbacks, event streams
You get back
Flows of events, managed relay connections, handled exceptions, tested async code

When to use kotlin-coroutines

  • Implementing multi-relay subscriptions
  • Handling backpressure in event streams
  • Testing asynchronous code with Turbine
  • Managing complex callbackFlows

About this skill

Kotlin Coroutines - Advanced Async Patterns

Expert guidance for complex async operations in Amethyst: relay pools, event streams, structured concurrency, and testing.

Mental Model

Async Architecture in Amethyst:

Relay Pool (supervisorScope)
    ├── Relay 1 (launch) → callbackFlow → Events
    ├── Relay 2 (launch) → callbackFlow → Events
    └── Relay 3 (launch) → callbackFlow → Events
            ↓
    merge() → distinctBy(id) → shareIn
            ↓
    Multiple Collectors (ViewModels, Services)

Key principles:

  • supervisorScope - Children fail independently
  • callbackFlow - Bridge callbacks to Flow
  • shareIn/stateIn - Hot flows from cold
  • Backpressure - buffer(), conflate(), DROP_OLDEST

When to Use This Skill

Use for advanced async patterns:

  • Multi-relay subscriptions with supervisorScope
  • Complex Flow operators (flatMapLatest, combine, merge)
  • callbackFlow for Android callbacks (connectivity, location)
  • Backpressure handling in high-frequency streams
  • Exception handling with CoroutineExceptionHandler
  • Testing coroutines with runTest and Turbine

Delegate to kotlin-expert for:

  • Basic StateFlow/SharedFlow patterns
  • Simple viewModelScope.launch
  • MutableStateFlow → asStateFlow()

Core Patterns

Pattern: callbackFlow for Relay Subscriptions

// Real pattern from NostrClientStaticReqAsStateFlow.kt
fun INostrClient.reqAsFlow(
    relay: NormalizedRelayUrl,
    filters: List<Filter>,
): Flow<List<Event>> = callbackFlow {
    val subId = RandomInstance.randomChars(10)
    var hasBeenLive = false
    val eventIds = mutableSetOf<HexKey>()
    var currentEvents = listOf<Event>()

    val listener = object : IRequestListener {
        override fun onEvent(event: Event, ...) {
            if (event.id !in eventIds) {
                currentEvents = if (hasBeenLive) {
                    // After EOSE: prepend
                    listOf(event) + currentEvents
                } else {
                    // Before EOSE: append
                    currentEvents + event
                }
                eventIds.add(event.id)
                trySend(currentEvents)
            }
        }

        override fun onEose(...) {
            hasBeenLive = true
        }
    }

    openReqSubscription(subId, mapOf(relay to filters), listener)

    awaitClose { close(subId) }
}

Key techniques:

  1. Deduplication with Set
  2. EOSE handling (append → prepend strategy)
  3. trySend (non-blocking from callback)
  4. awaitClose for cleanup

Pattern: Structured Concurrency for Relays

suspend fun connectToRelays(relays: List<Relay>) = supervisorScope {
    relays.forEach { relay ->
        launch {
            try {
                relay.connect()
                relay.subscribe(filters).collect { event ->
                    eventChannel.send(event)
                }
            } catch (e: IOException) {
                Log.e("Relay", "Connection failed: ${relay.url}", e)
                // Other relays continue
            }
        }
    }
}

Why supervisorScope:

  • One relay failure doesn't cancel others
  • All cancelled together when scope cancelled
  • Proper cleanup guaranteed

Pattern: Exception Handling for Services

// Real pattern from PushNotificationReceiverService.kt
class MyService : Service() {
    val exceptionHandler = CoroutineExceptionHandler { _, throwable ->
        Log.e("Service", "Caught: ${throwable.message}", throwable)
    }

    private val scope = CoroutineScope(
        Dispatchers.IO + SupervisorJob() + exceptionHandler
    )

    override fun onDestroy() {
        scope.cancel()
        super.onDestroy()
    }
}

Pattern benefits:

  • SupervisorJob: children fail independently
  • ExceptionHandler: log instead of crash
  • Scoped lifecycle: cancel all on destroy

Pattern: Network Connectivity as Flow

// Real pattern from ConnectivityFlow.kt
val status = callbackFlow {
    val networkCallback = object : NetworkCallback() {
        override fun onAvailable(network: Network) {
            trySend(ConnectivityStatus.Active(...))
        }
        override fun onLost(network: Network) {
            trySend(ConnectivityStatus.Off)
        }
    }

    connectivityManager.registerCallback(networkCallback)

    // Initial state
    activeNetwork?.let { trySend(ConnectivityStatus.Active(...)) }

    awaitClose {
        connectivityManager.unregisterCallback(networkCallback)
    }
}
    .distinctUntilChanged()
    .debounce(200)  // Stabilize flapping
    .flowOn(Dispatchers.IO)

Key patterns:

  1. Emit initial state immediately
  2. Register callback in flow body
  3. Cleanup in awaitClose
  4. Stabilize with debounce + distinctUntilChanged

Pattern: Merge Events from Multiple Relays

fun observeFromRelays(
    relays: List<NormalizedRelayUrl>,
    filters: List<Filter>
): Flow<Event> =
    relays.map { relay ->
        client.reqAsFlow(relay, filters)
            .flatMapConcat { it.asFlow() }
    }.merge()
    .distinctBy { it.id }

Flow:

  • Each relay: Flow<List<Event>>
  • flatMapConcat: flatten to Flow<Event>
  • merge(): combine all relays
  • distinctBy: deduplicate across relays

Advanced Operators

For comprehensive coverage of Flow operators:

  • flatMapLatest, combine, zip, merge → See advanced-flow-operators.md
  • shareIn, stateIn → Conversion to hot flows
  • buffer, conflate → Backpressure strategies
  • debounce, sample → Rate limiting

Quick Reference

OperatorUse CaseExample
flatMapLatestCancel previous, switch to newSearch (cancel old query)
combineLatest from ALL flowscombine(account, settings, connectivity)
mergeSingle stream from multiplemerge(relay1, relay2, relay3)
shareInMultiple collectors, single upstreamShare expensive computation
stateInStateFlow from FlowViewModel state
buffer(DROP_OLDEST)High-frequency streamsReal-time event feed
conflateLatest onlyUI updates
debounceWait for quiet periodSearch input

Nostr Relay Patterns

For complete relay-specific patterns: → See relay-patterns.md

Covers:

  • Multi-relay subscription management
  • Connection lifecycle and reconnection
  • Event deduplication strategies
  • Backpressure for high-frequency events
  • EOSE handling patterns

Testing

For comprehensive testing patterns: → See testing-coroutines.md

Quick testing pattern:

@Test
fun `relay subscription receives events`() = runTest {
    val client = FakeNostrClient()

    client.reqAsFlow(relay, filters).test {
        assertEquals(emptyList(), awaitItem())

        client.sendEvent(event1)
        assertEquals(listOf(event1), awaitItem())

        cancelAndIgnoreRemainingEvents()
    }
}

Testing tools:

  • runTest - Virtual time, auto cleanup
  • Turbine .test {} - Flow assertions
  • advanceTimeBy() - Control time
  • Fake implementations over mocks

Common Scenarios

Scenario: Implement New Relay Feature

Steps:

  1. callbackFlow for subscription
  2. Deduplication (Set of event IDs)
  3. awaitClose for cleanup
  4. Test with FakeNostrClient

Example: Add subscription for specific event kind

fun observeKind(kind: Int): Flow<Event> = callbackFlow {
    val listener = object : IRequestListener {
        override fun onEvent(event: Event, ...) {
            if (event.kind == kind) {
                trySend(event)
            }
        }
    }
    client.subscribe(listener)
    awaitClose { client.unsubscribe(listener) }
}

Scenario: Handle Network Connectivity Changes

Steps:

  1. callbackFlow for connectivity
  2. flatMapLatest to reconnect
  3. debounce to stabilize
  4. Exception handling for failures

Example: Reconnect relays on connectivity

connectivityFlow
    .flatMapLatest { status ->
        when (status) {
            Active -> relayPool.observeEvents()
            else -> emptyFlow()
        }
    }
    .catch { e -> Log.e("Error", e) }
    .collect { event -> handleEvent(event) }

Scenario: Optimize Multi-Collector Performance

Steps:

  1. Use shareIn for expensive upstream
  2. Configure SharingStarted strategy
  3. Set replay buffer size
  4. Test with multiple collectors

Example: Share relay subscription

val events: SharedFlow<Event> = client
    .reqAsFlow(relay, filters)
    .flatMapConcat { it.asFlow() }
    .shareIn(
        scope = viewModelScope,
        started = SharingStarted.WhileSubscribed(5000),
        replay = 0
    )

Anti-Patterns

Using GlobalScope

GlobalScope.launch { /* Leaks, no structured concurrency */ }

Use scoped coroutines

viewModelScope.launch { /* Cancelled with ViewModel */ }

Forgetting awaitClose

callbackFlow {
    registerCallback()
    // Missing cleanup!
}

Always cleanup

callbackFlow {
    registerCallback()
    awaitClose { unregisterCallback() }
}

Blocking in Flow

flow.map { Thread.sleep(1000); process(it) }

Suspend, don't block

flow.map { delay(1000); process(it) }.flowOn(Dispatchers.Default)

Ignoring backpressure

fastProducer.collect { slowConsumer(it) }  // Blocks producer!

Handle backpressure

fastProducer
    .buffer(64, BufferOverflow.DROP_OLDEST)
    .collect { slowConsumer(it) }

Delegation

Use kotlin-expert for:

  • Basic StateFlow/SharedFlow patterns
  • viewModelScope.launch usage
  • Simple MutableStateFlow → asStateFlow()

Use nostr-expert for:

  • Nostr protocol details (NIPs, event structure)
  • Event creation and signing
  • Cryptographic operations

This skill provides:

  • Advanced async patterns
  • Structured concurrency
  • Complex Flow

Content truncated.

How it compares

This skill focuses on advanced coroutine patterns like supervisorScope and callbackFlow for specific application architectures, unlike basic StateFlow/SharedFlow usage.

Compared to similar skills

kotlin-coroutines side by side with the closest alternatives in the catalog.

SkillInstallsUpdatedSafetyDifficulty
kotlin-coroutines (this skill)37moNo flagsAdvanced
survey-sdk-audit328dReviewIntermediate
android-kotlin74moNo flagsIntermediate
android-jetpack-compose-expert44moNo flagsAdvanced

Try saying

Example prompts that trigger this skill in your AI assistant.

More by vitorpamplona

View all by vitorpamplona

kotlin-multiplatform

vitorpamplona

Platform abstraction decision-making for Amethyst KMP project. Guides when to abstract vs keep platform-specific, source set placement (commonMain, jvmAndroid, platform-specific), expect/actual patterns. Covers primary targets (Android, JVM/Desktop, iOS) with web/wasm future considerations. Integrates with gradle-expert for dependency issues. Triggers on: abstraction decisions ("should I share this?"), source set placement questions, expect/actual creation, build.gradle.kts work, incorrect placement detection, KMP dependency suggestions.

32156

compose-expert

vitorpamplona

Advanced Compose Multiplatform UI patterns for shared composables. Use when working with visual UI components, state management patterns (remember, derivedStateOf, produceState), recomposition optimization (@Stable/@Immutable visual usage), Material3 theming, custom ImageVector icons, or determining whether to share UI in commonMain vs keep platform-specific. Delegates navigation to android-expert/desktop-expert. Complements kotlin-expert (handles Kotlin language aspects of state/annotations).

632

kotlin-expert

vitorpamplona

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.

526

nostr-expert

vitorpamplona

Nostr protocol implementation patterns in Quartz (AmethystMultiplatform's KMP Nostr library). Use when working with: (1) Nostr events (creating, parsing, signing), (2) Event kinds and tags, (3) NIP implementations (57 NIPs in quartz/), (4) Event builders and TagArrayBuilder DSL, (5) Nostr cryptography (secp256k1, NIP-44 encryption), (6) Relay communication patterns, (7) Bech32 encoding (npub, nsec, note, nevent). Complements nostr-protocol agent (NIP specs) - this skill provides Quartz codebase patterns and implementation details.

323

gradle-expert

vitorpamplona

Build optimization, dependency resolution, and multi-module KMP troubleshooting for AmethystMultiplatform. Use when working with: (1) Gradle build files (build.gradle.kts, settings.gradle), (2) Version catalog (libs.versions.toml), (3) Build errors and dependency conflicts, (4) Module dependencies and source sets, (5) Desktop packaging (DMG/MSI/DEB), (6) Build performance optimization, (7) Proguard/R8 configuration, (8) Common KMP + Android Gradle issues (Compose conflicts, secp256k1 JNI variants, source set problems).

19

Search skills

Search the agent skills registry