Status Update
Comments
go...@jakewharton.com <go...@jakewharton.com> #2
Triage notes: Still P3, still a real issue. Compat issue.
go...@jakewharton.com <go...@jakewharton.com> #3
Alas, no. It seems the global embedding context is exclusively used. The one I supply to the Recomposer
is never accessed.
So at the very least this is likely mostly blocked on
cl...@google.com <cl...@google.com>
go...@jakewharton.com <go...@jakewharton.com> #4
Alpha07 update to code, still doesn't exit cleanly.
I moved the embedding context to use the blockingScope rather than the effect scope thinking that (coupled with the effectCoroutineContext of a07) would do it. But nope. Now I'm not sure what keeps the inner effect scope alive...
fun main() {
runBlocking {
val blockingScope = this
val clock = BroadcastFrameClock()
val clockJob = launch {
println("Clock start")
while (true) {
clock.sendFrame(0L)
delay(50)
}
}
clockJob.invokeOnCompletion {
println("Clock end")
}
val composeContext = coroutineContext + clock
val mainThread = Thread.currentThread()
val embeddingContext = object : EmbeddingContext {
override fun isMainThread(): Boolean {
return Thread.currentThread() === mainThread
}
override fun mainThreadCompositionContext(): CoroutineContext {
return composeContext
}
}
yoloGlobalEmbeddingContext = embeddingContext
// This scope encapsulates the work required to render a composition.
val recomposeJob: Job
coroutineScope {
val recomposer = Recomposer(coroutineContext, embeddingContext)
val composition = compositionFor(Any(), NoOpApplier, recomposer)
println("Recompose start")
// This runs outside of the current coroutineScope because we do not want it to stop this
// scope from exiting cleanly (at which point we will cancel this job).
recomposeJob = blockingScope.launch(start = UNDISPATCHED, context = composeContext) {
recomposer.runRecomposeAndApplyChanges()
}
recomposeJob.invokeOnCompletion {
println("Recompose end")
}
println("Content start")
composition.setContent {
// State
var count by remember { mutableStateOf(0) }
// Presentation
println("The count is: $count")
// Logic
LaunchedEffect(null) {
repeat(10) {
delay(250)
count++
}
}
}
}
println("Content end")
recomposeJob.cancel()
clockJob.cancel()
}
}
object NoOpApplier : AbstractApplier<Nothing?>(null) {
override fun insert(index: Int, instance: Nothing?) {}
override fun remove(index: Int, count: Int) {}
override fun move(from: Int, to: Int, count: Int) {}
override fun onClear() {}
}
go...@jakewharton.com <go...@jakewharton.com> #5
If I could call or trigger effectJob.complete()
inside Recomposer
I think this would work.
Because this works:
fun main() = runBlocking {
// CODE **NOT** IN MY CONTROL:
val recomposerEffectJob = Job(coroutineContext[Job]).apply {
invokeOnCompletion {
println("Recomposer effect job completed")
}
}
val recomposerEffectScope = CoroutineScope(recomposerEffectJob)
recomposerEffectScope.launch {
println("Effect start")
delay(1000)
println("Effect stop")
}
// CODE IN MY CONTROL:
println("Completing effect job") // NOTE: Recomposer does NOT expose this...
check(recomposerEffectJob.complete())
println("Joining effect job")
recomposerEffectJob.join()
println("effect job joined")
}
but I cheat here by accessing the recomposerEffectJob
(a stand-in for the private recomposer.effectJob
).
If I try to do it with my own parent job it still hangs forever:
fun main() = runBlocking {
// CODE IN MY CONTROL:
val effectJob = Job(coroutineContext[Job]).apply {
invokeOnCompletion {
println("Effect job completed!")
}
}
val effectContext = coroutineContext + effectJob
// CODE **NOT** IN MY CONTROL:
val recomposerEffectJob = Job(effectContext[Job]).apply {
invokeOnCompletion {
println("Recomposer effect job completed")
}
}
val recomposerEffectScope = CoroutineScope(recomposerEffectJob)
recomposerEffectScope.launch {
println("Effect start")
delay(1000)
println("Effect stop")
}
// CODE IN MY CONTROL:
println("Completing effect job")
check(effectJob.complete())
println("Joining effect job")
effectJob.join()
println("effect job joined")
}
ad...@google.com <ad...@google.com> #6
No update yet but this is still a desired behavior to support
Description
Using runtime alpha03
Forgive, for a moment, a supremely contrived example: Let's say I have code prints the numbers from 0 to 10 with a delay.
This prints
and then the program exits.
Unfortunately I'm mixing state, logic, and presentation. Compose let's me clearly delineate these. So I rewrite my code to:
(Please ignore the fact that I'm relying on side-effects to print, normally I would use a node in the tree and print when needed as part of a frame)
This code isn't enough on its own, so we have to set up the composition and a frame clock inside
main
'srunBlocking
:yoloGlobalEmbeddingContext
looks like this in my runtime which is built for the JVM:This code prints:
and does not exit cleanly.
Unless I'm missing something, there doesn't seem to be a way to set up a composition so that it can exit cleanly when it has no more work to do. I believe this is caused by the
FrameManager
usingRecomposer.current()
which references the globalEmbeddingContext
.In the context of Android or desktop where a composition runs for as long as it's visible, a coroutine that suspends forever or is tied to the visible lifecycle can be used to keep the composition alive.
One other notable thing: The semi-cyclic dependency where I have to set up the issue 168110493 will help here.
coroutineScope
which encapsulates the work, pass it to theRecomposer
, but then launchrunRecomposeAndApplyChanges
outside of that scope to avoid keeping it alive forever is awkward. Perhaps