Awesome-android-agent-skills kotlin-concurrency-expert
Kotlin Coroutines review and remediation for Android. Use when asked to review concurrency usage, fix coroutine-related bugs, improve thread safety, or resolve lifecycle issues in Kotlin/Android code.
install
source · Clone the upstream repo
git clone https://github.com/new-silvermoon/awesome-android-agent-skills
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/new-silvermoon/awesome-android-agent-skills "$T" && mkdir -p ~/.claude/skills && cp -r "$T/.github/skills/concurrency_and_networking/kotlin-concurrency-expert" ~/.claude/skills/new-silvermoon-awesome-android-agent-skills-kotlin-concurrency-expert && rm -rf "$T"
manifest:
.github/skills/concurrency_and_networking/kotlin-concurrency-expert/SKILL.mdsource content
Kotlin Concurrency Expert
Overview
Review and fix Kotlin Coroutines issues in Android codebases by applying structured concurrency, lifecycle safety, proper scoping, and modern best practices with minimal behavior changes.
Workflow
1. Triage the Issue
- Capture the exact error, crash, or symptom (ANR, memory leak, race condition, incorrect state).
- Check project coroutines setup:
version,kotlinx-coroutines-android
version.lifecycle-runtime-ktx - Identify the current scope context (
,viewModelScope
, custom scope, or none).lifecycleScope - Confirm whether the code is UI-bound (
) or intended to run off the main thread (Dispatchers.Main
,Dispatchers.IO
).Dispatchers.Default - Verify Dispatcher injection patterns for testability.
2. Apply the Smallest Safe Fix
Prefer edits that preserve existing behavior while satisfying structured concurrency and lifecycle safety.
Common fixes:
- ANR / Main thread blocking: Move heavy work to
orwithContext(Dispatchers.IO)
; ensure suspend functions are main-safe.Dispatchers.Default - Memory leaks / zombie coroutines: Replace
with a lifecycle-bound scope (GlobalScope
,viewModelScope
, or injectedlifecycleScope
).applicationScope - Lifecycle collection issues: Replace deprecated
withlaunchWhenStarted
.repeatOnLifecycle(Lifecycle.State.STARTED) - State exposure: Encapsulate
/MutableStateFlow
; expose read-onlyMutableSharedFlow
orStateFlow
.Flow - CancellationException swallowing: Ensure generic
blocks rethrowcatch (e: Exception)
.CancellationException - Non-cooperative cancellation: Add
orensureActive()
in tight loops for cooperative cancellation.yield() - Callback APIs: Convert listeners to
with propercallbackFlow
cleanup.awaitClose - Hardcoded Dispatchers: Inject
via constructor for testability.CoroutineDispatcher
Critical Rules
Dispatcher Injection (Testability)
// CORRECT: Inject dispatcher class UserRepository( private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO ) { suspend fun fetchUser() = withContext(ioDispatcher) { ... } } // INCORRECT: Hardcoded dispatcher class UserRepository { suspend fun fetchUser() = withContext(Dispatchers.IO) { ... } }
Lifecycle-Aware Collection
// CORRECT: Use repeatOnLifecycle viewLifecycleOwner.lifecycleScope.launch { viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { viewModel.uiState.collect { state -> updateUI(state) } } } // INCORRECT: Direct collection (unsafe, deprecated) lifecycleScope.launchWhenStarted { viewModel.uiState.collect { state -> updateUI(state) } }
State Encapsulation
// CORRECT: Expose read-only StateFlow class MyViewModel : ViewModel() { private val _uiState = MutableStateFlow(UiState()) val uiState: StateFlow<UiState> = _uiState.asStateFlow() } // INCORRECT: Exposed mutable state class MyViewModel : ViewModel() { val uiState = MutableStateFlow(UiState()) // Leaks mutability }
Exception Handling
// CORRECT: Rethrow CancellationException try { doSuspendWork() } catch (e: CancellationException) { throw e // Must rethrow! } catch (e: Exception) { handleError(e) } // INCORRECT: Swallows cancellation try { doSuspendWork() } catch (e: Exception) { handleError(e) // CancellationException swallowed! }
Cooperative Cancellation
// CORRECT: Check for cancellation in tight loops suspend fun processLargeList(items: List<Item>) { items.forEach { item -> ensureActive() // Check cancellation processItem(item) } } // INCORRECT: Non-cooperative (ignores cancellation) suspend fun processLargeList(items: List<Item>) { items.forEach { item -> processItem(item) // Never checks cancellation } }
Callback Conversion
// CORRECT: callbackFlow with awaitClose fun locationUpdates(): Flow<Location> = callbackFlow { val listener = LocationListener { location -> trySend(location) } locationManager.requestLocationUpdates(listener) awaitClose { locationManager.removeUpdates(listener) } }
Scope Guidelines
| Scope | Use When | Lifecycle |
|---|---|---|
| ViewModel operations | Cleared with ViewModel |
| UI operations in Activity/Fragment | Destroyed with lifecycle owner |
| Flow collection in UI | Started/Stopped with lifecycle state |
(injected) | App-wide background work | Application lifetime |
| NEVER USE | Breaks structured concurrency |
Testing Pattern
@Test fun `loading data updates state`() = runTest { val testDispatcher = StandardTestDispatcher(testScheduler) val repository = FakeRepository() val viewModel = MyViewModel(repository, testDispatcher) viewModel.loadData() advanceUntilIdle() assertEquals(UiState.Success(data), viewModel.uiState.value) }