Composables

Shared Coroutines

Community

ahmed3elshaer

Shared coroutines configuration for Kotlin Multiplatform. Platform dispatchers, Flow sharing, and common scope patterns.

Overview

Repository
ahmed3elshaer/everything-claude-code-mobile
Publisher
ahmed3elshaer
Stars
66
Forks
18
Bundled files
0
License
MIT
Updated
Jun 14, 2026

Installation

tmp=$(mktemp -d)
git clone --depth 1 https://github.com/ahmed3elshaer/everything-claude-code-mobile.git "$tmp"
mkdir -p ~/.claude/skills
cp -R "$tmp/skills/shared-coroutines" ~/.claude/skills/shared-coroutines
rm -rf "$tmp"

Instructions

Shared Coroutines for KMP

Configure coroutines for cross-platform async operations with platform-appropriate dispatchers.

Dependencies

// build.gradle.kts
sourceSets {
    val commonMain by getting {
        dependencies {
            implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3")
            implementation("org.jetbrains.kotlinx:kotlinx-datetime:1.6.0")
        }
    }
    val androidMain by getting {
        dependencies {
            implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3")
        }
    }
}

Platform Dispatchers

// commonMain/kotlin/coroutines/PlatformDispatcher.kt
interface PlatformDispatcher {
    val main: CoroutineDispatcher
    val io: CoroutineDispatcher
    val default: CoroutineDispatcher
    val unconfined: CoroutineDispatcher
}

expect object PlatformDispatchers : PlatformDispatcher
Android Implementation
// androidMain/kotlin/coroutines/PlatformDispatcher.android.kt
actual object PlatformDispatchers : PlatformDispatcher {
    actual val main: CoroutineDispatcher = Dispatchers.Main
    actual val io: CoroutineDispatcher = Dispatchers.IO
    actual val default: CoroutineDispatcher = Dispatchers.Default
    actual val unconfined: CoroutineDispatcher = Dispatchers.Unconfined
}
iOS Implementation
// iosMain/kotlin/coroutines/PlatformDispatcher.ios.kt
actual object PlatformDispatchers : PlatformDispatcher {
    actual val main: CoroutineDispatcher =
        NSQueueDispatcher(dispatch_get_main_queue())

    actual val io: CoroutineDispatcher =
        DispatchQueueDispatcher(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT.toLong(), 0))

    actual val default: CoroutineDispatcher =
        DispatchQueueDispatcher(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT.toLong(), 0))

    actual val unconfined: CoroutineDispatcher = Dispatchers.Unconfined
}

class NSQueueDispatcher(
    private val queue: dispatch_queue_t
) : CoroutineDispatcher() {
    override fun dispatch(context: CoroutineContext, block: Runnable) {
        dispatch_async(queue) { block.run() }
    }
}

class DispatchQueueDispatcher(
    private val queue: dispatch_queue_t
) : CoroutineDispatcher() {
    override fun dispatch(context: CoroutineContext, block: Runnable) {
        dispatch_async(queue) { block.run() }
    }
}

Common Scopes

// commonMain/kotlin/coroutines/Scopes.kt
// Application scope - lives for app lifetime
class ApplicationScope : CoroutineScope {
    private val job = SupervisorJob()

    override val coroutineContext: CoroutineContext
        get() = job + PlatformDispatchers.main +
        CoroutineExceptionHandler { _, throwable ->
            println("Unhandled exception: $throwable")
        }

    fun cancel() {
        job.cancel()
    }
}

// View scope - tied to a view/presenter lifecycle
class ViewScope : CoroutineScope {
    private val job = SupervisorJob()

    override val coroutineContext: CoroutineContext
        get() = job + PlatformDispatchers.main

    fun cancel() {
        job.cancelChildren()
        job.cancel()
    }
}

// Use case scope - for single operations
fun useCaseScope(): CoroutineScope =
    CoroutineScope(
        SupervisorJob() +
        PlatformDispatchers.io +
        CoroutineExceptionHandler { _, throwable ->
            println("Use case failed: $throwable")
        }
    )

Flow Sharing Patterns

StateFlow for UI State
// commonMain/kotlin/coroutines/StateFlowExtensions.kt
fun <T> StateFlow<T>.shareIn(
    scope: CoroutineScope,
    started: SharingStarted = SharingStarted.WhileSubscribed(5000),
    replay: Int = 1
): StateFlow<T> {
    return this
}

// Multiplatform caching
fun <T> Flow<T>.cachedIn(
    scope: CoroutineScope
): Flow<T> = cached(scope)
Connect to Platform Lifecycle
// commonMain/kotlin/coroutines/LifecycleAwareFlow.kt
fun <T> Flow<T>.collectWhileActive(
    lifecycle: Lifecycle,
    context: CoroutineContext = PlatformDispatchers.main,
    collector: suspend (value: T) -> Unit
): Job {
    val job = SupervisorJob()
    val scope = CoroutineScope(job + context)

    lifecycle.subscribe { event ->
        when (event) {
            Lifecycle.Event.ON_RESUME -> {
                // Start collecting
                scope.launch {
                    collect(collector)
                }
            }
            Lifecycle.Event.ON_PAUSE -> {
                // Cancel collection
                job.cancelChildren()
            }
            else -> { /* ignore */ }
        }
    }

    return job
}

// Simple repeat on view active
fun <T> Flow<T>.repeatOnViewActive(
    isActive: () -> Boolean,
    minInterval: Duration = 1.seconds
): Flow<T> = channelFlow {
    var lastValue: T? = null
    val collectorJob = launch {
        collect { value ->
            lastValue = value
            if (isActive()) {
                send(value)
            }
        }
    }

    while (isActive()) {
        lastValue?.let { send(it) }
        delay(minInterval)
    }

    collectorJob.cancel()
}

Repository Layer Coroutines

// commonMain/kotlin/data/repository/BaseRepository.kt
abstract class BaseRepository {

    protected suspend fun <T> execute(
        dispatcher: CoroutineDispatcher = PlatformDispatchers.io,
        block: suspend () -> T
    ): Result<T> = withContext(dispatcher) {
        try {
            Result.success(block())
        } catch (e: CancellationException) {
            throw e
        } catch (e: Exception) {
            Result.failure(e)
        }
    }

    protected suspend fun <T> flowFrom(
        dispatcher: CoroutineDispatcher = PlatformDispatchers.io,
        block: suspend () -> T
    ): Flow<Result<T>> = flow {
        emit(withContext(dispatcher) {
            try {
                Result.success(block())
            } catch (e: CancellationException) {
                throw e
            } catch (e: Exception) {
                Result.failure(e)
            }
        })
    }

    // For single-shot database operations
    protected suspend fun <T> databaseQuery(
        block: suspend () -> T
    ): T = withContext(PlatformDispatchers.io) {
        block()
    }

    // For network operations with timeout
    protected suspend fun <T> networkCall(
        timeout: Duration = 30.seconds,
        block: suspend () -> T
    ): T = withTimeout(timeout) {
        withContext(PlatformDispatchers.io) {
            block()
        }
    }
}

// Usage
class UserRepository(
    private val localDataSource: UserLocalDataSource,
    private val remoteDataSource: UserRemoteDataSource
) : BaseRepository() {

    suspend fun getUser(id: String): Result<User> = execute {
        remoteDataSource.getUser(id).also { user ->
            localDataSource.saveUser(user)
        }
    }

    fun observeUser(id: String): Flow<Result<User>> = flowFrom {
        localDataSource.getUser(id) ?: remoteDataSource.getUser(id).also {
            localDataSource.saveUser(it)
        }
    }
}

Use Case Patterns

// commonMain/kotlin/domain/base/BaseUseCase.kt
abstract class BaseUseCase<in P, R> {
    suspend operator fun invoke(parameters: P): Result<R> {
        return try {
            execute(parameters).let { Result.success(it) }
        } catch (e: CancellationException) {
            throw e
        } catch (e: Exception) {
            Result.failure(e)
        }
    }

    protected abstract suspend fun execute(parameters: P): R
}

// No parameters
abstract class BaseNoParamsUseCase<R> {
    suspend operator fun invoke(): Result<R> {
        return try {
            execute().let { Result.success(it) }
        } catch (e: CancellationException) {
            throw e
        } catch (e: Exception) {
            Result.failure(e)
        }
    }

    protected abstract suspend fun execute(): R
}

// Flow use case
abstract class FlowUseCase<in P, R> {
    operator fun invoke(parameters: P): Flow<Result<R>> = flow {
        try {
            emit(Result.success(execute(parameters)))
        } catch (e: CancellationException) {
            throw e
        } catch (e: Exception) {
            emit(Result.failure(e))
        }
    }

    protected abstract suspend fun execute(parameters: P): R
}

// Usage
class GetUsersUseCase(
    private val repository: UserRepository
) : BaseNoParamsUseCase<List<User>>() {
    override suspend fun execute(): List<User> {
        return repository.getAllUsers()
    }
}

class ObserveUserUseCase(
    private val repository: UserRepository
) : FlowUseCase<String, User>() {
    override suspend fun execute(parameters: String): User {
        return repository.getUser(parameters)
    }

    fun flow(id: String): Flow<Result<User>> {
        return repository.observeUser(id)
    }
}

Worker for Background Tasks

// commonMain/kotlin/coroutines/BackgroundWorker.kt
class BackgroundWorker(
    private val scope: CoroutineScope
) {
    private val workers = mutableMapOf<String, Job>()

    fun start(
        key: String,
        interval: Duration,
        dispatcher: CoroutineDispatcher = PlatformDispatchers.io,
        work: suspend () -> Unit
    ) {
        workers[key]?.cancel()
        workers[key] = scope.launch(dispatcher) {
            while (isActive) {
                try {
                    work()
                } catch (e: Exception) {
                    println("Worker $key failed: $e")
                }
                delay(interval)
            }
        }
    }

    fun stop(key: String) {
        workers[key]?.cancel()
        workers.remove(key)
    }

    fun stopAll() {
        workers.values.forEach { it.cancel() }
        workers.clear()
    }
}

Testing Helpers

// commonTest/kotlin/coroutines/TestDispatcher.kt
class TestPlatformDispatcher : PlatformDispatcher {
    private val testDispatcher = StandardTestDispatcher()

    override val main: CoroutineDispatcher = testDispatcher
    override val io: CoroutineDispatcher = testDispatcher
    override val default: CoroutineDispatcher = testDispatcher
    override val unconfined: CoroutineDispatcher = testDispatcher

    fun advanceUntilIdle() {
        testDispatcher.scheduler.advanceUntilIdle()
    }
}

// Usage in tests
class UserRepositoryTest {
    private lateinit var testDispatcher: TestPlatformDispatcher

    @BeforeTest
    fun setup() {
        testDispatcher = TestPlatformDispatcher()
    }

    @Test
    fun `fetches user from remote`() = runTest {
        val repository = UserRepository(mockRemote, mockLocal)
        val result = repository.getUser("123")

        testDispatcher.advanceUntilIdle()

        assertTrue(result.isSuccess)
    }
}

Best Practices

✅ DO
// ✅ Use platform dispatchers through abstraction
withContext(PlatformDispatchers.io) { ... }

// ✅ Scope coroutines to lifecycle
scope.launch { ... }

// ✅ Handle cancellation
try {
    longRunningTask()
} catch (e: CancellationException) {
    throw e  // Re-throw cancellation
}

// ✅ Use Flow for state
val users: StateFlow<List<User>>

// ✅ Provide meaningful coroutine names
scope.launch(Dispatchers.Default + CoroutineName("FetchUsers")) { ... }
❌ DON'T
// ❌ Don't use GlobalScope
GlobalScope.launch { ... }  // ❌

// ❌ Don't block threads
runBlocking { ... }  // ❌ in production code

// ❌ Don't swallow exceptions
launch {
    throw RuntimeException()
}  // ❌ unhandled

// ❌ Don't use Dispatchers.Main directly
// Use PlatformDispatchers.main for KMP

Remember: Coroutines are your async foundation. Abstract platform differences, scope properly, and handle cancellation.

FAQ

What does Shared Coroutines do?

Shared coroutines configuration for Kotlin Multiplatform. Platform dispatchers, Flow sharing, and common scope patterns.

Who made Shared Coroutines?

ahmed3elshaer (a community member) publishes it in the ahmed3elshaer/everything-claude-code-mobile repository on GitHub.

Is Shared Coroutines free?

Yes. Shared Coroutines is open source, published by ahmed3elshaer under the MIT license.

What comes with Shared Coroutines?

Just its instructions. Everything the agent needs is in one SKILL.md file.

Which agents can use Shared Coroutines?

Any coding agent that reads SKILL.md files, including Claude Code, Codex, Cursor, GitHub Copilot and Gemini CLI. The agent loads the skill when a task matches its description.

What are agent skills?

An agent skill is a folder with a SKILL.md file: instructions that teach a coding agent how to do one kind of task, and optionally files it can read while it works. The agent keeps only the skill's name and description in mind and reads the rest when a task calls for it.

How are agent skills different from MCP servers?

An MCP server gives an agent new tools to call, code that runs somewhere and returns results. A skill gives it knowledge: how to approach a task with the tools it already has. Many agents use both.

How can my agent test my Android app?

Robomori lets coding agents create, start and control Android emulators and devices from one command, so they can install your app, take screenshots and check dark mode, font sizes and languages.

You're subscribed

The next lesson lands in on Wednesday.