agentsclimarketplace

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

Install
npx -y skills add and3r817/dot-claude-plugins --skill android-kotlin-coroutines

Assembled 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

  1. Structured Concurrency — Coroutines bound to lifecycle prevent leaks
  2. Suspend over Callbacks — Transform callback APIs to suspend functions
  3. Main-Safety — Suspend functions safe to call from main thread
  4. 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 strategies
  • references/testing-coroutines.md — Unit testing with Turbine, TestDispatcher, runTest
  • references/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

Keep looking

Skills are one crate of 328,083. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.