rive-app / rive-app/rive-android
Rive composable causes ComposeNotIdleException in Espresso/Compose UI tests due to infinite withFrameNanos loop
Nobody has claimed this yet.
- Dominant language
- Kotlin
- Stars
- 538
- Forks
- 66
- PR merge metrics
- No merged PRs in 30d
Description
Bug: Rive composable keeps Compose recomposer perpetually busy
Labels: bug testing compose
Any screen containing a Rive composable (from the Compose integration in rive-android v11.3.1) prevents Espresso and Compose UI Test from ever reaching idle state. All waitForIdle(), waitUntil(), and fetchSemanticsNodes() calls time out with ComposeNotIdleException.
Root cause
There are two sources of continuous withFrameNanos usage.
Source 1 — Rive composable: draw loop never yields
In Rive.kt, the drawing loop calls withFrameNanos on every frame, even when the state machine is settled. When isSettled = true, the loop continues spinning on the frame clock without doing any work:
lifecycleOwner.lifecycle.repeatOnLifecycle(Lifecycle.State.RESUMED) {
var lastFrameTime = 0.nanoseconds
while (isActive) {
val deltaTime = withFrameNanos { ... } // runs every frame
if (isSettled) {
continue // skips draw but still occupies the frame clock
}
// advance and draw
}
}
Source 2 — rememberRiveWorker: uses ComposeFrameTicker
In rememberRiveWorker.kt, the worker begins polling with ComposeFrameTicker, which also uses withFrameNanos — adding a second continuous consumer of the Compose frame clock:
worker.beginPolling(lifecycleOwner.lifecycle, ComposeFrameTicker)
Impact
ComposeNotIdleException: Idling resource timed out: possibly due to compose being busy.
- [busy] ComposeIdlingResource is busy due to pending recompositions.
- Debug: hadRecomposerChanges = true, hadSnapshotChanges = false, hadAwaitersOnMainClock = false
This affects all Rive animations — not just actively playing ones — since settled animations still run the withFrameNanos loop. Any screen containing a Rive composable makes it impossible to:
- Find nodes via
onNodeWithTag()/onNodeWithText() - Wait for conditions via
waitUntil() - Perform any assertion that requires idle state
Suggested fix
Option A — Break the loop when settled (minimal change)
Suspend until isSettled flips to false instead of spinning on withFrameNanos:
while (isActive) {
if (isSettled) {
// suspend until unsettled, instead of spinning on withFrameNanos
snapshotFlow { isSettled }.first { !it }
continue
}
val deltaTime = withFrameNanos { ... }
// advance and draw
}
Option B — Use ChoreographerFrameTicker for the draw loop
Replace the withFrameNanos call in the draw loop with ChoreographerFrameTicker, which already exists in the codebase. This decouples animation rendering from Compose's frame clock entirely — matching the behavior of the View-based API.
Option C — Make FrameTicker configurable
Expose a parameter on rememberRiveWorker and/or the Rive composable to choose between ComposeFrameTicker and ChoreographerFrameTicker. This lets apps opt into Choreographer-based timing for test compatibility.
Environment
rive-android |
11.3.1 |
| Compose BOM | 2024.x |
| Test framework | compose-ui-test-junit4 + Espresso |
| Android API | 34 |
Reproduction
- Place a
Rivecomposable on any screen. - Write a Compose UI test that navigates to that screen.
- Call
composeTestRule.onNodeWithTag("any_tag")orwaitUntil { ... }. - Test times out with
ComposeNotIdleException.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with Rive.kt and rememberRiveWorker.kt, then reproduce the issue using a Compose UI test with a Rive composable and waitForIdle or waitUntil. Compare the draw loop and worker frame-ticker paths, and consider the listed alternatives. Done means screens containing Rive can reach Compose test idle state without ComposeNotIdleException.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- android, kotlin
- Domain
- mobile-dev, testing
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 65/100