Travis Griggs
Travis Griggs

Reputation: 22280

Calling a suspend function from legacy library on background thread

I'm using the HiveMQ Android Client in my Android App. I "probe" for a device using a transient object for the transaction, which wraps an Mqtt3AsyncClient.

fun probe(callback: ((String) -> Unit)) {
    ...
    client
        .connectWith()
        .cleanSession(true)
        .keepAlive(60)
        .simpleAuth()...applySimpleAuth().send()
        .whenComplete { _, _ ->
            client
                .subscribeWith()
                .addSubscription().topicFilter(eventTopic).applySubscription()
                .callback { message ->
                    handleMessage(message.topic, message.payloadAsBytes))
                }
                .send()
                .whenComplete { _, _ -> attemptToCommunicate()}
        }
}

The problem is that my handleMessage function (which will run async because that's what the async client is all about) makes a call to a suspend fun in it's body, e.g.

fun handleMessage(topic:String, message:Bytes) {
    ...
    saveDataFuncThatIsDefinedWithSuspend(...)
    ...
}

I can't exactly mark handleMessage as suspend, because it's call from the callback will be an error. Marking the probe function as suspend doesn't solve that either. And I'm not at liberty to not use the HiveMQ client, so I'm stuck with the callback.

I toyed with wrapping the suspend function as such:

CoroutineScope(Dispatchers.IO).launch { saveDataFuncThatIsDefinedWithSuspend(...) }

but I got the impression, that this is considered a bad idea? Or I could wrap a runBlocking around it, but again, that seems frowned upon. Should I make my transient transaction object itself a CoroutineScope implementor? I'm struggling with how one bridges these two worlds in a case like this.

Upvotes: 0

Views: 204

Answers (2)

Tenfour04
Tenfour04

Reputation: 93902

This is what I believe is the one situation on Android where runBlocking is an appropriate solution. If you cannot design a blocking version of your suspend function, there’s no way you can avoid tying up two threads for this (one from the coroutine dispatcher, and the one the client library that waits for it). So runBlocking is the simplest, cleanest way to achieve this.

If your message handler doesn’t need to wait for your suspend function to return, then it does make sense to launch a coroutine to call it. But doing this with a one-off CoroutineScope that you create and abandon the reference for is an anti-pattern. Either you want to launch the coroutine globally so it doesn’t automatically get cancelled under some event like navigating away from a related screen or you don’t. For a global coroutine like the first case, you would use GlobalScope, and for the second case you would use a scope tied to the relevant lifecycle for whatever you’re doing.

Upvotes: 1

tyg
tyg

Reputation: 16092

Yes, in most cases you will want to define a new CoroutineScope as a child of another scope, and not create a new scope out of the blue.

That said, you can always pass in a scope as a parameter (to probe and/or handleMessage) so that you can use it like this:

scope.launch { 
    saveDataFuncThatIsDefinedWithSuspend(...)
}

That scope should be created somewhere outside of your legacy code where you have access to a parent scope, for example in a suspend function (possibly the one that calls probe).

Upvotes: 1

Related Questions