Suspend function should be called only from a coroutine or another suspend function

Let’s say that I have a callback interface like below:

    interface MyCallback {
        ...
        fun onPackAdded(apps: List<MyApplication>)
    }

And in another Class I have the codebase like:

private val myApps = context.getSystemService(Context.LAUNCHER_APPS_SERVICE)
private var mc: MyCallback

myApps.registerCallback(object : MyCallback() {
        override fun..{}
        override fun..{}
        override fun onPackageAdded(packageName: String, user: UserHandle) {
                GlobalScope.launch(coroutineContext) {
                    // do my jobs here
                    ...
                    mc.onPackageAdded(activities)
                }
            }
            
            ...
}

Since GlobalScope is marked as a delicate API in [kotlin coroutine 1.5], I therefore refact the GlobalScope part to:

suspend fun toAdd(packageName: String, user: UserHandle) {
         // do my jobs here
          ...
         mc.onPackageAdded(activities)
}

So now it turns to:

myApps.registerCallback(object : MyCallback() {
        override fun..{}
        override fun..{}
         override fun onPackageAdded(packageName: String, user: UserHandle) {
                toAdd(packageName: String, user: UserHandle)
            }
            ...
}

I got an error that complains “Suspend function ‘toAdd’ should be called only from a coroutine or another suspend function”, any idea about how to fix it? Much appreciate!

First, why are you calling GlobalScope.launch? You aren’t explicitly calling any suspend methods. Is the “…” some suspending method calls?

As for GlobalScope, CoroutineScopes allows you manage the lifetime of coroutines by grouping them together. You can cancel the group or wait for them all to finish. So the answer of what CoroutineScope to use depends on what the lifetime of the work should be.

Activity.lifecycleScope is commonly used on Android since it is cancelled when the Activity is destroyed. If this code is for a specific Activity, this is what you likely want to use.

If this code is in an Application class, then you probably want to use GlobalScope since you never want the launched code to stop running until the process is killed.

On another note, launch does not do any synchronization or guarantee order. So anytime I have an add/update/remove style API or anything where order matters (you don’t want your “remove” code to run before “add” finishes), I use a Channel or Flow to ensure they are processed serially.

Example:

        val myChannel = Channel<() -> Unit>(Channel.UNLIMITED)
        lifecycleScope.launch {
            myChannel.foreach { it() } //Call code to handle each event one at a time
        }
        myApps.registerCallback(object : MyCallback() {
            ...
            override fun onPackageAdded(packageName: String, user: UserHandle) {
                myChannel.trySend {
                        val app = myApps.getActivityList(packageName, user)
                        ifSomeSuspendingCodeIsHere()
                        mc.onPackAdded(app)
                }
            }

            override fun onPackageRemoved(packageName: String, user: UserHandle) {
                myChannel.trySend {
                    packageName.let { 
                        user.let { mc.onPackRemoved(....) } 
                    }
                }
            }
}

Again, if you aren’t dealing with suspending code, you probably don’t want to be launching at all.

1 Like

Hi @nickallendev , thank you for the answer! I have re-edited the question to be more clear, could you please take a look again? Thanks!

I’m not sure what do you try to achieve. Your last code is basically the same as:

override fun onPackageAdded(packageName: String, user: UserHandle) {
    mc.onPackAdded(activities)
}

override fun onPackageRemoved(packageName: String, user: UserHandle) {
    mc.onPackRemoved(application)
}

And because your implementation of MyCallback just delegates to another MyCallback, then this is (almost) the same as simply:

myApps.registerCallback(mc)

So again: what do you try to achieve? Make MyCallback wrapper that “converts” from synchronous to asynchronous execution?

1 Like

Yes, but then we are back at the original question of @nickallendev : why do you need to use GlobalScope.launch() in the first place? I think you didn’t answer it.

Or let me rephrase the question: what would be the problem if you just remove it entirely?

like nick Said, this code is in an Application class so I use GlobalScope for all the add/remove/change app package operations, I want the code keep running until the process is killed.

I tried to replace the GlobalScope with CoroutineScope().launch, but it seems the offcial doc suggests that do not replace GlobalScope.launch { … } with CoroutineScope().launch { … } constructor function call. The latter has the same pitfalls as GlobalScope.

Look, one of features of coroutines is to make easier to properly handle exceptional states, cancellations and to not leak any background tasks. It is pretty easy to run a thread in the background and just forget to stop it. Or don’t notice that this thread crashed and another half of our applications still thinks it is running fine. Coroutines make it much harder to introduce such bugs, thanks to their structured concurrency.

GlobalScope is discouraged, because by using it coroutines are detached from the structured concurrency. We can’t easily cancel them if we don’t need them anymore (e.g. they were created by some activity and it was destroyed), we can’t cancel them in the case their “owner” crashed, we can’t wait for them to finish, we ignore their exceptions, etc. And doing something like: CoroutineScope().launch() is almost the same - we created a scope, but we immediately removed any references to it, so we basically run coroutines detached from the rest of our application.

But still, there are cases where GlobalScope is fine. For example, if we have a service that needs to run as long as our whole application then we never need to cancel it, we don’t care about our application crashing (because then the service will die anyway) and we don’t need to wait for the service to finish. We lose handling of errors in the service, but generally, this case is considered a valid usage of GlobalScope, so we can ignore the warning and use it.

I’m not entirely sure if this is really your case, because your code does not start some long-running service, but just handle events. What if you would like to react to exceptions in these handlers? What if you would like to check if these coroutines don’t freeze somehow and remain forever? I don’t say you should not use GlobalScope, I just think you should consider such cases.

Also, I just noticed that you provide a coroutineContext when using GlobalScope.lanuch() and now I’m a little confused. What is this context? I ask, because if this context contains Job then I believe you actually still use structured concurrency, but using some kind of a workaround. In that case you can create a CoroutineScope(coroutineContext) and use launch() on it, instead of using GlobalScope. This would run all coroutines as children of your Job.

Thank you very much for the explanation in detail, the context in the GlobalScope is actually a background execute context, so guess I will then in my case create a CoroutineScope(bgcontext) and launch it

much appreciated!