-
Notifications
You must be signed in to change notification settings - Fork 95
Open
Description
The issue is demonstrated by the below code, which reproduces the issue without actually depending on Molecule. The original
problem manifested as images loaded by Coil not fading in correctly - Coil's CrossfadePainter
functions in much the same way as the Painter in this code.
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
MaterialTheme {
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
TestImage(modifier = Modifier.padding(innerPadding))
}
}
}
// Replacing the below block of code with this also causes the same issue
// lifecycle.coroutineScope.launchMolecule(RecompositionMode.Immediate) {}
lifecycle.coroutineScope.launch {
var applyScheduled = false
Snapshot.registerGlobalWriteObserver {
if (!applyScheduled) {
applyScheduled = true
launch {
applyScheduled = false
Snapshot.sendApplyNotifications()
}
}
}
awaitCancellation()
}
}
}
@Composable
fun TestImage(modifier: Modifier = Modifier) {
Image(
painter = remember { InvalidatingPainter() },
modifier = modifier,
contentDescription = ""
)
}
class InvalidatingPainter : Painter() {
private var invalidateTick by mutableIntStateOf(0)
private var lastStartTime: TimeSource.Monotonic.ValueTimeMark? = null
private var duration = 1.seconds
private var reverse = false
override val intrinsicSize: Size = Size(200f, 200f)
override fun DrawScope.onDraw() {
val startTime = lastStartTime ?: TimeSource.Monotonic.markNow().also { lastStartTime = it }
val percent = (startTime.elapsedNow() / duration).toFloat().coerceIn(0f, 1f)
val alpha = if (reverse) 1 - percent else percent
drawRect(Color.Blue, alpha = alpha)
if (percent >= 1f) {
lastStartTime = null
reverse = !reverse
}
invalidateTick++
}
}
The cause seems to be
- The default coroutine scopes exposed by AndroidX use Dispatchers.Main.immediate
- Because of this, the
launch
within the global write observer doesn't actually dispatch, and apply notifications are sent immediately instead of in the next event loop - The
invalidateTick
state write is happening within the draw phase. This is where my Compose internals knowledge gets a bit fuzzy, but I'm guessing Snapshot.sendApplyNotifications() needs to happen after read SnapshotStateObserver.observeReads finishes for any updates to the read state to actually cause a change notification.
The answer here seems to be "never use the immediate dispatcher", but it's very easy to do so without knowing at the moment.
e.g. viewModelScope.launchMolecule(..)
will use the immediate dispatcher unless otherwise specified.
Perhaps Molecule could detect this and do the right thing?
Metadata
Metadata
Assignees
Labels
No labels