install
source · Clone the upstream repo
git clone https://github.com/ComeOnOliver/skillshub
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/ComeOnOliver/skillshub "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/new-silvermoon/awesome-android-agent-skills/android-coroutines" ~/.claude/skills/comeonoliver-skillshub-android-coroutines && rm -rf "$T"
manifest:
skills/new-silvermoon/awesome-android-agent-skills/android-coroutines/SKILL.mdsource content
Android Coroutines Expert Skill
This skill provides authoritative rules and patterns for writing production-quality Kotlin Coroutines code on Android. It enforces structured concurrency, lifecycle safety, and modern best practices (2025 standards).
Responsibilities
- Asynchronous Logic: Implementing suspend functions, Dispatcher management, and parallel execution.
- Reactive Streams: Implementing
,Flow
,StateFlow
, andSharedFlow
.callbackFlow - Lifecycle Integration: Managing scopes (
,viewModelScope
) and safe collection (lifecycleScope
).repeatOnLifecycle - Error Handling: Implementing
,CoroutineExceptionHandler
, and properSupervisorJob
hierarchies.try-catch - Cancellability: Ensuring long-running operations are cooperative using
.ensureActive() - Testing: Setting up
andTestDispatcher
.runTest
Applicability
Activate this skill when the user asks to:
- "Fetch data from an API/Database."
- "Perform background processing."
- "Fix a memory leak" related to threads/tasks.
- "Convert a listener/callback to Coroutines."
- "Implement a ViewModel."
- "Handle UI state updates."
Critical Rules & Constraints
1. Dispatcher Injection (Testability)
- NEVER hardcode Dispatchers (e.g.,
,Dispatchers.IO
) inside classes.Dispatchers.Default - ALWAYS inject a
via the constructor.CoroutineDispatcher - DEFAULT to
in the constructor argument for convenience, but allow it to be overridden.Dispatchers.IO
// CORRECT class UserRepository( private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO ) { ... } // INCORRECT class UserRepository { fun getData() = withContext(Dispatchers.IO) { ... } }
2. Main-Safety
- All suspend functions defined in the Data or Domain layer must be main-safe.
- One-shot calls should be exposed as
functions.suspend - Data changes should be exposed as
.Flow - The caller (ViewModel) should be able to call them from
without blocking the UI.Dispatchers.Main - Use
inside the repository implementation to move execution to the background.withContext(dispatcher)
3. Lifecycle-Aware Collection
- NEVER collect a flow directly in
orlifecycleScope.launch
(deprecated/unsafe).launchWhenStarted - ALWAYS use
for collecting flows in Activities or Fragments.repeatOnLifecycle(Lifecycle.State.STARTED)
// CORRECT viewLifecycleOwner.lifecycleScope.launch { viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { viewModel.uiState.collect { ... } } }
4. ViewModel Scope Usage
- Use
for initiating coroutines in ViewModels.viewModelScope - Do not expose suspend functions from the ViewModel to the View. The ViewModel should expose
orStateFlow
that the View observes.SharedFlow
5. Mutable State Encapsulation
- NEVER expose
orMutableStateFlow
publicly.MutableSharedFlow - Expose them as read-only
orStateFlow
usingFlow
or upcasting..asStateFlow()
6. GlobalScope Prohibition
- NEVER use
. It breaks structured concurrency and leads to leaks.GlobalScope - If a task must survive the current scope, use an injected
(a custom scope tied to the Application lifecycle).applicationScope
7. Exception Handling
- NEVER catch
in a genericCancellationException
block without rethrowing it.catch (e: Exception) - Use
only if you explicitly rethrowrunCatching
.CancellationException - Use
only for top-level coroutines (insideCoroutineExceptionHandler
). It has no effect insidelaunch
or child coroutines.async
8. Cancellability
- Coroutines feature cooperative cancellation. They don't stop immediately unless they check for cancellation.
- ALWAYS call
orensureActive()
in tight loops (e.g., processing a large list, reading files) to check for cancellation.yield() - Standard functions like
anddelay()
are already cancellable.withContext()
9. Callback Conversion
- Use
to convert callback-based APIs to Flow.callbackFlow - ALWAYS use
at the end of theawaitClose
block to unregister listeners.callbackFlow
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) }