Hi, ```class Foo {     private val scope = Corout...
# coroutines
u
Hi,
Copy code
class Foo {
    private val scope = CoroutineScope(SupervisedJob() + Dispatchers.Main.immediate)

    fun init() {
        println("a")

        scope.launch {
            println("b")

            flowOf("hello")
                .collect {
                    println("c")
                }
        }

        println("d")
    }
}
I'm pretty sure this emmited printed
a, b, c, d
in the past when using immediate main dispatcher on android but now it emits
a, d, b, c
Is there a way to make this blocking as it was in rxjava? I thought this exactly was the difference between
Dispatchers.Main
and
Dispatchers.Main.immediate
e
the difference between
Dispatchers.main
and
Dispatchers.Main.immediate
is
Copy code
withContext(Dispatchers.Main) {
    println("a") // may be in next event loop
}
withContext(DIspatchers.Main.immediate) {
    println("b") // in the same event loop, if current thread is main UI thread
}
but I don't think it is meant to guarantee anything about
launch()
you might be looking for something more like CoroutineStart.UNDISPATCHED
u
damn, I moved whole codebase from rx to coroutines assuming this
https://kotlinlang.slack.com/archives/C1CFAFJSK/p1602987450429400?thread_ts=1602987450.429400&cid=C1CFAFJSK yep I asked it year ago when migrating, and then it worked as I expect. hmm
l
You should use lifecycleScope, brought by AndroidX Lifecycle runtime KTX
👍 1
u
Copy code
Returns dispatcher that executes coroutines immediately
I’d also say
launch
creates a coroutine that should therefor be executed immediately. So I’d at least. expect
b
before
d
. I don’t think there are any guarantees about
c
though.
b
@ursus weird question, but are you sure Foo is created from main thread?
a, d, b, c
is expected if it's called from non-main dispatcher
u
yes, init() is called from main thread, its a view model subclass, however its my subclass, not using the androidx thing, since its tied to fragments api, which i dont use
u
I get the expected results in onCreate of an Activity:
Copy code
2021-09-28 15:36:20.409 27971-27971/I/System.out: Coroutines: a
2021-09-28 15:36:20.413 27971-27971/I/System.out: Coroutines: b
2021-09-28 15:36:20.414 27971-27971/I/System.out: Coroutines: c
2021-09-28 15:36:20.414 27971-27971/I/System.out: Coroutines: d
That’s Kotlin 1.5.31 on Android with Coroutines 1.5.2
I would follow @ursus and log the thread of each checkpoint:
Copy code
println("${Thread.currentThread()}: a")
and so on
Assuming that your posted code is just a demonstration of your real problem, Another hint is, that in case your real code has any suspension points inside launch. that will yield the main thread to
d
something like
Copy code
scope.launch {
            sleep(1)
            println("b")
u
this is odd, I tried activity and some othe view model and it works correctly; jsut this one screen, the same logging, yields the incorrect order, will investigate
Ok I isolated it but not sure what the error The screen is being inserted via a firebase dynamick link, like this
Copy code
class Activity {
    private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)

    fun onCreate() {
        scope.launch {
            val uri = fetchUri()
            if (uri != null) {
                val viewModel = FooViewModel(..)
                viewModel.init()
            }
        }
    }
}

class FooViewModel {
    private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)

    fun init() {
        Log.d("Default", "a")
        scope.launch {
            Log.d("Default", "b")
            flowOf("hello")
                .collect {
                    Log.d("Default", "c")
                }
        }
        Log.d("Default", "d")
    }
}
If I dont fetch the uri via coroutines (use callbacks; basically not using the outer scope.launch), it works correctly 🤷‍♂️🤷‍♂️ So the issue is nesting the 2 main thread coroutine scopes .. not sure why Which is a rather big problem, since I do this all over (Activity subscribing to some "global" streams like flow of User instances, and when
null
is emitted, it should reset backstack to login screen (which has its viewmodel etc, the same situation as above)
u
Not reproducible on my side:
Copy code
class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)
        scope.launch {
            fetchUri()
            init()
        }
    }

    suspend fun fetchUri() = withContext(<http://Dispatchers.IO|Dispatchers.IO>) {
        delay(100)
    }

    private fun init() {
        val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)
        println("Coroutines: ${Thread.currentThread()}: a")
        scope.launch {
            println("Coroutines: ${Thread.currentThread()}: b")
            flowOf("hello")
                .collect {
                    println("Coroutines: ${Thread.currentThread()}: c")
                }
        }
        println("Coroutines: ${Thread.currentThread()}: d")
    }
}
Copy code
2021-09-28 22:30:56.895 16765-16765/I/System.out: Coroutines: Thread[main,5,main]: a
2021-09-28 22:30:56.897 16765-16765/I/System.out: Coroutines: Thread[main,5,main]: b
2021-09-28 22:30:56.905 16765-16765/I/System.out: Coroutines: Thread[main,5,main]: c
2021-09-28 22:30:56.906 16765-16765/I/System.out: Coroutines: Thread[main,5,main]: d
Are the above onCreate & init methods the full, true methods that you use for testing?
u
maybe please try making those scopes fields?
u
as expected - same same
can you copy paste my code into your project and test
whic kotlin/coroutines version are you using?
u
1.5.21; 1.5.1
u
can you upgrade to 1.5.31/1.5.2
u
im trying a minimal repro, and cannot reproduce it as you do even with my older version
u
funny game of “find the difference” 😉
u
if I insert delay here
Copy code
class Activity {
    private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)

    fun onCreate() {
        scope.launch {
        	delay(1000)
            val viewModel = FooViewModel(..)
            viewModel.init()
        }
    }
}
then it fixes it self, is that a clue?
b
Could you add thread to log as Uli did? Maybe there is some issue that coroutine wake up in non-main thread after fetch. You can also add to the log Dispatchers.Main.immediate.isDispatchNeeded(EmptyCoroutineContext) (it actually controls whether coroutine be dispatched or executed immediatelly)
u
I can reproduce the issue now. As @ursus said. If I remove the call to
fetchUri()
(i.e. delay) it behaves wired here as well
u
yea now I have it too, will push to github
which is weird because, delay on main scope should be a handler.post right?; which is what fixes it and my original issue was firebase dynamic links api, which posts callback to main thread .. which breaks it
u
It’s a feature. It’s called stack overflow protection 🙂
u
how so, not even 2 nested launches allowed?
u
If you are in the undispatched part of a coroutine it will always dispatch. no matter wether it is an immediate dispatcher or not. `delay`will dispatch and after that immediate works again. This is to make recursion not stack based, because dispatching does not add to the stack
e
unless you force it with
launch(start = UNDISPATCHED)
as I mentioned way back
u
yea but this is hard to notice in nested screens, you shouldn't really care what your parent is doing
btw how come firebase dynamic links breaks it? is it the fact that I wrapped the api in suspendableCouroutine, and the callback is already posted to main thread by the library, therefore dispatcher correctly keeps it synchrounous; which then later allows for the protection to be triggered? therefore putting yield in the callbacks should make it work?
u
or follow @ephemient’s hint. Dispatchers.Main.immediate is more of a performance optimization. So I guess they think not giving a guarnte to be undispatched is fine. Asking launch to start undispatched seems more explicit and seems to work
Still, not really the principle of least surprise
u
well yea but the screen is also triggered normally, not in a nested way, would it not break things there?
e
perhaps the docs aren't super clear, but I think relying on
launch()
to start without dispatching was never a safe assumption even if it happened to work
u
why would it. I’d say it will do what you expected any way. start your coroutine without posting to the main thread. just by calling your code (up to the first suspension point)
u
tbh I kind of do, coming from rxjava, and the real use case is I have a state holder which emits state, and I don't want the cached emit, so I
drop(1)
and trigger the work again; now since the collection is dispatched, and I trigger work sooner, I miss a state emit, which is bad
u
u
does undispatched mode basically just turn off the protection in my case?
u
it does the same as Dispatchers.Main.immediate, just guaranteed. and yes that means no stack overflow protection if your code is recursive
u
Whats a better solution? using the Undispatched start mode, or putting yield after the uri fetching? Both make it work
u
undispatched!
u
but that hardcodes upstream behavior in this concrete screen, doesnt it? Or should I do that for all view models? That would mimic rxjava behaviour, right?
okay thank you all for your help, so much
u
👍
e
if yield happens to work, it is probably because you're getting resumed at the top of the stack, but that definitely feels too fragile to rely on
definitely UNDISPATCHED if you're relying on that behavior, but I don't understand why you need it
u
@ephemient I rather think yield() works, because after resume the coroutine is no longer running in the callers stack frame any more. I still agree though, that that would be relying much too much on implementation detail. UNDISPATCHED is there for exactly that use case. Not being dispatched until first suspension point. Still be aware @ursus, I would consider it quite fragile if I could break my code just by calling a suspend function in the wrong place.
Maybe now that the riddle is solved. You can think about getting rid of relying on the details of when and where you get dispatched.
u
yea im still partially with rx, after i move to flow it wont work at all regardless of undispatched or now