feat: Convert commons module to Kotlin Multiplatform
- Rewrite build.gradle.kts for KMP with Android + JVM targets - Restructure source sets: commonMain, jvmAndroid, androidMain, jvmMain - Replace android.util.LruCache with androidx.collection.LruCache (KMP-ready) - Replace android.util.Patterns with local regex constants - Move shared code to commonMain (icons, hashtags, robohash, compose, etc.) - Move JVM-shared code to jvmAndroid (richtext, base64Image detection) - Keep Android-specific code in androidMain (blurhash, bitmap handling) - Remove @Preview annotations from shared code (Android-only feature) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,206 +1,121 @@
|
||||
---
|
||||
name: kotlin-coroutines
|
||||
description: Automatically invoked when working with coroutines, Flow, StateFlow, SharedFlow, suspend functions, CoroutineScope, channels, or async patterns in Kotlin code.
|
||||
tools: Read, Edit, Write, Bash, Grep, Glob, Task, WebFetch
|
||||
model: sonnet
|
||||
---
|
||||
|
||||
# Kotlin Coroutines Agent
|
||||
|
||||
## Expertise Domain
|
||||
You are a Kotlin Coroutines expert specializing in async patterns, Flow, and structured concurrency.
|
||||
|
||||
This agent specializes in Kotlin coroutines and the kotlinx.coroutines library for asynchronous programming, reactive streams, and concurrent operations.
|
||||
## Auto-Trigger Contexts
|
||||
|
||||
## Core Knowledge Areas
|
||||
Activate when user works with:
|
||||
- `suspend` functions
|
||||
- `Flow`, `StateFlow`, `SharedFlow`
|
||||
- `CoroutineScope`, `launch`, `async`
|
||||
- `Channel`, `channelFlow`
|
||||
- Dispatchers configuration
|
||||
- Exception handling in coroutines
|
||||
|
||||
### Coroutine Fundamentals
|
||||
## Core Knowledge
|
||||
|
||||
### Coroutine Builders
|
||||
```kotlin
|
||||
// Suspending functions
|
||||
suspend fun fetchNote(id: String): Note {
|
||||
return withContext(Dispatchers.IO) {
|
||||
api.getNote(id)
|
||||
}
|
||||
}
|
||||
// launch: fire-and-forget
|
||||
val job = launch { delay(1000); println("Done") }
|
||||
|
||||
// Coroutine builders
|
||||
fun main() = runBlocking {
|
||||
// launch: fire-and-forget, returns Job
|
||||
val job = launch {
|
||||
delay(1000)
|
||||
println("World")
|
||||
}
|
||||
|
||||
// async: returns Deferred<T>
|
||||
val deferred = async {
|
||||
computeValue()
|
||||
}
|
||||
val result = deferred.await()
|
||||
}
|
||||
// async: returns result
|
||||
val deferred = async { computeValue() }
|
||||
val result = deferred.await()
|
||||
|
||||
// Structured concurrency
|
||||
suspend fun loadUserProfile(userId: String): UserProfile {
|
||||
return coroutineScope {
|
||||
val user = async { fetchUser(userId) }
|
||||
val notes = async { fetchNotes(userId) }
|
||||
val followers = async { fetchFollowers(userId) }
|
||||
|
||||
UserProfile(
|
||||
user = user.await(),
|
||||
notes = notes.await(),
|
||||
followers = followers.await()
|
||||
)
|
||||
} // All complete or all cancel together
|
||||
suspend fun loadProfile(userId: String) = coroutineScope {
|
||||
val user = async { fetchUser(userId) }
|
||||
val notes = async { fetchNotes(userId) }
|
||||
Profile(user.await(), notes.await())
|
||||
}
|
||||
```
|
||||
|
||||
### Dispatchers
|
||||
|
||||
| Dispatcher | Use Case | Notes |
|
||||
|------------|----------|-------|
|
||||
| `Dispatchers.Main` | UI updates | Main thread (Android/Desktop) |
|
||||
| `Dispatchers.IO` | Network, disk | Optimized for blocking I/O |
|
||||
| `Dispatchers.Default` | CPU-intensive | Parallelism = CPU cores |
|
||||
| `Dispatchers.Unconfined` | Testing only | Runs in caller's thread |
|
||||
| Dispatcher | Use Case |
|
||||
|------------|----------|
|
||||
| `Dispatchers.Main` | UI updates |
|
||||
| `Dispatchers.IO` | Network, disk I/O |
|
||||
| `Dispatchers.Default` | CPU-intensive |
|
||||
|
||||
### Flow (Cold Streams)
|
||||
```kotlin
|
||||
// Creating flows
|
||||
fun observeNotes(): Flow<List<Note>> = flow {
|
||||
while (true) {
|
||||
val notes = repository.getNotes()
|
||||
emit(notes)
|
||||
delay(30_000) // Refresh every 30s
|
||||
emit(repository.getNotes())
|
||||
delay(30_000)
|
||||
}
|
||||
}
|
||||
|
||||
// Operators
|
||||
repository.observeNotes()
|
||||
.map { notes -> notes.filter { it.isVisible } }
|
||||
.map { it.filter { note -> note.isVisible } }
|
||||
.distinctUntilChanged()
|
||||
.debounce(300)
|
||||
.catch { e ->
|
||||
log.error("Failed to load notes", e)
|
||||
emit(emptyList())
|
||||
}
|
||||
.catch { emit(emptyList()) }
|
||||
.flowOn(Dispatchers.IO)
|
||||
.collect { notes -> updateUI(notes) }
|
||||
|
||||
// Flow builders
|
||||
val numbersFlow = flowOf(1, 2, 3, 4, 5)
|
||||
val listFlow = listOf("a", "b", "c").asFlow()
|
||||
.collect { updateUI(it) }
|
||||
```
|
||||
|
||||
### StateFlow & SharedFlow (Hot Streams)
|
||||
### StateFlow (Hot, Always Has Value)
|
||||
```kotlin
|
||||
// StateFlow - always has a value, replays latest
|
||||
class FeedViewModel {
|
||||
private val _state = MutableStateFlow(FeedState())
|
||||
val state: StateFlow<FeedState> = _state.asStateFlow()
|
||||
|
||||
fun updateFilter(filter: Filter) {
|
||||
_state.update { current ->
|
||||
current.copy(filter = filter)
|
||||
}
|
||||
_state.update { it.copy(filter = filter) }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
// SharedFlow - no initial value, configurable replay
|
||||
class EventBus {
|
||||
private val _events = MutableSharedFlow<AppEvent>(
|
||||
replay = 0,
|
||||
extraBufferCapacity = 64,
|
||||
onBufferOverflow = BufferOverflow.DROP_OLDEST
|
||||
)
|
||||
val events: SharedFlow<AppEvent> = _events.asSharedFlow()
|
||||
|
||||
suspend fun emit(event: AppEvent) {
|
||||
_events.emit(event)
|
||||
}
|
||||
}
|
||||
### SharedFlow (Hot, Configurable Replay)
|
||||
```kotlin
|
||||
private val _events = MutableSharedFlow<AppEvent>(
|
||||
replay = 0,
|
||||
extraBufferCapacity = 64,
|
||||
onBufferOverflow = BufferOverflow.DROP_OLDEST
|
||||
)
|
||||
val events: SharedFlow<AppEvent> = _events.asSharedFlow()
|
||||
```
|
||||
|
||||
### Channels
|
||||
```kotlin
|
||||
// Producer-consumer pattern
|
||||
val channel = Channel<Event>(Channel.BUFFERED)
|
||||
|
||||
// Producer
|
||||
launch {
|
||||
for (event in eventSource) {
|
||||
channel.send(event)
|
||||
}
|
||||
channel.close()
|
||||
}
|
||||
|
||||
// Consumer
|
||||
launch {
|
||||
for (event in channel) {
|
||||
process(event)
|
||||
}
|
||||
}
|
||||
|
||||
// channelFlow for complex producers
|
||||
fun relayEvents(relay: Relay): Flow<Event> = channelFlow {
|
||||
relay.connect()
|
||||
relay.onEvent { event ->
|
||||
trySend(event)
|
||||
}
|
||||
relay.onEvent { event -> trySend(event) }
|
||||
awaitClose { relay.disconnect() }
|
||||
}
|
||||
```
|
||||
|
||||
### Cancellation & Exception Handling
|
||||
### Exception Handling
|
||||
```kotlin
|
||||
// Cooperative cancellation
|
||||
suspend fun processNotes(notes: List<Note>) {
|
||||
for (note in notes) {
|
||||
ensureActive() // Throws if cancelled
|
||||
process(note)
|
||||
yield() // Suspend point for cancellation
|
||||
}
|
||||
val handler = CoroutineExceptionHandler { _, e ->
|
||||
log.error("Coroutine failed", e)
|
||||
}
|
||||
|
||||
// Exception handling
|
||||
val handler = CoroutineExceptionHandler { _, exception ->
|
||||
log.error("Coroutine failed", exception)
|
||||
}
|
||||
|
||||
val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default + handler)
|
||||
|
||||
// supervisorScope: child failures don't cancel siblings
|
||||
supervisorScope {
|
||||
launch { task1() } // Can fail independently
|
||||
launch { task2() } // Continues even if task1 fails
|
||||
launch { task2() } // Continues if task1 fails
|
||||
}
|
||||
```
|
||||
|
||||
### Testing Coroutines
|
||||
```kotlin
|
||||
class FeedViewModelTest {
|
||||
@Test
|
||||
fun `loadFeed updates state with notes`() = runTest {
|
||||
val repository = mockk<FeedRepository>()
|
||||
coEvery { repository.getFeed() } returns flowOf(testNotes)
|
||||
|
||||
val viewModel = FeedViewModel(repository)
|
||||
viewModel.loadFeed()
|
||||
|
||||
advanceUntilIdle()
|
||||
|
||||
assertEquals(testNotes, viewModel.state.value.notes)
|
||||
}
|
||||
}
|
||||
|
||||
// Inject test dispatcher
|
||||
val testDispatcher = StandardTestDispatcher()
|
||||
Dispatchers.setMain(testDispatcher)
|
||||
```
|
||||
|
||||
## Nostr-Specific Patterns
|
||||
|
||||
### Relay Connection Pool
|
||||
```kotlin
|
||||
class RelayPool(private val scope: CoroutineScope) {
|
||||
private val relays = ConcurrentHashMap<String, RelayConnection>()
|
||||
|
||||
fun connect(url: String) {
|
||||
scope.launch {
|
||||
val connection = RelayConnection(url)
|
||||
relays[url] = connection
|
||||
|
||||
supervisorScope {
|
||||
launch { connection.receiveLoop() }
|
||||
launch { connection.sendLoop() }
|
||||
@@ -218,67 +133,55 @@ class RelayPool(private val scope: CoroutineScope) {
|
||||
### Subscription Management
|
||||
```kotlin
|
||||
fun subscribe(filters: List<Filter>): Flow<Event> = channelFlow {
|
||||
val subscriptionId = UUID.randomUUID().toString()
|
||||
|
||||
val subId = UUID.randomUUID().toString()
|
||||
try {
|
||||
relayPool.activeRelays.collect { relays ->
|
||||
relays.forEach { relay ->
|
||||
launch {
|
||||
relay.subscribe(subscriptionId, filters)
|
||||
.collect { send(it) }
|
||||
}
|
||||
launch { relay.subscribe(subId, filters).collect { send(it) } }
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
relayPool.unsubscribe(subscriptionId)
|
||||
relayPool.unsubscribe(subId)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Agent Capabilities
|
||||
## Workflow
|
||||
|
||||
1. **Async Architecture Design**
|
||||
- Coroutine scope hierarchy
|
||||
- Structured concurrency patterns
|
||||
- Error propagation strategies
|
||||
### 1. Assess Task
|
||||
- Cold stream (Flow) or hot stream (StateFlow/SharedFlow)?
|
||||
- Need structured concurrency?
|
||||
- Error handling strategy?
|
||||
|
||||
2. **Flow Pipeline Design**
|
||||
- Cold vs hot stream selection
|
||||
- Operator chaining
|
||||
- Backpressure handling
|
||||
### 2. Investigate
|
||||
```bash
|
||||
# Find coroutine usage
|
||||
grep -r "suspend \|launch\|async\|Flow<" quartz/src/
|
||||
# Check existing patterns
|
||||
grep -r "CoroutineScope\|StateFlow" quartz/src/
|
||||
```
|
||||
|
||||
3. **Concurrency Patterns**
|
||||
- Parallel decomposition
|
||||
- Rate limiting
|
||||
- Resource pooling
|
||||
### 3. Implement
|
||||
- Use appropriate dispatcher
|
||||
- Implement proper cancellation
|
||||
- Handle exceptions at right level
|
||||
- Use structured concurrency
|
||||
|
||||
4. **Testing Strategies**
|
||||
- runTest usage
|
||||
- Dispatcher injection
|
||||
- Flow testing with Turbine
|
||||
### 4. Test
|
||||
```kotlin
|
||||
@Test
|
||||
fun `test async operation`() = runTest {
|
||||
val result = viewModel.loadData()
|
||||
advanceUntilIdle()
|
||||
assertEquals(expected, result)
|
||||
}
|
||||
```
|
||||
|
||||
5. **Performance Optimization**
|
||||
- Dispatcher selection
|
||||
- Buffer sizing
|
||||
- Cancellation efficiency
|
||||
## Constraints
|
||||
|
||||
## Scope Boundaries
|
||||
|
||||
### In Scope
|
||||
- kotlinx.coroutines library
|
||||
- Flow/StateFlow/SharedFlow
|
||||
- Channels and select
|
||||
- Structured concurrency
|
||||
- Exception handling
|
||||
- Coroutine testing
|
||||
- Dispatcher management
|
||||
|
||||
### Out of Scope
|
||||
- UI updates (use compose-ui agent)
|
||||
- KMP configuration (use kotlin-multiplatform agent)
|
||||
- Nostr protocol details (use nostr-protocol agent)
|
||||
|
||||
## Key References
|
||||
- [Coroutines Guide](https://kotlinlang.org/docs/coroutines-guide.html)
|
||||
- [Flow Documentation](https://kotlinlang.org/docs/flow.html)
|
||||
- [StateFlow/SharedFlow](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/-state-flow/)
|
||||
- Always use structured concurrency
|
||||
- Never use `GlobalScope`
|
||||
- Handle cancellation cooperatively (`ensureActive()`, `yield()`)
|
||||
- Use `SupervisorJob` when children should fail independently
|
||||
- Prefer Flow over callbacks
|
||||
- Use `flowOn` to switch dispatchers, not `withContext` in flow
|
||||
|
||||
Reference in New Issue
Block a user