rive-app / rive-app/rive-android

Rive composable causes ComposeNotIdleException in Espresso/Compose UI tests due to infinite withFrameNanos loop

Open
#444 3 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

bug confirmed
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

  1. Place a Rive composable on any screen.
  2. Write a Compose UI test that navigates to that screen.
  3. Call composeTestRule.onNodeWithTag("any_tag") or waitUntil { ... }.
  4. Test times out with ComposeNotIdleException.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.