-
Notifications
You must be signed in to change notification settings - Fork 673
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Remove dependency on Dispatchers.Main.immediate for immediate dispatching in Compose. #2725
base: main
Are you sure you want to change the base?
Conversation
cb33907
to
cd47369
Compare
083e410
to
ea0bbbc
Compare
e2cda61
to
3fe0ca5
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is really clever, glad you were able to figure out a solution and even get rid of that swing dep!
val request = updateRequest(input.request, isPreview = false) | ||
input.imageLoader.execute(request).toState() | ||
val originalDispatcher = scope.coroutineContext.dispatcher ?: Dispatchers.Unconfined | ||
val scope = ForwardingUnconfinedCoroutineScope(scope.coroutineContext) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Took me a while to understand how ForwardingUnconfinedCoroutineScope
and ForwardingUnconfinedCoroutineDispatcher
interact. Could the code that wires them up with the launch be extracted into a helper function? That logic seems mostly unrelated to the other logic in this function, which is more high-level, and pulling it out might make this a bit more readable.
Eg. (names are arbitrary):
// Observe the latest request and execute any emissions.
rememberJob = scope.launchTemporarilyUnconfined {
restartSignal.transformLatest<Unit, Nothing> {
_input.collect { input ->
withNormalDispatch {
val previewHandler = previewHandler
// ...
}
}
}
}
fun CoroutineScope.launchTemporarilyUnconfined(
block: suspend DeferringDispatchScope.() -> Unit
): Job {
val originalDispatcher = coroutineContext.dispatcher ?: Dispatchers.Unconfined
val forwardingScope = ForwardingUnconfinedCoroutineScope(scope.coroutineContext)
forwardingScope.launch(Dispatchers.Unconfined, CoroutineStart.UNDISPATCHED) {
DeferringDispatchScope(this, originalDispatcher).block()
}
}
class DeferringDispatchScope internal constructor(
scope: CoroutineScope,
private val originalDispatcher: CoroutineDispatcher
): CoroutineScope by scope {
// This could also just be a function passed as a normal param to launchDeferringDispatch's
// block lambda if you don't want to bother with a separate class.
suspend fun <T> withNormalDispatch(block: suspend CoroutineScope.() -> T) =
withContext(ForwardingUnconfinedCoroutineDispatcher(originalDispatcher), block)
}
but that might be a bit over-engineered, just an idea.
private val delegate: CoroutineDispatcher, | ||
) : CoroutineDispatcher() { | ||
private val _unconfined = atomic(true) | ||
var unconfined by _unconfined |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Consider documenting that after setting this to false, it's ignored even if set to true again.
/** | ||
* A special [CoroutineContext] implementation that lets us observe changes to its elements. | ||
*/ | ||
internal class ForwardingCoroutineContext( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oo this is a neat trick.
Fixes an issue with Coil's public composables where its memory cache check could miss the first frame due to dispatching. This case occurs with Paparazzi/Roborazzi/AndroidX screenshot tests and is due to the dispatcher provided by
rememberCoroutineScope()
either not supporting immediate dispatching,Dispatchers.Main.immediate
being unavailable, orDispatchers.Main.immediate
using a different underlying thread.To fix this I tried a couple things:
CoroutineStart.UNDISPATCHED
with aStateFlow
. This works for initial composition, but does not resolve synchronously from the memory cache on subsequent requests (e.g. when theAsyncImage(model)
changes).CoroutineDispatcher
that re-enables dispatching after the first time the context'sCoroutineDispatcher
changes. As far as I can tell this isn't possible at the moment withCoroutineInterceptor
's hooks.I settled on using a custom
CoroutineDispatcher
that acts likeDispatchers.Unconfined
until after theCoroutineContext
's dispatcher changes. This is safe for Coil's internals as it's all thread safe. Aside from fixing the above issue this has a number of benefits:Dispatchers.Unconfined
ifDispatchers.Main.immediate
is unavailable.kotlinx-coroutines-swing
no longer needs to be added manually as a dependency on JVM.