← All shorts
45Kotlin 1.7.20+2 min read

Sharing Coroutines and Flow Across Platforms

Kotlin/Native's coroutine runtime lets commonMain code use suspend functions and Flow exactly like JVM code.

Async state on Android usually means StateFlow or LiveData; on iOS it's Combine or callback closures. Kotlin/Native's coroutine support means shared code doesn't need an expect/actual abstraction over any of that — suspend fun and Flow just work in commonMain, and each platform consumes the same asynchronous API.

#The old way

An Android repository exposed a Flow<WeatherReport> backed by coroutines; the iOS equivalent re-implemented the same refresh-and-cache logic with a Combine CurrentValueSubject and URLSession completion handlers. Two implementations of one caching policy, drifting further apart with every bug fix applied to only one side.

#The new way

data class WeatherReport(val city: String, val tempCelsius: Double, val condition: String)

interface WeatherApi {
    suspend fun fetchWeather(city: String): WeatherReport
}

class WeatherRepository(private val api: WeatherApi) {
    private val _currentWeather = MutableStateFlow<WeatherReport?>(null)
    val currentWeather: StateFlow<WeatherReport?> = _currentWeather.asStateFlow()

    suspend fun refresh(city: String) {
        val report = api.fetchWeather(city)
        _currentWeather.update { report }
    }
}

WeatherRepository lives in commonMain untouched. Android collects currentWeather from a ViewModel with viewModelScope; iOS collects it through a thin wrapper that turns Flow into a callback or async sequence, but the caching and refresh logic itself is written once.

#Why it matters

  • One coroutine-based data layer instead of two async runtimes to keep behaviorally identical
  • StateFlow gives iOS a value-holding, replayable stream — the same shape Combine developers already expect
  • Bug fixes to refresh logic, retry policy, or error mapping apply to both platforms automatically

#Gotcha

_currentWeather.value = report and _currentWeather.update { report } look interchangeable but aren't under concurrent writers: direct .value assignment is a plain write, not a compare-and-set, so two coroutines updating from different threads can race and one update can silently overwrite the other. MutableStateFlow.update { } performs an atomic read-modify-write and is the safer default once more than one caller can mutate the same flow — which, in shared KMP code called from both a JVM dispatcher and a Kotlin/Native worker, is the common case.

kmpkotlincross-platform

Related shorts