Render failures and Android interop effects
RenderSession keeps render failures observable without turning recoverable frame failures into
process crashes. Root hosts accept one correlated RenderDiagnostics sink.
fun renderDiagnosticsEventSample(): RenderDiagnostics {
return RenderDiagnostics(
collection = RenderDiagnosticCollection(
lifecycle = true,
failures = true,
frameLevel = RenderFrameDiagnosticLevel.Stats,
),
sink = { event ->
when (event) {
is RenderFrameCompleted -> println(event.stats)
is RenderFailureObserved -> println(event.failure.phase)
is RenderSessionStarted,
is RenderSessionActivityChanged,
is RenderSessionEnded,
-> println(event.context)
}
},
)
}
The host RenderSession also exposes lastRenderFailure and lastFrameReport. A frame report is
either Committed or RolledBack and contains every synchronous failure observed in that frame.
Asynchronous composition-coroutine failures are reported separately and do not rewrite an already
completed frame report.
Recovery guarantees
CompositionPrepareandViewTreeRenderfailures abort the candidate composition and reportPreviousFrameRestored. The renderer restores the previous VNode bindings, mounted children, layout parameters, View order, and releases newly inserted nodes on a best-effort basis.ObservedPropertyPreparereportsFrameUnchanged: candidate values and dependency guards are abandoned before any native mutation.ObservedPropertyRenderreportsPreviousFrameRestored: the renderer preflights the complete exact-target batch and rebinds every earlier target to its committed VNode when one patch fails.ObservedPropertyCommitreportsFrameCommittedbecause native values are already authoritative; dependency commit failures are observable and do not silently trigger a whole-tree fallback.- commit, side-effect, overlay, and native commit failures report
FrameCommitted. These happen after the new View tree has become authoritative and are isolated so that one failure does not prevent the remaining operations from running. A throwing remembered activation stays pending and is retried by a later successful composition commit; successful siblings are not activated twice, and removal before success abandons the pending value. - composition-coroutine failures report
FrameUnchanged. - disposal failures report
SessionDisposed; cleanup continues across remaining nodes and hosts. - a throwing diagnostics sink is stored locally as
DiagnosticsSink, disabled for that session, and never changes the authoritative frame report or recursively emits a failure event.
RenderFailureOperation and nodeKey identify AndroidView factory, update, reset, commit, and
release failures without parsing exception messages.
Optional bounded production aggregation
Applications that need recurring-failure counts can install BoundedRenderFailureAggregator from
the optional viewcompose-diagnostics artifact as the failure-only root sink. The default
fingerprint retains phase, recovery, optional Android View operation, direct exception binary type,
and at most three class/method-only com.viewcompose.* frames. It never retains the message,
cause chain, application frames, file/line data, nodeKey, or original Throwable.
The aggregator defaults to 64 distinct fingerprints in a 15-minute monotonic window, with hard
valid ranges of 1..128 and one minute through 24 hours. Capacity evicts the least recently
updated fingerprint and reports both lost observations and evicted entries. Expiration occurs only
on record or snapshot; there is no timer, storage, transport, vendor SDK, or process-global sink.
Snapshots are immutable application-owned values. Export them outside synchronous sink delivery so
network or persistence work cannot block a render session.
See the Diagnostics module manual for exact redaction, synchronization, reset, and counter contracts.
AndroidView side-effect boundary
AndroidView has two deliberately different update paths:
AndroidView(
key = playerId,
factory = { context -> PlayerView(context) },
update = { view ->
// Replay-safe View configuration only.
view.isEnabled = enabled
},
onReset = { view ->
// Replay-safe cleanup before the View is rebound.
view.player = null
},
onCommit = { view ->
// Non-replayable external action. Runs only after a successful tree transaction.
analytics.recordPlayerAttached(playerId)
},
onRelease = { view ->
// One-shot resource release after any permanent abandonment.
view.player = null
},
)
The rules are strict:
factory,update,onReset, andModifier.nativeVieware part of the renderer transaction.updatecan run again while an old node is restored after a later binding fails. Keep these callbacks idempotent and limited to the supplied View.- Put network writes, analytics, database writes, service calls, or other non-replayable external
effects in
onCommit. The renderer publishes these callbacks only after the complete recursive View-tree transaction commits. A rolled-back candidate never publishes or runs them. onReleaseis resource cleanup, not a general commit effect. It runs at most once whenever a created node is permanently abandoned, including candidate rollback, successful removal, final reuse-cache eviction, or session disposal.- Native platform state cannot be cloned generically. The rollback guarantee therefore covers framework-owned tree structure plus replay of the previous View configuration, not arbitrary state hidden inside a third-party View.