Fix/android detach cancels render#127
Merged
erweixin merged 3 commits intoJul 9, 2026
Merged
Conversation
32 formulas mount inside opacity:0 wrappers revealed one frame later; the style drop flips the wrappers' Fabric view-flattening status, so the commit re-parents them and transiently detaches every formula while its render job is in flight. On a cold start (fonts loading, all jobs pending) the broken library shows rendered 0/32 with 32 'StandaloneCoroutine was cancelled' errors; the fixed library renders 32/32.
…he render job A transient detach (Fabric re-parenting on a view-flattening change, view moves, RecyclerView) cancelled the pending renderJob, and nothing ever restarted it — every rerender() trigger is a prop setter guarded by field == value, so the view kept its laid-out size but stayed blank forever, reporting the cancellation through onError. - onAttachedToWindow re-kicks the render when nothing is rendered and no job is in flight (same pattern as Coil's ViewTargetRequestManager) - rethrow CancellationException instead of converting it into an error state; it must reach the top of the call stack uncaught - standalone platforms/android copy: cancel only the job on detach, not the whole scope (+ SupervisorJob) — a cancelled scope ignores every future launch
Owner
|
Thank you for putting this PR together — this is an excellent, very carefully documented fix. Really thoughtful contribution. Thanks again for digging into this and for the thorough validation notes. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The bug
On Android a formula can end up permanently invisible: it occupies exactly the right
space (measurement is synchronous and fine), draws nothing, and only a full remount
brings it back.
onErrorfires with the confusingStandaloneCoroutine was cancelled.Reproduction
Got this on real App, and it's reproducible ;-)
demo/react-native-expo(upgraded to Expo SDK 57 / RN 0.86) has a new Detach-cancelcase that reproduces this deterministically: 32 formulas mount inside
opacity: 0wrappers that are revealed one frame later. Dropping the style makes the wrappers
eligible for Fabric view flattening, so the commit re-parents them
(
SurfaceMountingManager.removeViewAt+addView) — transiently detaching every formulawhile its render job is still in flight. Kill + relaunch the app (cold start is the most
reliable: the first job blocks on the KaTeX font load, keeping all 32 pending):
rendered 0/32 · errors 32, blank boxesrendered 32/32 · errors 0Root cause
onDetachedFromWindowcancels the pendingrenderJob— but the detach is onlytransient (the flattening re-parent above; also view moves,
RecyclerView, RN#38567). The view comes right
back; its job doesn't.
rerender()trigger is a prop setter guarded byif (field == value) return, and the props never change again → blank forever.catch (e: Throwable)swallows theCancellationExceptionand reports it as a rendererror. For a
CancellationExceptionto terminate a coroutine properly it must reachthe top of the call stack uncaught.
The fix
Applied to
RaTeXView.kt+RaTeXInlineView.kt(react-native) and the standaloneplatforms/androidRaTeXView.kt— the same pattern Coil uses inViewTargetRequestManager(cancel on detach is only safe when paired with restart on attach):
onAttachedToWindowre-kicks the render when nothing is rendered, content isnon-blank, and no job is in flight (a normal attach is a no-op).
CancellationExceptioninstead of turning it into an error state + bogusonError(see kotlinx.coroutines#1814).scope(+SupervisorJob) — a cancelled scope ignores every futurelaunch, killing the viewfor good.
Validation
document went from mostly-blank to all rendered, pixel-identical across a remount
cycle (
pr-screenshots/android-broken-blank-formulas.png/android-fixed-all-formulas.png).Consumer-side tip (independent of this fix): pinning the revealing wrapper with
collapsable={false}keeps its flattening status stable and avoids the detach waveentirely.