.claude/skills/kotlin-concurrency-expert/SKILL.md
Diagnose and fix Kotlin coroutine race conditions, Flow issues, and thread safety bugs
npx skillsauth add ChooJeongHo/MovieFinder kotlin-concurrency-expertInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Analyze $ARGUMENTS for race conditions, coroutine misuse, and thread safety issues.
// UNSAFE: two coroutines can both pass the check
if (!isLoading) {
isLoading = true
fetchData()
}
// SAFE: Mutex.tryLock() is atomic
if (loadMutex.tryLock()) {
try { fetchData() } finally { loadMutex.unlock() }
}
→ In this project: DetailViewModel uses Mutex.tryLock() for API dedup, toggleMutex.withLock() for FAB debounce.
// UNSAFE: read-modify-write is not atomic
_uiState.value = _uiState.value.copy(isLoading = true) // concurrent writers corrupt state
// SAFE: update{} is atomic
_uiState.update { it.copy(isLoading = true) }
// UNSAFE: each collector triggers independent upstream (DB query runs N times)
val data = repository.getData() // cold Flow, no sharing
// SAFE: stateIn shares a single upstream subscription
val data = repository.getData()
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), emptyList())
// UNSAFE: catches CancellationException, breaks structured concurrency
try { ... } catch (e: Exception) { handleError(e) }
// SAFE: rethrow CancellationException
try { ... } catch (e: CancellationException) { throw e } catch (e: Exception) { handleError(e) }
// UNSAFE: GlobalScope survives ViewModel destruction → leak
GlobalScope.launch { repository.save() }
// SAFE: viewModelScope (auto-cancelled on ViewModel clear)
viewModelScope.launch { repository.save() }
// For fire-and-forget that must outlive a specific coroutine:
// use a separate viewModelScope.launch{} block, NOT coroutineScope{}
// In this project: snackbar events use Channel.CONFLATED
// CONFLATED = only latest event kept if consumer is slow
// Use receiveAsFlow() — NOT consumeAsFlow() (single-consumer only)
val events = _channel.receiveAsFlow()
// UNSAFE: Room/Retrofit called on Main thread
flow { emit(dao.query()) } // no dispatcher → runs on caller's dispatcher
// SAFE: flowOn moves upstream to IO
repository.getData().flowOn(Dispatchers.IO)
// In this project: use flowOn in Repository, NOT in ViewModel
// UNSAFE: nested withLock on same Mutex → deadlock
mutex.withLock {
mutex.withLock { ... } // hangs forever
}
// Check: loadMutex and toggleMutex must be SEPARATE instances in DetailViewModel
| Component | Mechanism | Purpose |
|-----------|-----------|---------|
| DetailViewModel.loadMutex | Mutex.tryLock() | Prevent duplicate API calls |
| DetailViewModel.toggleMutex | Mutex.withLock() | FAB toggle debounce |
| WatchGoalNotificationHelper | Mutex.withLock() | Atomic goal-check + notify |
| RateLimiter | AtomicLong.compareAndSet | 2s retry cooldown |
| CircuitBreaker | @Synchronized | State transition atomicity |
| All ViewModels | Channel.CONFLATED | One-shot Snackbar events |
| Home/Detail flows | stateIn(WhileSubscribed(5000)) | Shared upstream, 5s buffer |
## Concurrency Analysis: [Target]
### Race Conditions Found
- **[CRITICAL/WARNING]** (file:line): Pattern → Risk → Fix
### Thread Safety Assessment
| Variable/Flow | Access Pattern | Safe? | Fix Needed |
|---------------|---------------|-------|------------|
| _uiState | update{} | ✅ | - |
| isLoading | read-then-write | ❌ | Use Mutex |
### Scope & Lifecycle
[Are all coroutines in appropriate scopes? Any leak risk?]
### Dispatcher Correctness
[Are blocking calls on IO? UI updates on Main?]
### Recommended Fixes
[Concrete before/after code for each issue]
testing
Write and fix Unit, Hilt integration, and Espresso tests following MovieFinder patterns
development
Systematically diagnose and fix bugs using evidence-driven root cause analysis
development
Optimize Gradle build speed with Configuration Cache, parallel execution, and AGP 9 tuning
development
Enforce Clean Architecture and Offline-first patterns in this MovieFinder codebase