feat: add worklet bridge for Reanimated shared value support - #250
Open
mfazekas wants to merge 137 commits into
Open
feat: add worklet bridge for Reanimated shared value support#250mfazekas wants to merge 137 commits into
mfazekas wants to merge 137 commits into
Conversation
mfazekas
force-pushed
the
feat/worklet-bridge
branch
from
May 12, 2026 13:57
53f140a to
9613d1e
Compare
mfazekas
force-pushed
the
feat/rive-ios-experimental
branch
from
May 19, 2026 06:47
80b8072 to
f299fe1
Compare
Add new experimental iOS backend (ios/new/) with synchronous API, move legacy backend files to ios/legacy/, add getEnums() support, retry listener streams on missingData, and restore TestComponentOverlay. Generated with [Claude Code](https://claude.ai/code) via [Happy](https://happy.engineering)
Generated with [Claude Code](https://claude.ai/code) via [Happy](https://happy.engineering)
getEnums() in legacy now throws directing users to the experimental backend instead of creating throwaway Worker+File instances. Generated with [Claude Code](https://claude.ai/code) via [Happy](https://happy.engineering)
…essing Generated with [Claude Code](https://claude.ai/code) via [Happy](https://happy.engineering)
… binding Each Worker has its own C++ command server with its own m_artboards handle map. Creating separate Workers per file meant artboard handles from one file were invalid on another file's server. Using a shared singleton Worker fixes cross-file artboard property set. Also wires fit/alignment through experimental Fit enum and improves asset type detection with audio/font magic bytes. Generated with [Claude Code](https://claude.ai/code) via [Happy](https://happy.engineering) Co-Authored-By: Happy <yesreply@happy.engineering>
Passing fit to the Rive() constructor breaks layout mode because the MTKView drawable isn't ready yet. Set rive.fit after setupRiveUIView() instead.
Port Flutter data binding tests for VM access, enums, creation variants, list properties, artboard and image properties. Includes new .riv test assets and react-native-harness upgrade to alpha.25.
…ceByIndex Prevents fatal crash when passing negative numbers to Swift APIs expecting unsigned integers.
CocoaPods doesn't embed SPM-resolved dynamic frameworks automatically. Patches the embed script to include RiveRuntime when USE_RIVE_SPM=1.
Move 19 backend-specific files to src/legacy/java/, add Gradle sourceSets switching via USE_RIVE_NEW_API property, prepare empty experimental dirs.
Uses the new handle-based Rive Android SDK (app.rive.*) with CommandQueue, path-based ViewModelInstance property access, and Kotlin Flows for reactivity. Throws UnsupportedOperationException for SMI inputs, text runs, and events. Also fixes Gradle property resolution to use rootProject.findProperty.
Custom TextureView renderer with Choreographer render loop, CommandQueue polling infrastructure, ViewModel resolution and property validation, and example Compose test activities for manual testing.
Also fix viewModelByIndex/ByName on Android legacy to catch SDK exceptions instead of letting them propagate as JniExceptions.
…fferences Fix colorProperty setter Double→Int overflow on Android legacy (toLong().toInt()). Make enum and replaceViewModel tests tolerant of Android SDK behavioral differences.
Use .viewModelDefault(from: .name(vmName)) for default instance creation instead of artboard-based lookup that only worked for the default artboard's VM. Skip instanceName, color get/set, and non-existent property checks on experimental iOS where the SDK doesn't support them.
…anceName Add async variants for viewModelByName, defaultArtboardViewModel, and ViewModel create methods. Pass instanceName through at creation time on both iOS and Android experimental backends as a workaround for the SDK not exposing ViewModelInstance.name.
Bump example app from RN 0.79.2 to 0.80.3. This unblocks upgrading to reanimated 4.2+ and worklets 0.7+ for Nitro auto-box support. - react 19.0.0 → 19.1.0 - react-native 0.79.2 → 0.80.3 - @react-native-community/cli 18.0.0 → 19.1.2 - @react-native/* packages 0.79.2 → 0.80.3 - Kotlin 2.0.21 → 2.1.20, Gradle 8.13 → 8.14.1 - Removed compose-compiler-gradle-plugin (no longer needed)
Bump react-native-harness and platform packages from 1.0.0 to 1.1.0.
## Summary Adds 31 harness tests for the non-deprecated async API that previously had zero test coverage. Existing tests only exercise the deprecated sync methods (`.value`, `viewModelByName`, `createInstanceByName`, etc.) — this fills the gap for the intended async replacements. ## What's covered - `createDefaultInstanceAsync`, `createInstanceByNameAsync`, `createBlankInstanceAsync` - `defaultArtboardViewModelAsync`, `getArtboardCountAsync`, `getArtboardNamesAsync` - `getPropertiesAsync`, `getPropertyCountAsync`, `getInstanceCountAsync` - `getValueAsync()` + `set()` round-trip for all 5 property types (number, string, boolean, color, enum) - `viewModelAsync` for nested ViewModel access - Async list operations: `getLengthAsync`, `getInstanceAtAsync`, `addInstanceAsync`, `removeInstanceAtAsync`, `swapAsync` ## Test plan - [x] `yarn test:harness:ios -- __tests__/async-api.harness.ts` — 31/31 pass - [x] Full suite `yarn test:harness:ios` — 136 pass (no regressions)
coverage-ios is now integrated in the harness packages directly, removing the local file: dependency.
Fixes flaky autoplay harness tests. The 6.20.1 fix for "massive timestamp delta on first frame" was causing the animation to jump through its entire cycle in one frame, making ypos property change detection unreliable. Verified with 100 consecutive test runs (0 failures).
Adds 15 new harness tests covering listener callback invocation, set() + getValueAsync() round-trips, and removeListeners() for all ViewModel property types. iOS native coverage goes from 41% to 50%. Target types: BooleanProperty (16→84%), ColorProperty (14→84%), EnumProperty (16→84%), TriggerProperty (0→81%), StringProperty (37→84%), NumberProperty (85→90%).
Adds harness tests for two previously untested areas: **Font config** (6 tests): loadFont by system name, loadFont by URL, systemFallback, setFallbackFonts with default/weight-specific fonts, clearFallbackFonts, error on invalid font name. **Asset loading** (4 tests): referencedAssets with font type, image URL type, multiple asset types, and undefined assets. Covers HybridRiveFontConfig.swift (was 0%), AssetLoader.swift (was 1%), DataSourceResolver.swift (was 0%), HTTPDataLoader.swift (was 43%).
Adds 7 new tests covering previously untested native code paths: - **fromBytes**: load .riv from raw ArrayBuffer (HybridRiveFileFactory) - **RiveImages.loadFromURLAsync**: load image and verify byteSize (HybridRiveImageFactory + HybridRiveImage) - **RiveLog.setLogLevel**: exercise all 4 log levels (HybridRiveLogger) - **RiveView pause/play/reset**: exercise view control methods with a rendered RiveView (HybridRiveView)
Upgrades harness from 1.2.0 to 1.3.0. Key improvements from harness PRs #129 and #132: - Bridge RPC closes immediately on app disconnect instead of waiting for timeout - New internal RPC transport with heartbeat-based liveness detection - Dead app connections fail fast with `AppBridgeDisconnectedError` instead of ambiguous "device did not respond" after 90s Should reduce the intermittent CI flakiness we've been seeing on legacy iOS.
mfazekas
force-pushed
the
feat/rive-ios-experimental
branch
from
June 1, 2026 09:21
a5bf096 to
42e7d76
Compare
…ntModificationException (#286) `onChanged` iterates `listeners.values` on a `Dispatchers.Default` thread (the flow-collection coroutine), while `addListener`/`removeListener` mutate the same map from the JS thread. A plain `LinkedHashMap` fails fast with `ConcurrentModificationException` when this overlap occurs. `ConcurrentHashMap` iterates safely with concurrent mutations — no CME, no locks needed at call sites.
…oid) (#285) ## Problem On the experimental backend, `play()`/`pause()`/`reset()` were no-ops — animations couldn't be paused/resumed and `autoPlay={false}` was ignored. - **iOS**: only mutated a private `isPaused` flag never forwarded to `RiveUIView`. - **Android**: empty bodies; the render loop advanced unconditionally and `autoPlay` was never consumed. Additionally, `useRiveTrigger` never fired on Android experimental — three separate issues: 1. `awaitViewReady()` resolved at end of `configure()`, before the TextureView surface existed. Triggers fired before the surface was up were lost. 2. `getTriggerFlow` has `replay=0` (no initial value on subscription); `drop=1` was silently eating the first real trigger. 3. The listener coroutine was launched with `DEFAULT` start, so it was merely queued on `Dispatchers.Default` and could miss a trigger fired before it reached `collect()`. Also, `dataBind={{ byName }}` on Android always bound from `vmNames.first()` (first VM in file) instead of the artboard's default ViewModel. ## Fix - **iOS** (`ios/new/RiveReactNativeView.swift`): forward `isPaused` to `RiveUIView` in play/pause/playIfNeeded, pass it into `RiveUIView(rive:isPaused:)`, set it from `autoPlay`. - **Android** (`android/src/new/.../RiveReactNativeView.kt`): gate `advanceStateMachine` on a `paused` flag toggled by play/pause, honor `autoPlay`. Signal `awaitViewReady()` on first rendered frame (not end of configure). - **Android triggers**: use `drop=0` for trigger flow (no initial value to skip); launch listener coroutine with `CoroutineStart.UNDISPATCHED`. - **Android byName**: use `ViewModelSource.DefaultForArtboard(art)` instead of `Named(vmNames.first())`, matching iOS and the Auto path. - **reset()**: deprecated. Neither experimental backend has a reset primitive — marked `@deprecated` in the spec, experimental impl logs an error and does nothing. ## Verification - `autoplay.harness.tsx`: pause freezes `ypos`, play resumes it, `autoPlay={false}` holds. Green on both platforms. - `use-rive-trigger.harness.tsx`: both stable and unstable-callback tests now pass on Android. - `viewmodel-instance-lookup.harness.tsx`: byName regression test verifies `artboard2 + byName("vmi1") → _id="vm2.vmi1.id"` (not vm1). - CI: legacy iOS `pod install` step clears `Podfile.lock` to avoid external `fast_float` checksum rejection.
`asyncExecuteOnUiThread` posts to the UI thread and returns immediately,
so the Promise resolves before the work runs. If the action throws, it
crashes on the UI thread instead of rejecting the Promise.
`play()`/`pause()`/`reset()` on the experimental backend are just flag
toggles or a deprecation log — no UI thread requirement, no throw.
Replace with `Promise.async { view.play() }` etc.
Property accessors (`numberProperty`, `stringProperty`, etc.) were using
`runBlocking { flow.first() }` as an existence probe before returning
the wrapper. This blocks the calling thread and can deadlock if called
from the main thread (e.g. via a worklet).
The iOS experimental backend already does the right thing — it just
creates and returns the wrapper directly. This PR aligns Android
experimental to the same behaviour: return a wrapper always, fail lazily
when `getValueAsync()` is called on an invalid path.
Also removes the `propertyNames` lazy val (used for `hasProperty`) which
had its own one-time `runBlocking` call.
Test: updated guard for the "non-existent returns undefined" test (skips
on all experimental backends), added new test asserting
`getValueAsync()` rejects for invalid paths on experimental.
Removes unreachable and never-populated code from the experimental iOS backend. - Drop `eventListeners`, `addEventListener`, `removeEventListeners` from `RiveReactNativeView` — the Hybrid layer throws before these are ever called - Drop `listenerTasks` from `HybridViewModelListProperty` and `HybridViewModelImageProperty` — the dict is declared and torn down but never populated
…iew reuse, bind-before-ready (#289) Addresses several correctness issues from the experimental runtime review. - **Android**: cancel auto-bind `CoroutineScope` on dispose; adds `disposed` guard inside the Main-thread hop to prevent post-teardown binding against a stale/reconfigured handle - **iOS**: reuse existing `RiveUIView` on reconfigure by assigning `riveUIView.rive = rive` instead of tearing down and recreating the view — eliminates orphaned MTKView draw calls on file switch - **iOS**: queue `bindViewModelInstance` calls that arrive before `configTask` completes, then apply them once `riveInstance` is set — previously the bind was silently dropped Includes harness tests for the iOS fixes: - `reconfigure.harness.tsx` — verifies animation still plays after switching file prop and back - `bind-before-ready.harness.tsx` — reproduces and verifies the bind-before-ready race (failed before fix, passes after)
Stacked on #289. Adds `setValueAsync(value)` to Number, String, Boolean, Color, and Enum property types across all 4 backends (iOS experimental, iOS legacy, Android experimental, Android legacy). On iOS experimental, this is a proper async write — the promise resolves only after the SDK `setValue` call completes on the main actor, giving ordering guarantees and error propagation. On legacy and Android, `set()` is already synchronous so `setValueAsync` is a thin wrapper that makes the API consistent. Includes harness tests covering all 5 property types.
…ental # Conflicts: # example/package.json # package.json # yarn.lock
Adds getPropertiesAsync() to ViewModel and ViewModelInstance for runtime introspection. Returns ViewModelPropertyInfo[] with name and type for each property.
Port from #79. Adds RiveWorkletBridge HybridObject that installs Nitro's dispatcher on Reanimated's UI runtime, enabling ViewModel property listeners to drive SharedValues on the UI thread without blocking when JS is busy. Also bumps rive-ios to 6.20.2 and adds bouncing ball exerciser demo.
Suppress eslint no-deprecated warnings for runOnUI, defaultArtboardViewModel, and createDefaultInstance. These are properly fixed in the next stacked PR.
mfazekas
force-pushed
the
feat/worklet-bridge
branch
from
June 12, 2026 04:56
0b0d2e0 to
f245c11
Compare
…letDispatcher to JLooperDispatcher
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.
Port of #79 to the experimental repo. Adds worklet bridge that installs Nitro's dispatcher on Reanimated's UI runtime, enabling ViewModel property listeners to drive SharedValues on the UI thread.
Stack: