mcpbeat

Android Coroutines

livekit/android-coroutines

Implement and review Kotlin Coroutines on Android: configure CoroutineScopes, manage Dispatchers, expose StateFlow/SharedFlow from ViewModels, collect with repeatOnLifecycle, and convert callback APIs to callbackFlow. Use when writing suspend functions, wiring up Flow or StateFlow, fixing async bugs, replacing GlobalScope, or integrating coroutines with Android lifecycle components.

1k tokens
context cost
the whole folder, loaded on every use
2
files
instructions only
0
copies elsewhere
how many repositories repackaged it
350
stars on the repo
on the repository, not the skill itself

Install

one command, takes just this skill from the repository
npx skills add https://github.com/livekit/client-sdk-android --skill android-coroutines

What comes with it

285 bytes besides the instruction
.openskills.json

The instruction itself

16 sections, as written by the author

Android Coroutines Expert Skill

Workflow

  • Identify scope — Determine the correct CoroutineScope (viewModelScope, lifecycleScope, or injected applicationScope).
  • Wire data layer — Expose data as suspend functions (one-shot) or Flow (streams) with injected Dispatchers.
  • Connect UI — Collect flows using repeatOnLifecycle(Lifecycle.State.STARTED) and expose read-only StateFlow.
  • Verify — Run ./gradlew test and confirm coroutine behavior. If tests fail, check: uncaught CancellationException in generic catch blocks, GlobalScope usage causing leaked coroutines, missing awaitClose in callbackFlow, or hardcoded Dispatchers breaking test determinism. Run ./gradlew detektDebug to catch structural issues.

Critical Rules

1. Dispatcher Injection (Testability)

  • NEVER hardcode Dispatchers (e.g., Dispatchers.IO, Dispatchers.Default) inside classes.
  • ALWAYS inject a CoroutineDispatcher via the constructor.
  • DEFAULT to Dispatchers.IO in the constructor argument for convenience, but allow it to be overridden.
// CORRECT
class UserRepository(
    private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO
) { ... }

// INCORRECT
class UserRepository {
    fun getData() = withContext(Dispatchers.IO) { ... }
}

2. Main-Safety

  • All suspend functions in Data/Domain layers must be main-safe — use withContext(dispatcher) internally.
  • One-shot calls: expose as suspend functions. Data streams: expose as Flow.

3. Lifecycle-Aware Collection

  • NEVER collect a flow directly in lifecycleScope.launch or launchWhenStarted (deprecated/unsafe).
  • ALWAYS use repeatOnLifecycle(Lifecycle.State.STARTED) for collecting flows in Activities or Fragments.
// CORRECT
viewLifecycleOwner.lifecycleScope.launch {
    viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
        viewModel.uiState.collect { ... }
    }
}

4. ViewModel Scope Usage

  • Use viewModelScope for initiating coroutines in ViewModels.
  • Do not expose suspend functions from the ViewModel to the View. The ViewModel should expose StateFlow or SharedFlow that the View observes.

5. Mutable State Encapsulation

  • NEVER expose MutableStateFlow or MutableSharedFlow publicly.
  • Expose them as read-only StateFlow or Flow using .asStateFlow() or upcasting.

6. GlobalScope Prohibition

  • NEVER use GlobalScope. It breaks structured concurrency and leads to leaks.
  • If a task must survive the current scope, use an injected applicationScope (a custom scope tied to the Application lifecycle).

7. Exception Handling

  • NEVER catch CancellationException in a generic catch (e: Exception) block without rethrowing it.
  • Use runCatching only if you explicitly rethrow CancellationException.
  • Use CoroutineExceptionHandler only for top-level coroutines (inside launch). It has no effect inside async or child coroutines.

8. Cancellability

  • ALWAYS call ensureActive() or yield() in tight loops (e.g., processing a large list, reading files).
  • delay() and withContext() are already cancellable — no extra checks needed there.

9. Callback Conversion

  • Use callbackFlow to convert callback-based APIs to Flow.
  • ALWAYS use awaitClose at the end of the callbackFlow block to unregister listeners.

Code Patterns

Repository Pattern with Flow

class NewsRepository(
    private val remoteDataSource: NewsRemoteDataSource,
    private val externalScope: CoroutineScope, // For app-wide events
    private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO
) {
    val newsUpdates: Flow<List<News>> = flow {
        val news = remoteDataSource.fetchLatestNews()
        emit(news)
    }.flowOn(ioDispatcher) // Upstream executes on IO
}

Parallel Execution

suspend fun loadDashboardData() = coroutineScope {
    val userDeferred = async { userRepo.getUser() }
    val feedDeferred = async { feedRepo.getFeed() }
    
    // Wait for both
    DashboardData(
        user = userDeferred.await(),
        feed = feedDeferred.await()
    )
}

Testing with runTest

@Test
fun testViewModel() = runTest {
    val testDispatcher = StandardTestDispatcher(testScheduler)
    val viewModel = MyViewModel(testDispatcher)
    
    viewModel.loadData()
    advanceUntilIdle() // Process coroutines
    
    assertEquals(expectedState, viewModel.uiState.value)
}

How to use it

Copy the folder

Take livekit/android-coroutines from the repository into ~/.claude/skills for personal use, or into .claude/skills inside a project.

Check the name does not clash

The agent identifies a skill by the name field in its header. Two skills with the same name cannot sit side by side — one of them will be ignored.