Skip to content

iOS Dispatchers.Main.immediate behaves like non-immediate #4430

Open
@pablo432

Description

@pablo432

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.

CoroutinesTest-minimal.zip

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

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions