Description
Describe the bug
Having a multiplatform project targetting iOS and Android with shared Compose UI, we have observed a situation, where application started on iOS behaves differently than one on Android when running coroutines in viewModelScope
, which, according to documentation at https://www.jetbrains.com/help/kotlin-multiplatform-dev/compose-viewmodel.html should be an immediate dispatcher (and also DarwinMainDispatcher implementation in kotlinx-coroutines-core suggests it should be supported).
I am attaching a sample project that shows the issue. See AppViewModel.kt class in commonMain
module and simply run the project on Android (I was using Pixel 7 API 34 Emulator and Samsung S24 with Android 14) and observe logcat by AppViewModel tag. Afterwards run the same project on iOS via Xcode (I was using iPhone 16 Pro Simulator with iOS 18.0) and observe console output there (also filtering by AppViewModel). Compare the logs.
Consider the following piece of code, which is a part of the minimal project I'm attaching to this report:
class AppViewModel : ViewModel() {
companion object {
private val TAG = "AppViewModel"
}
private val sharedFlow = MutableSharedFlow<String>(replay = 1).apply {
tryEmit("first")
}
init {
viewModelScope.launch {
val dispatcher = currentCoroutineContext()[CoroutineDispatcher]
getPlatform().log(TAG, "Dispatcher is $dispatcher")
launch {
sharedFlow.collect {
getPlatform().log(TAG, "collected: $it")
}
}
delay(1000)
getPlatform().log(TAG, "will emit 'second'")
val secondSuccess = sharedFlow.tryEmit("second")
getPlatform().log(TAG, "secondSuccess = $secondSuccess")
getPlatform().log(TAG, "will emit 'third'")
val thirdSuccess = sharedFlow.tryEmit("third")
getPlatform().log(TAG, "thirdSuccess = $thirdSuccess")
}
}
}
Expected Behavior
As dispatcher used for viewModelScope is supposed to be immediate, collection should happen on the same stack as the new value has been emitted to sharedFlow
. For the code above, below is the logcat output of Android project which I consider to be correct:
2025-05-05 23:27:11.318 18244-18244 AppViewModel org.example.project D Dispatcher is Dispatchers.Main.immediate
2025-05-05 23:27:11.323 18244-18244 AppViewModel org.example.project D collected: first
2025-05-05 23:27:12.319 18244-18244 AppViewModel org.example.project D will emit 'second'
2025-05-05 23:27:12.320 18244-18244 AppViewModel org.example.project D collected: second
2025-05-05 23:27:12.320 18244-18244 AppViewModel org.example.project D secondSuccess = true
2025-05-05 23:27:12.320 18244-18244 AppViewModel org.example.project D will emit 'third'
2025-05-05 23:27:12.320 18244-18244 AppViewModel org.example.project D collected: third
2025-05-05 23:27:12.320 18244-18244 AppViewModel org.example.project D thirdSuccess = true
The order of logs is correct for immediate dispatcher: will emit second
-> collected second
-> secondSuccess = true
-> will emit third
-> collected third
-> thirdSuccess = true
.
Actual behavior on iOS
Below is the console output from iOS:
2025-05-05 23:29:39.076 AppViewModel: Dispatcher is Dispatchers.Main.immediate
2025-05-05 23:29:39.099 AppViewModel: collected: first
2025-05-05 23:29:40.100 AppViewModel: will emit 'second'
2025-05-05 23:29:40.100 AppViewModel: secondSuccess = true
2025-05-05 23:29:40.100 AppViewModel: will emit 'third'
2025-05-05 23:29:40.100 AppViewModel: thirdSuccess = false
2025-05-05 23:29:40.100 AppViewModel: collected: second
The order of logs suggests iOS Dispatcher does not behave in an immediate manner.
When a second
value is attempted to be emitted, it would be expected that collect
would happen immediately in the same stack, as there is no thread change involved and it is supposed to be an immediate dispatcher. Yet there is no collect
for second
at this point - it seems to happen out of stack, after an attempt to emit third
fails. I understand that tryEmit
returns false when there is no chance to emit a value without suspending, but there should be no need for suspending in the example I'm showing.
Provide a Reproducer
I am attaching a minimal example project below.
Environment summary
Android: Pixel 7 API 34 Emulator, Samsung S24 Android 14 (works as expected for both)
iOS: Simulator for iPhone 16 Pro with iOS 18.0
Library versions:
- kotlinx-coroutines-core 1.8.0
- Compose multiplatform: 1.7.3
- androidx-lifecycle-viewmodel / androidx-lifecycle-viewmodel-compose: 2.8.4
- Kotlin: 2.1.20