Android kotlin coroutines
Skill and3r817/dot-claude-plugins/android-kotlin-coroutines/skills/android-kotlin-coroutines
A small collection of plugins I use with Claude Code
npx -y skills add and3r817/dot-claude-plugins --skill android-kotlin-coroutinesAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 2 stars2 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
Android development with Kotlin Coroutines and Flow. Use when user mentions "coroutines", "suspend", "Flow", "StateFlow", "SharedFlow", "viewModelScope", "lifecycleScope", or needs to implement async programming, handle structured concurrency, integrate coroutines with Retrofit/Room/WorkManager, or write coroutine tests. Triggers on Android concurrency, async, or Flow tasks.
SKILL.md
13.8 KB, as published. Nobody here has run it
Android Kotlin Coroutines
Expert guidance for Android development using Kotlin Coroutines and Flow for asynchronous programming with structured concurrency.
When to Use This Skill
Invoke this skill when:
- Implementing asynchronous operations in Android
- Managing concurrent operations with structured concurrency
- Using Flow for reactive data streams (StateFlow, SharedFlow, callbackFlow)
- Integrating coroutines with Retrofit, Room, or WorkManager
- Handling cancellation and error propagation
- Writing coroutine-based unit tests
- User explicitly mentions "coroutines", "async", "Flow", "suspend", or related patterns
Core Principles
- Structured Concurrency — Coroutines bound to lifecycle prevent leaks
- Suspend over Callbacks — Transform callback APIs to suspend functions
- Main-Safety — Suspend functions safe to call from main thread
- Flow for Streams — Use Flow for multiple values over time
Quick Reference
Essential Dependencies
// build.gradle.kts (app)
dependencies {
// Coroutines core
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.0")
// Lifecycle-aware scopes
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.7.0")
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.7.0")
// Flow integration with Compose
implementation("androidx.lifecycle:lifecycle-runtime-compose:2.7.0")
// Testing
testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.8.0")
testImplementation("app.cash.turbine:turbine:1.0.0")
}
Coroutine Scopes in Android
ViewModel Scope
@HiltViewModel
class UserViewModel @Inject constructor(
private val repository: UserRepository
) : ViewModel() {
private val _uiState = MutableStateFlow(UserUiState())
val uiState: StateFlow<UserUiState> = _uiState.asStateFlow()
init {
loadUser()
}
private fun loadUser() {
// Automatically cancelled when ViewModel is cleared
viewModelScope.launch {
_uiState.update { it.copy(isLoading = true) }
repository.getUser()
.onSuccess { user ->
_uiState.update { it.copy(user = user, isLoading = false) }
}
.onFailure { error ->
_uiState.update { it.copy(error = error.message, isLoading = false) }
}
}
}
fun refresh() {
viewModelScope.launch {
repository.sync()
}
}
}
Lifecycle Scope
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Cancelled when Activity is destroyed
lifecycleScope.launch {
viewModel.events.collect { event ->
handleEvent(event)
}
}
// Only runs when at least STARTED
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.uiState.collect { state ->
updateUI(state)
}
}
}
}
}
// In Compose - preferred approach
@Composable
fun UserScreen(viewModel: UserViewModel = hiltViewModel()) {
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
UserContent(uiState = uiState)
}
Custom Scopes with SupervisorJob
class SearchManager(
private val searchApi: SearchApi,
private val dispatcher: CoroutineDispatcher = Dispatchers.IO
) {
// SupervisorJob: child failure doesn't cancel siblings
private val scope = CoroutineScope(SupervisorJob() + dispatcher)
fun search(query: String): Flow<SearchResult> = flow {
val results = searchApi.search(query)
emit(results)
}.flowOn(dispatcher)
fun cancel() {
scope.cancel()
}
}
Suspend Functions
Basic Patterns
// Main-safe suspend function (moves work off main thread internally)
suspend fun fetchUser(id: String): User = withContext(Dispatchers.IO) {
api.getUser(id)
}
// Parallel decomposition
suspend fun fetchUserWithPosts(id: String): UserWithPosts = coroutineScope {
val userDeferred = async { api.getUser(id) }
val postsDeferred = async { api.getPosts(userId = id) }
UserWithPosts(
user = userDeferred.await(),
posts = postsDeferred.await()
)
}
// Timeout
suspend fun fetchWithTimeout(id: String): User? {
return withTimeoutOrNull(5000L) {
api.getUser(id)
}
}
Converting Callbacks to Suspend
// suspendCancellableCoroutine for one-shot callbacks
suspend fun Location.awaitLastLocation(): Location = suspendCancellableCoroutine { cont ->
val client = LocationServices.getFusedLocationProviderClient(context)
client.lastLocation
.addOnSuccessListener { location ->
cont.resume(location)
}
.addOnFailureListener { e ->
cont.resumeWithException(e)
}
cont.invokeOnCancellation {
// Cleanup if coroutine is cancelled
}
}
// For Firebase/Firestore
suspend fun DocumentReference.awaitGet(): DocumentSnapshot = suspendCancellableCoroutine { cont ->
get()
.addOnSuccessListener { cont.resume(it) }
.addOnFailureListener { cont.resumeWithException(it) }
}
Flow Patterns
StateFlow for UI State
@HiltViewModel
class ProductListViewModel @Inject constructor(
private val repository: ProductRepository
) : ViewModel() {
private val _searchQuery = MutableStateFlow("")
// Derived state from search query
val products: StateFlow<ProductsUiState> = _searchQuery
.debounce(300)
.flatMapLatest { query ->
repository.searchProducts(query)
.map { ProductsUiState.Success(it) }
.catch { emit(ProductsUiState.Error(it.message)) }
}
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5000),
initialValue = ProductsUiState.Loading
)
fun updateSearch(query: String) {
_searchQuery.value = query
}
}
sealed interface ProductsUiState {
data object Loading : ProductsUiState
data class Success(val products: List<Product>) : ProductsUiState
data class Error(val message: String?) : ProductsUiState
}
SharedFlow for Events
@HiltViewModel
class CheckoutViewModel @Inject constructor(
private val orderRepository: OrderRepository
) : ViewModel() {
// Events that should not be replayed
private val _events = MutableSharedFlow<CheckoutEvent>()
val events: SharedFlow<CheckoutEvent> = _events.asSharedFlow()
fun placeOrder(order: Order) {
viewModelScope.launch {
orderRepository.placeOrder(order)
.onSuccess { orderId ->
_events.emit(CheckoutEvent.OrderSuccess(orderId))
}
.onFailure { e ->
_events.emit(CheckoutEvent.OrderFailed(e.message ?: "Unknown error"))
}
}
}
}
sealed interface CheckoutEvent {
data class OrderSuccess(val orderId: String) : CheckoutEvent
data class OrderFailed(val message: String) : CheckoutEvent
}
// Collecting in Compose
@Composable
fun CheckoutScreen(
viewModel: CheckoutViewModel = hiltViewModel(),
onNavigateToConfirmation: (String) -> Unit
) {
val snackbarHostState = remember { SnackbarHostState() }
LaunchedEffect(Unit) {
viewModel.events.collect { event ->
when (event) {
is CheckoutEvent.OrderSuccess -> onNavigateToConfirmation(event.orderId)
is CheckoutEvent.OrderFailed -> snackbarHostState.showSnackbar(event.message)
}
}
}
// Screen content
}
callbackFlow for Continuous Streams
// Location updates as Flow
fun LocationClient.locationUpdates(
request: LocationRequest
): Flow<Location> = callbackFlow {
val callback = object : LocationCallback() {
override fun onLocationResult(result: LocationResult) {
result.lastLocation?.let { trySend(it) }
}
}
requestLocationUpdates(request, callback, Looper.getMainLooper())
awaitClose {
removeLocationUpdates(callback)
}
}
// Firebase Realtime Database listener
fun DatabaseReference.valueEvents(): Flow<DataSnapshot> = callbackFlow {
val listener = object : ValueEventListener {
override fun onDataChange(snapshot: DataSnapshot) {
trySend(snapshot)
}
override fun onCancelled(error: DatabaseError) {
close(error.toException())
}
}
addValueEventListener(listener)
awaitClose { removeEventListener(listener) }
}
Flow Operators
// Combining flows
val userWithSettings: Flow<UserWithSettings> = combine(
userFlow,
settingsFlow
) { user, settings ->
UserWithSettings(user, settings)
}
// Retry with exponential backoff
fun <T> Flow<T>.retryWithBackoff(
maxRetries: Int = 3,
initialDelay: Long = 1000
): Flow<T> = retryWhen { cause, attempt ->
if (attempt < maxRetries && cause is IOException) {
delay(initialDelay * (2.0.pow(attempt.toInt())).toLong())
true
} else {
false
}
}
// Throttle/sample
val throttledClicks = clickFlow
.sample(300) // Emit at most once per 300ms
Error Handling
Try-Catch in Coroutines
viewModelScope.launch {
try {
val user = repository.getUser(id)
_uiState.update { it.copy(user = user) }
} catch (e: CancellationException) {
throw e // Always rethrow cancellation
} catch (e: Exception) {
_uiState.update { it.copy(error = e.message) }
}
}
Result Pattern
class UserRepository @Inject constructor(
private val api: UserApi,
@IoDispatcher private val dispatcher: CoroutineDispatcher
) {
suspend fun getUser(id: String): Result<User> = withContext(dispatcher) {
runCatching {
api.getUser(id)
}
}
}
// Usage
viewModelScope.launch {
repository.getUser(id)
.onSuccess { user -> _uiState.update { it.copy(user = user) } }
.onFailure { e -> _uiState.update { it.copy(error = e.message) } }
}
CoroutineExceptionHandler
// Global handler for uncaught exceptions
val handler = CoroutineExceptionHandler { _, exception ->
Timber.e(exception, "Uncaught exception")
analytics.trackError(exception)
}
val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main + handler)
Dispatcher Injection
// Qualifiers
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class IoDispatcher
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class MainDispatcher
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class DefaultDispatcher
// Hilt module
@Module
@InstallIn(SingletonComponent::class)
object DispatcherModule {
@Provides
@IoDispatcher
fun provideIoDispatcher(): CoroutineDispatcher = Dispatchers.IO
@Provides
@MainDispatcher
fun provideMainDispatcher(): CoroutineDispatcher = Dispatchers.Main
@Provides
@DefaultDispatcher
fun provideDefaultDispatcher(): CoroutineDispatcher = Dispatchers.Default
}
// Repository using injected dispatcher
class DataRepository @Inject constructor(
private val api: DataApi,
@IoDispatcher private val ioDispatcher: CoroutineDispatcher
) {
suspend fun fetchData(): List<Data> = withContext(ioDispatcher) {
api.getData()
}
}
Common Anti-Patterns
❌ Avoid
// DON'T: GlobalScope - no structured concurrency
GlobalScope.launch {
repository.sync()
}
// DON'T: Blocking the main thread
runBlocking {
api.fetchData() // Blocks main thread!
}
// DON'T: Swallowing CancellationException
try {
suspendFunction()
} catch (e: Exception) { // Catches CancellationException!
log(e)
}
// DON'T: Using flow { } for single values
fun getUser(): Flow<User> = flow {
emit(api.getUser()) // Use suspend function instead
}
// DON'T: Collecting in ViewModel init without proper scope
init {
repository.dataFlow.collect { data -> // Suspends forever in init
_uiState.value = data
}
}
✅ Correct
// DO: Use viewModelScope
viewModelScope.launch {
repository.sync()
}
// DO: Use suspendCancellableCoroutine properly
suspend fun fetch() = withContext(Dispatchers.IO) {
api.fetchData()
}
// DO: Rethrow CancellationException
try {
suspendFunction()
} catch (e: CancellationException) {
throw e
} catch (e: Exception) {
log(e)
}
// DO: Use suspend for single values
suspend fun getUser(): User = api.getUser()
// DO: Launch collection properly
init {
viewModelScope.launch {
repository.dataFlow.collect { data ->
_uiState.value = data
}
}
}
Resources
For detailed patterns and integration guides, see:
references/flow-patterns.md— Advanced Flow operations, hot/cold flows, sharing strategiesreferences/testing-coroutines.md— Unit testing with Turbine, TestDispatcher, runTestreferences/integrations.md— Retrofit, Room, WorkManager, Firebase integration patterns
When to Consult References
- Complex Flow transformations →
flow-patterns.md - Testing suspend functions and Flows →
testing-coroutines.md - Retrofit/Room integration setup →
integrations.md - WorkManager with coroutines →
integrations.md