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
StateFlowgives 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.