client/.github/skills/kotlin-concurrency-expert/SKILL.md
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.
npx skillsauth add ahaodev/heji 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.
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.
kotlinx-coroutines-android version, lifecycle-runtime-ktx version.viewModelScope, lifecycleScope, custom scope, or none).Dispatchers.Main) or intended to run off the main thread (Dispatchers.IO, Dispatchers.Default).Prefer edits that preserve existing behavior while satisfying structured concurrency and lifecycle safety.
Common fixes:
withContext(Dispatchers.IO) or Dispatchers.Default; ensure suspend functions are main-safe.GlobalScope with a lifecycle-bound scope (viewModelScope, lifecycleScope, or injected applicationScope).launchWhenStarted with repeatOnLifecycle(Lifecycle.State.STARTED).MutableStateFlow / MutableSharedFlow; expose read-only StateFlow or Flow.catch (e: Exception) blocks rethrow CancellationException.ensureActive() or yield() in tight loops for cooperative cancellation.callbackFlow with proper awaitClose cleanup.CoroutineDispatcher via constructor for 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) { ... }
}
// 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) }
}
// 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
}
// 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!
}
// 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
}
}
// CORRECT: callbackFlow with awaitClose
fun locationUpdates(): Flow<Location> = callbackFlow {
val listener = LocationListener { location ->
trySend(location)
}
locationManager.requestLocationUpdates(listener)
awaitClose { locationManager.removeUpdates(listener) }
}
| Scope | Use When | Lifecycle |
|-------|----------|-----------|
| viewModelScope | ViewModel operations | Cleared with ViewModel |
| lifecycleScope | UI operations in Activity/Fragment | Destroyed with lifecycle owner |
| repeatOnLifecycle | Flow collection in UI | Started/Stopped with lifecycle state |
| applicationScope (injected) | App-wide background work | Application lifetime |
| GlobalScope | NEVER USE | Breaks structured concurrency |
@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)
}
development
Apply Shadmin feature-development standards (backend Go/Gin/Ent + frontend React/TS). Use when adding/modifying features, CRUD modules, API routes/controllers/usecases/repositories, Ent schemas, or web pages/routes.
data-ai
Convert Android XML layouts to Jetpack Compose. Use when asked to migrate Views to Compose, convert XML to Composables, or modernize UI from View system to Compose.
development
Debug and optimize Android/Gradle build performance. Use when builds are slow, investigating CI/CD performance, analyzing build scans, or identifying compilation bottlenecks.
development
Best practices for building UI with Jetpack Compose, focusing on state hoisting, detailed performance optimizations, and theming. Use this when writing or refactoring Composable functions.