Coroutines and Android's Handler question: There a...
# android
m
Coroutines and Android's Handler question: There are some Android classes with methods that take a callback, an optional
Handler
, and return a
FutureTask
. (i.e. AccountManager.getAuthToken) The gist of it is that these methods basically end up invoking something like this:
Copy code
val handler = optionalHandler ?: Handler(context.getMainLooper()) // unless given, always use the main looper handler. Context is the main Activity context for example.
    <http://handler.post|handler.post>{ callback(futuretask) }
Now, I want to wrap these with coroutines, so I ended up writing something like the below, which works fine:
Copy code
suspend fun doMethod() = suspendCancellableCoroutine<Bundle?> { continuation->
        callback= {result -> //do stuff with result }
        invokeAndroidMethod(..., callback, /* Handler */ null)
    }
But, because I'm not passing in a handler, the
callback
always ends up running on the main thread (main looper) How can I ensure that the
callback
always runs in a different thread than main? Any way I can get a
Handler
out of some coroutine scope/context, to pass it to
invokeAndroidMethod
? How about getting it for the current/parent coroutine? Example: If I ever want to do
launch(<http://Dispatchers.IO|Dispatchers.IO>) { doMethod() }
to basically ensure that
callback
also runs in
<http://Dispatchers.IO|Dispatchers.IO>
r
This isn't quite an answer but it's possible you're trying to do too much work in the callback. Generally you just want to call
continuation.resume
as fast as possible with whatever data you have at which point control will be switched back to the parent dispatcher (IO) and the fact that you were briefly on the main thread won't be a problem
👍 1
If being on the main thread really is a problem (maybe some kind of deadlock means you can't dispatch to that thread at all) it's possible to create a HandlerThread and get a new Handler from that which won't be linked to Main at all
👍 1
You can also use
asCoroutineDispatcher
if you want to dispatch things back to that thread (not sure why you'd ever need this, but it is exactly how Dispatchers.Main is implemented)
🙌 1
(as far as I know there's no way to get a Handler directly from a Dispatcher as originally asked because general purpose dispatchers aren't implemented using Looper)
m
I did come across HandlerThread but didn't explore it much, thinking that maybe there's something simpler I'm missing
But you're right, I was only doing boolean checks in the callback and then resuming the continuation, so that'd be minimal work for the callback on the main thread
👍 1
I did notice at one point that the main thread started freezing, when invoking multiple such coroutine wrappers for multiple accounts, but I suspect that was just me not launching the calls with a different dispatcher. I.e. my calls were
launch { refreshAccounts() }
and probably I should've used
launch {<http://Dispatchers.IO|Dispatchers.IO>) { refreshAccounts() }
or some other dispatcher instead
I was looking through the source code of AccountManager and it seems that if the callback is null, there's no
<http://Handler.post|Handler.post>
call. I suppose that if I want to completely avoid any main thread work, I could pass a
null
callback and handle the future in a separate thread that does a blocking call?
For context,
callback
is run via the handler, once the
future
settles, but
invokeAndroidMethod
returns the future (in its unsettled state initially, and
future.getResult()
is a blocking call that returns when the future settles)
so I'm thinking that maybe I could do something like:
Copy code
future = invokeAndroidMethod(..., /* callback */, null, /* handler */ null)
launch(someDispatcher) {
  result = future.getResult()
  // Work with result.
}
That would keep me of the main thread completely, in principle, right?
r
Yes, but it'd also break cancellation which is arguably much worse
m
Oh...you mean that
future.getResult()
being a blocking op would prevent the
doMethod
coroutine from being suspended?
(I'm still an Android/Kotlin noob, only picked it up a couple of months ago, so sorry if I'm missing something or don't get it right away 🙏 )
r
It means if the scope that called doMethod gets cancelled the getResult will continue to block until it finishes
In the callback implementation you were using suspendCancellableCoroutine so it'll just stop waiting if it gets cancelled
m
In this case would it be possible to handle the future in some other task or job, outside the scope of the coroutine that would still allow it to be cancelled and ignore the running task with the blocking getResult() ?
Btw, I put up a gist with what I came up for the callback variant and non-callback variant that I was thinking of: https://gist.github.com/mpotra/66414e67d8c1832adf5f7d80943f588f