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…

INSTALLATION
npx skills add https://github.com/new-silvermoon/awesome-android-agent-skills --skill kotlin-concurrency-expert
Run in your project or agent environment. Adjust flags if your CLI version differs.

SKILL.md

$27

Common fixes:

  • ANR / Main thread blocking: Move heavy work to withContext(Dispatchers.IO) or Dispatchers.Default; ensure suspend functions are main-safe.
  • Memory leaks / zombie coroutines: Replace GlobalScope with a lifecycle-bound scope (viewModelScope, lifecycleScope, or injected applicationScope).
  • Lifecycle collection issues: Replace deprecated launchWhenStarted with repeatOnLifecycle(Lifecycle.State.STARTED).
  • State exposure: Encapsulate MutableStateFlow / MutableSharedFlow; expose read-only StateFlow or Flow.
  • CancellationException swallowing: Ensure generic catch (e: Exception) blocks rethrow CancellationException.
  • Non-cooperative cancellation: Add ensureActive() or yield() in tight loops for cooperative cancellation.
  • Callback APIs: Convert listeners to callbackFlow with proper awaitClose cleanup.
  • Hardcoded Dispatchers: Inject CoroutineDispatcher via constructor for testability.

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

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

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)

}

Reference Material

BrowserAct

Let your agent run on any real-world website

Bypass CAPTCHA & anti-bot for free. Start local, scale to cloud.

Explore BrowserAct Skills →

Stop writing automation&scrapers

Install the CLI. Run your first Skill in 30 seconds. Scale when you're ready.

Start free
free · no credit card