Matt Nelson
04/10/2024, 7:48 AMMatt Nelson
04/10/2024, 7:48 AMDispatchers.Main
(preferring Dispatchers.Main.immediate
if available), and another instance of it that will not do any context switching (direct dispatch):
// Callback that events are dispatched to using
// Dispatchers.Main
fun interface OnEvent<in T: Any> {
operator fun invoke(event: T)
// Callback that events are dispatched to using
// whatever thread that event originated on.
// Implementation **MUST** be thread-safe.
fun interface Unconfined<in T: Any>: OnEvent<T>
}
But what if Dispatchers.Main
is not available (e.g. Someone using this with ktor-server
, or in headless mode, or native linux/mingw)???Sam
04/10/2024, 9:04 AMcallbackFlow
and let the caller choose their dispatcher? That's what dispatchers are for, after all.Matt Nelson
04/10/2024, 9:07 AMkotlinx.corutines
Sam
04/10/2024, 9:15 AMSam
04/10/2024, 9:16 AMMatt Nelson
04/10/2024, 9:21 AMLOG.DEBUG
), it is dispatched to all observers registered for that event. Implementors of that OnEvent
callback must have thread safety (e.g. using synchrnoized
and/or @Volatile
) when interacting with anything outside of the OnEvent
lambda (such as global variables in a class or something)Sam
04/10/2024, 9:28 AMSam
04/10/2024, 9:35 AMBut what ifThese folks should already know they're running in a multithreaded environment, and they'll also be used to the idea of adapting callbacks to suspending functions and flows. A coroutine dispatcher gives the caller the ability to adapt your callback into a suspension point and choose what thread their code will run on when the suspension point resumes. That means they have control of their thread choice, making it less of a worry for you.is not available (e.g. Someone using this withDispatchers.Main
) […]?ktor-server
Matt Nelson
04/10/2024, 9:37 AM2.0.0
version of a current library where, previously, Dispatchers.Main
was utilized for all events. So sort of need to maintain that default functionality, sadly.Sam
04/10/2024, 10:00 AMfun registerObserver(observer: MyObserver, executor: CallbackExecutor)
fun interface CallbackExecutor {
fun execute(callback: () -> Unit)
}
Then you can provide a couple of built-ins:
object MainExecutor: CallbackExecutor {
override fun execute(callback: () -> Unit) = Dispatchers.Main.dispatch(...)
}
object ImmediateExecutor: CallbackExecutor {
override fun execute(callback: () -> Unit) = callback()
}
Technically the extra indirection doesn't enable anything the user wouldn't be able to do with a plain callback—but it does remove the ambiguity and assumptions. You could provide the old implementation as a (possibly-deprecated) overload, for compatibility:
@Deprecated
fun registerObserver(observer: MyObserver) = registerObserver(observer, MainExecutor)
I like it as an approach because it makes everything explicit—the user knows they're making a choice, and can make an informed one based on their own threading needs and assumptions.Matt Nelson
04/10/2024, 10:10 AMMatt Nelson
04/11/2024, 12:05 PMSam
04/12/2024, 9:49 AM