Run Coroutine functions on broadcast receiver
Asked Answered
H

3

8

Im making an Alarm app, and using AlarmManager to set alarms. Im saving every alarm using Room after running the setAlarm on the AlarmManager, so I can later restore them if the phone gets turned off and o .

Im running a BroadcastReceiver after the device gets booted using the guide from Android Developer site: https://developer.android.com/training/scheduling/alarms#boot

And my idea is to get the alarms from Room on the onReceive method But Room uses a suspend fun to get the alarms, but I cant run it on the onReceive since BroadcastReceiver doesnt have a lifecycle

How could I achieve a similar result?

Hedvige answered 18/10, 2022 at 13:19 Comment(3)
I would argue that some sort of singleton repository object should be the one interacting with Room, with your receiver telling the repository to go do something. The repository can use a process-level CoroutineScope that you specifically set up for this sort of work.Therapsid
@Therapsid I think they're fetching something from Room, so they need a way for the receiver to wait for the IO.Marika
Exactly. The repository has a suspend fun that gets the information. And to call that function, I need to run it on a coroutine scopeHedvige
M
36

This section in the BroadcastReceiver documentation gives an example of how to do this.

You could clean it up a bit with an extension function:

fun BroadcastReceiver.goAsync(
    context: CoroutineContext = EmptyCoroutineContext,
    block: suspend CoroutineScope.() -> Unit
) {
    val pendingResult = goAsync()
    @OptIn(DelicateCoroutinesApi::class) // Must run globally; there's no teardown callback.
    GlobalScope.launch(context) {
        try {
            block()
        } finally {
            pendingResult.finish()
        }
    }
}

Then in your receiver, you can use it like below. The code in the goAsync block is a coroutine. Remember that you should not use Dispatchers.Main in this coroutine and it must complete within 10 seconds.

override fun onReceive(context: Context, intent: Intent) = goAsync {
    val repo = MyRepository.getInstance(context)
    val alarms = repo.getAlarms() // a suspend function
    // do stuff
}
Marika answered 18/10, 2022 at 13:53 Comment(7)
Love it!. Ill test it out tonight and if it works ill mark your answer as the solutionHedvige
Note, the linked documentation above no longer has the code snippet for using goAsync with a coroutine that the above code is based on.Marika
Why should coroutine run globally?Kreisler
@AlexanderKitaev, Maybe was partially explained in that documentation I linked that has since been removed. I think the issue is that BroadcastReceiver doesn't have any callback for when it's getting killed "under ten seconds" (elsewhere it says "~5 seconds") after you call goAsync.Marika
So there's no way to create a CoroutineScope that you can properly manage, i.e. cancel at an appropriate time. Even without coroutines, we get this fuzzy requirement to do your work well within five seconds. Extending that to coroutines, we just have to do the work quickly. I suppose as extra leak protection, you might consider using withTimeout { } in this function and giving it something like 4 seconds (to give a little buffer since the documentation is so imprecise about it).Marika
@Marika thank you for the explanation! I was using locally created scope in the receiver (code similar to one in polis answer below) to launch coroutine. I wondered if that "in-place" scope might be killed prematurely, contrary to the GlobalScope, even before "finish" is called on pendingResult.Kreisler
Creating an in-place scope is not effectively different than using GlobalScope, but the GlobalScope documentation says not to do it ("kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/…" section). Presumably this is because it's creating a redundant scope and sort of masks that it's still global. Either way, the Android system doesn't care what you're doing--it's holding your app open until you call finish() or your 5s is up.Marika
E
0

The correct way is:

fun BroadcastReceiver.doAsync(
    appScope: CoroutineScope,
    coroutineContext: CoroutineContext = EmptyCoroutineContext,
    block: suspend CoroutineScope.() -> Unit
){
    val pendingResult = goAsync()
    appScope.launch(coroutineContext) { block() }.invokeOnCompletion { pendingResult.finish() }
}

The accepted answer may lead to bugs, because doesnt take in the account structured concurency of coroutines. If the block launches subcoroutines "finally" part of try/finally may be called before they are finished and you can easily test it. With invokeOnCompletion this kind of bugs are eliminated.

Euniceeunuch answered 29/8, 2024 at 23:6 Comment(0)
G
-1

You can do it like this:

    override fun onReceive(context: Context, intent: Intent)  {
        if (intent.action == "android.intent.action.BOOT_COMPLETED") {
            CoroutineScope(Dispatchers.IO).launch {
                try {
                   // you code here
                    
                } finally {
                    cancel()
                }
            }
        }
    }
Gerigerianna answered 31/1, 2023 at 14:15 Comment(2)
The Android system can finish the broadcast receiver without work being done in the launch function. The @Marika 's answer is haven't this problem.Spokeshave
If you don't call goAsync, this is likely to get torn down before your coroutine finishes.Marika

© 2022 - 2025 — McMap. All rights reserved.