Skip to content

feat!: experimental Rive runtime backend (iOS + Android)#134

Open
mfazekas wants to merge 75 commits intomainfrom
feat/rive-ios-experimental
Open

feat!: experimental Rive runtime backend (iOS + Android)#134
mfazekas wants to merge 75 commits intomainfrom
feat/rive-ios-experimental

Conversation

@mfazekas
Copy link
Copy Markdown
Collaborator

@mfazekas mfazekas commented Jan 23, 2026

Adds a new native backend using Rive's experimental runtime APIs on both iOS and Android. The new backend is async-native — all ViewModel operations go through a CommandQueue, eliminating the need for blockingAsync/runBlocking wrappers on the non-deprecated API surface.

The experimental backend is now the default. Legacy backend files are moved to ios/legacy/ and android/src/legacy/ (identical to main except getEnums() stub and backend property). New implementations live in ios/new/ and android/src/experimental/.

Opting into the legacy backend

# iOS
USE_RIVE_LEGACY=1 pod install

# Android — add to gradle.properties
USE_RIVE_LEGACY=true

Without the flag, the experimental backend is used.

Known limitations (Android experimental)

  • defaultArtboardViewModel doesn't expose the ViewModel name — pending rive-android#443
  • replaceViewModel is a no-op (not yet implemented)
  • viewModelAsync('nonexistent') returns a handle instead of null (SDK doesn't validate paths)
  • Pointer events / Rive Listeners broken in experimental CommandQueue API (upstream SDK bug)

@mfazekas mfazekas force-pushed the feat/rive-ios-experimental branch 2 times, most recently from 95816cf to 4fe9e12 Compare January 23, 2026 11:23
@mfazekas mfazekas force-pushed the feat/rive-ios-experimental branch 4 times, most recently from 8485f9f to 9b4acd8 Compare February 9, 2026 10:25
@mfazekas mfazekas changed the title feat: experimental iOS API support (getEnums via SPM) feat: experimental iOS API support Feb 9, 2026
Copy link
Copy Markdown
Contributor

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remaining comments which cannot be posted as a review comment to avoid GitHub Rate Limit

ktlint

🚫 [ktlint] standard:multiline-if-else reported by reviewdog 🐶
Missing { ... }

data[2] == 0x54.toByte() && data[3] == 0x4F.toByte()) return AssetType.FONT


🚫 [ktlint] standard:chain-method-continuation reported by reviewdog 🐶
Expected newline before '.'


🚫 [ktlint] standard:chain-method-continuation reported by reviewdog 🐶
Expected newline before '.'


🚫 [ktlint] standard:try-catch-finally-spacing reported by reviewdog 🐶
Expected a newline after '{'


🚫 [ktlint] standard:statement-wrapping reported by reviewdog 🐶
Missing newline after '{'


🚫 [ktlint] standard:statement-wrapping reported by reviewdog 🐶
Missing newline before '}'


🚫 [ktlint] standard:try-catch-finally-spacing reported by reviewdog 🐶
Expected a newline before '}'


🚫 [ktlint] standard:try-catch-finally-spacing reported by reviewdog 🐶
Expected a newline after '{'


🚫 [ktlint] standard:statement-wrapping reported by reviewdog 🐶
Missing newline after '{'


🚫 [ktlint] standard:statement-wrapping reported by reviewdog 🐶
Missing newline before '}'


🚫 [ktlint] standard:try-catch-finally-spacing reported by reviewdog 🐶
Expected a newline before '}'


🚫 [ktlint] standard:no-unused-imports reported by reviewdog 🐶
Unused import


🚫 [ktlint] standard:string-template reported by reviewdog 🐶
Redundant curly braces

Log.d(TAG, "onSurfaceTextureAvailable: ${w}x${h} worker=${this@RiveReactNativeView.riveWorker != null}")


🚫 [ktlint] standard:if-else-wrapping reported by reviewdog 🐶
Expected a newline

val deltaTime = if (lastFrameTimeNs == 0L) Duration.ZERO


🚫 [ktlint] standard:multiline-if-else reported by reviewdog 🐶
Missing { ... }

val deltaTime = if (lastFrameTimeNs == 0L) Duration.ZERO


🚫 [ktlint] standard:if-else-wrapping reported by reviewdog 🐶
Expected a newline

else (frameTimeNanos - lastFrameTimeNs).nanoseconds


🚫 [ktlint] standard:multiline-if-else reported by reviewdog 🐶
Missing { ... }

else (frameTimeNanos - lastFrameTimeNs).nanoseconds


🚫 [ktlint] standard:chain-method-continuation reported by reviewdog 🐶
Expected newline before '.'

Log.d(TAG, "configure: reload=$reload initialUpdate=$initialUpdate fit=$activeFit surfaceTexture=${surfaceTexture != null} surfaceW=${surfaceWidth} surfaceH=${surfaceHeight}")


🚫 [ktlint] standard:argument-list-wrapping reported by reviewdog 🐶
Argument should be on a separate line (unless all arguments can fit a single line)

Log.d(TAG, "configure: reload=$reload initialUpdate=$initialUpdate fit=$activeFit surfaceTexture=${surfaceTexture != null} surfaceW=${surfaceWidth} surfaceH=${surfaceHeight}")


🚫 [ktlint] standard:argument-list-wrapping reported by reviewdog 🐶
Argument should be on a separate line (unless all arguments can fit a single line)

Log.d(TAG, "configure: reload=$reload initialUpdate=$initialUpdate fit=$activeFit surfaceTexture=${surfaceTexture != null} surfaceW=${surfaceWidth} surfaceH=${surfaceHeight}")


🚫 [ktlint] standard:string-template reported by reviewdog 🐶
Redundant curly braces

Log.d(TAG, "configure: reload=$reload initialUpdate=$initialUpdate fit=$activeFit surfaceTexture=${surfaceTexture != null} surfaceW=${surfaceWidth} surfaceH=${surfaceHeight}")


🚫 [ktlint] standard:max-line-length reported by reviewdog 🐶
Exceeded max line length (140)

Log.d(TAG, "configure: reload=$reload initialUpdate=$initialUpdate fit=$activeFit surfaceTexture=${surfaceTexture != null} surfaceW=${surfaceWidth} surfaceH=${surfaceHeight}")


🚫 [ktlint] standard:string-template reported by reviewdog 🐶
Redundant curly braces

Log.d(TAG, "configure: reload=$reload initialUpdate=$initialUpdate fit=$activeFit surfaceTexture=${surfaceTexture != null} surfaceW=${surfaceWidth} surfaceH=${surfaceHeight}")


🚫 [ktlint] standard:argument-list-wrapping reported by reviewdog 🐶
Missing newline before ")"

Log.d(TAG, "configure: reload=$reload initialUpdate=$initialUpdate fit=$activeFit surfaceTexture=${surfaceTexture != null} surfaceW=${surfaceWidth} surfaceH=${surfaceHeight}")


🚫 [ktlint] standard:statement-wrapping reported by reviewdog 🐶
Missing newline after '{'

val worker = riveWorker ?: run { Log.w(TAG, "touch: no worker"); return }


🚫 [ktlint] standard:statement-wrapping reported by reviewdog 🐶
Missing newline after ';'

val worker = riveWorker ?: run { Log.w(TAG, "touch: no worker"); return }


🚫 [ktlint] standard:statement-wrapping reported by reviewdog 🐶
Missing newline before '}'

val worker = riveWorker ?: run { Log.w(TAG, "touch: no worker"); return }


🚫 [ktlint] standard:statement-wrapping reported by reviewdog 🐶
Missing newline after '{'

val smHandle = stateMachineHandle ?: run { Log.w(TAG, "touch: no smHandle"); return }


🚫 [ktlint] standard:statement-wrapping reported by reviewdog 🐶
Missing newline after ';'

val smHandle = stateMachineHandle ?: run { Log.w(TAG, "touch: no smHandle"); return }


🚫 [ktlint] standard:statement-wrapping reported by reviewdog 🐶
Missing newline before '}'

val smHandle = stateMachineHandle ?: run { Log.w(TAG, "touch: no smHandle"); return }


🚫 [ktlint] standard:if-else-wrapping reported by reviewdog 🐶
A single line if-statement should be kept simple. The 'THEN' may not be wrapped in a block.

if (w <= 0 || h <= 0) { Log.w(TAG, "touch: invalid surface ${w}x${h}"); return }


🚫 [ktlint] standard:statement-wrapping reported by reviewdog 🐶
Missing newline after '{'

if (w <= 0 || h <= 0) { Log.w(TAG, "touch: invalid surface ${w}x${h}"); return }


🚫 [ktlint] standard:string-template reported by reviewdog 🐶
Redundant curly braces

if (w <= 0 || h <= 0) { Log.w(TAG, "touch: invalid surface ${w}x${h}"); return }


🚫 [ktlint] standard:statement-wrapping reported by reviewdog 🐶
Missing newline after ';'

if (w <= 0 || h <= 0) { Log.w(TAG, "touch: invalid surface ${w}x${h}"); return }


🚫 [ktlint] standard:statement-wrapping reported by reviewdog 🐶
Missing newline before '}'

if (w <= 0 || h <= 0) { Log.w(TAG, "touch: invalid surface ${w}x${h}"); return }


🚫 [ktlint] standard:chain-method-continuation reported by reviewdog 🐶
Expected newline before '.'

val legacyFile = app.rive.runtime.kotlin.core.File(bytes)


🚫 [ktlint] standard:chain-method-continuation reported by reviewdog 🐶
Expected newline before '.'

Log.d("ComposeRiveTest", "[$label] input[$j]: name=${input.name} isBoolean=${input.isBoolean} isTrigger=${input.isTrigger} isNumber=${input.isNumber}")


🚫 [ktlint] standard:argument-list-wrapping reported by reviewdog 🐶
Argument should be on a separate line (unless all arguments can fit a single line)

Log.d("ComposeRiveTest", "[$label] input[$j]: name=${input.name} isBoolean=${input.isBoolean} isTrigger=${input.isTrigger} isNumber=${input.isNumber}")


🚫 [ktlint] standard:argument-list-wrapping reported by reviewdog 🐶
Argument should be on a separate line (unless all arguments can fit a single line)

Log.d("ComposeRiveTest", "[$label] input[$j]: name=${input.name} isBoolean=${input.isBoolean} isTrigger=${input.isTrigger} isNumber=${input.isNumber}")


🚫 [ktlint] standard:max-line-length reported by reviewdog 🐶
Exceeded max line length (140)

Log.d("ComposeRiveTest", "[$label] input[$j]: name=${input.name} isBoolean=${input.isBoolean} isTrigger=${input.isTrigger} isNumber=${input.isNumber}")


🚫 [ktlint] standard:argument-list-wrapping reported by reviewdog 🐶
Missing newline before ")"

Log.d("ComposeRiveTest", "[$label] input[$j]: name=${input.name} isBoolean=${input.isBoolean} isTrigger=${input.isTrigger} isNumber=${input.isNumber}")


🚫 [ktlint] standard:function-naming reported by reviewdog 🐶
Function name should start with a lowercase letter (except factory methods) and use camel case


🚫 [ktlint] standard:chain-method-continuation reported by reviewdog 🐶
Expected newline before '.'

Log.d("ComposeRiveTest", "artboard=${artboard.artboardHandle} sm=${stateMachine.stateMachineHandle} name=${artboard.name} smName=${stateMachine.name}")


🚫 [ktlint] standard:argument-list-wrapping reported by reviewdog 🐶
Argument should be on a separate line (unless all arguments can fit a single line)

Log.d("ComposeRiveTest", "artboard=${artboard.artboardHandle} sm=${stateMachine.stateMachineHandle} name=${artboard.name} smName=${stateMachine.name}")


🚫 [ktlint] standard:argument-list-wrapping reported by reviewdog 🐶
Argument should be on a separate line (unless all arguments can fit a single line)

Log.d("ComposeRiveTest", "artboard=${artboard.artboardHandle} sm=${stateMachine.stateMachineHandle} name=${artboard.name} smName=${stateMachine.name}")


🚫 [ktlint] standard:max-line-length reported by reviewdog 🐶
Exceeded max line length (140)

Log.d("ComposeRiveTest", "artboard=${artboard.artboardHandle} sm=${stateMachine.stateMachineHandle} name=${artboard.name} smName=${stateMachine.name}")


🚫 [ktlint] standard:argument-list-wrapping reported by reviewdog 🐶
Missing newline before ")"

Log.d("ComposeRiveTest", "artboard=${artboard.artboardHandle} sm=${stateMachine.stateMachineHandle} name=${artboard.name} smName=${stateMachine.name}")

@mfazekas mfazekas force-pushed the feat/rive-ios-experimental branch from dd06b3a to a5855c9 Compare February 16, 2026 10:41
Copy link
Copy Markdown
Contributor

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remaining comments which cannot be posted as a review comment to avoid GitHub Rate Limit

ktlint

🚫 [ktlint] standard:if-else-wrapping reported by reviewdog 🐶
A single line if-statement should be kept simple. The 'THEN' may not be wrapped in a block.

if (w <= 0 || h <= 0) { Log.w(TAG, "touch: invalid surface ${w}x${h}"); return }


🚫 [ktlint] standard:statement-wrapping reported by reviewdog 🐶
Missing newline after '{'

if (w <= 0 || h <= 0) { Log.w(TAG, "touch: invalid surface ${w}x${h}"); return }


🚫 [ktlint] standard:string-template reported by reviewdog 🐶
Redundant curly braces

if (w <= 0 || h <= 0) { Log.w(TAG, "touch: invalid surface ${w}x${h}"); return }


🚫 [ktlint] standard:statement-wrapping reported by reviewdog 🐶
Missing newline after ';'

if (w <= 0 || h <= 0) { Log.w(TAG, "touch: invalid surface ${w}x${h}"); return }


🚫 [ktlint] standard:statement-wrapping reported by reviewdog 🐶
Missing newline before '}'

if (w <= 0 || h <= 0) { Log.w(TAG, "touch: invalid surface ${w}x${h}"); return }


🚫 [ktlint] standard:chain-method-continuation reported by reviewdog 🐶
Expected newline before '.'

val legacyFile = app.rive.runtime.kotlin.core.File(bytes)


🚫 [ktlint] standard:chain-method-continuation reported by reviewdog 🐶
Expected newline before '.'

Log.d("ComposeRiveTest", "[$label] input[$j]: name=${input.name} isBoolean=${input.isBoolean} isTrigger=${input.isTrigger} isNumber=${input.isNumber}")


🚫 [ktlint] standard:argument-list-wrapping reported by reviewdog 🐶
Argument should be on a separate line (unless all arguments can fit a single line)

Log.d("ComposeRiveTest", "[$label] input[$j]: name=${input.name} isBoolean=${input.isBoolean} isTrigger=${input.isTrigger} isNumber=${input.isNumber}")


🚫 [ktlint] standard:argument-list-wrapping reported by reviewdog 🐶
Argument should be on a separate line (unless all arguments can fit a single line)

Log.d("ComposeRiveTest", "[$label] input[$j]: name=${input.name} isBoolean=${input.isBoolean} isTrigger=${input.isTrigger} isNumber=${input.isNumber}")


🚫 [ktlint] standard:max-line-length reported by reviewdog 🐶
Exceeded max line length (140)

Log.d("ComposeRiveTest", "[$label] input[$j]: name=${input.name} isBoolean=${input.isBoolean} isTrigger=${input.isTrigger} isNumber=${input.isNumber}")


🚫 [ktlint] standard:argument-list-wrapping reported by reviewdog 🐶
Missing newline before ")"

Log.d("ComposeRiveTest", "[$label] input[$j]: name=${input.name} isBoolean=${input.isBoolean} isTrigger=${input.isTrigger} isNumber=${input.isNumber}")


🚫 [ktlint] standard:function-naming reported by reviewdog 🐶
Function name should start with a lowercase letter (except factory methods) and use camel case


🚫 [ktlint] standard:chain-method-continuation reported by reviewdog 🐶
Expected newline before '.'

Log.d("ComposeRiveTest", "artboard=${artboard.artboardHandle} sm=${stateMachine.stateMachineHandle} name=${artboard.name} smName=${stateMachine.name}")


🚫 [ktlint] standard:argument-list-wrapping reported by reviewdog 🐶
Argument should be on a separate line (unless all arguments can fit a single line)

Log.d("ComposeRiveTest", "artboard=${artboard.artboardHandle} sm=${stateMachine.stateMachineHandle} name=${artboard.name} smName=${stateMachine.name}")


🚫 [ktlint] standard:argument-list-wrapping reported by reviewdog 🐶
Argument should be on a separate line (unless all arguments can fit a single line)

Log.d("ComposeRiveTest", "artboard=${artboard.artboardHandle} sm=${stateMachine.stateMachineHandle} name=${artboard.name} smName=${stateMachine.name}")


🚫 [ktlint] standard:max-line-length reported by reviewdog 🐶
Exceeded max line length (140)

Log.d("ComposeRiveTest", "artboard=${artboard.artboardHandle} sm=${stateMachine.stateMachineHandle} name=${artboard.name} smName=${stateMachine.name}")


🚫 [ktlint] standard:argument-list-wrapping reported by reviewdog 🐶
Missing newline before ")"

Log.d("ComposeRiveTest", "artboard=${artboard.artboardHandle} sm=${stateMachine.stateMachineHandle} name=${artboard.name} smName=${stateMachine.name}")

@mfazekas mfazekas force-pushed the feat/rive-ios-experimental branch from 8134a07 to ec12673 Compare February 17, 2026 12:11
@mfazekas mfazekas changed the title feat: experimental iOS API support WIP feat: experimental iOS / Android POC Feb 17, 2026
@mfazekas mfazekas force-pushed the feat/rive-ios-experimental branch 2 times, most recently from 91e2fb6 to cfb2ff6 Compare February 19, 2026 13:50
@mfazekas mfazekas force-pushed the feat/rive-ios-experimental branch from cfb2ff6 to 44681b5 Compare February 26, 2026 13:57
@mfazekas mfazekas force-pushed the feat/rive-ios-experimental branch 2 times, most recently from f1b851e to aa2fdf0 Compare March 16, 2026 09:39
@mfazekas mfazekas force-pushed the feat/rive-ios-experimental branch 4 times, most recently from 35c6fea to 4f1ab3f Compare March 27, 2026 15:52
@mfazekas mfazekas changed the base branch from main to feat/hooks-undefined-initial-value March 27, 2026 15:52
@mfazekas mfazekas force-pushed the feat/rive-ios-experimental branch from 4e0c53a to be6d334 Compare March 30, 2026 12:39
@mfazekas mfazekas force-pushed the feat/hooks-undefined-initial-value branch from 164180e to 7116ac7 Compare March 30, 2026 18:17
Base automatically changed from feat/hooks-undefined-initial-value to main March 30, 2026 18:20
mfazekas and others added 5 commits March 31, 2026 07:28
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)
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)
… 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>
mfazekas added 22 commits March 31, 2026 07:29
Previously both the sync and async variants ignored `index` and always
called createDefaultInstance(). Fix implements createInstanceByIndexImpl
which calls getInstanceNames(of:) on the File and picks the name at the
requested position, then creates via .name(name, from:).

Also adds a post_install Podfile hook that strips the RiveRuntime.Swift
submodule from RiveRuntime's modulemaps. Without it the project cannot be
built with Xcode 26 / Swift 6.2 because Clang sees conflicting definitions
of swift::Optional / swift::String between the pre-built RiveRuntime
XCFramework (Swift 6.1 ABI) and the freshly-compiled NitroModules
(Swift 6.2 ABI).

Closes issue 1 (iOS) from .local/docs/issues.md.
…Android

Remove the async variant — callers should use createInstanceByNameAsync
instead. The deprecated sync createInstanceByIndex now correctly uses
getViewModelInstanceNames + index lookup on Android (was returning
default instance). Tests updated to use the sync API.
Were hardcoded to 0. Now use file.getProperties(of:) and
file.getInstanceNames(of:) respectively. Tests strengthened to
assert > 0 instead of >= 0.
Was accepting both replaced and original values. Now strictly asserts
the replacement took effect. Passes on iOS; Android experimental SDK
lacks the native bridge method (known limitation).
viewModelImpl was passing the property path as instanceName, which is
misleading. The SDK doesn't expose the actual instance name, so leave
it as "" (consistent with Android and Issue 11 limitation).
…DefaultForArtboard

The Rive Android SDK does not expose the ViewModel name from a ViewModelInstance,
making name-dependent operations (propertyCount, instanceCount, createInstanceByIndex,
createInstanceByName) impossible when using ViewModelSource.DefaultForArtboard.

Instead of guessing the name via string property value comparison, viewModelName is
now nullable (null for DefaultForArtboard), and name-dependent operations throw
UnsupportedOperationException with a clear message pointing to the upstream gap.
createDefaultInstance and createBlankInstance continue to work as they use vmSource
directly. The iOS SDK already exposes getDefaultViewModelInfo(for:) — a matching
API has been requested from the Rive Android team.
…ndroid

When auto-binding, cppDefaultVMCreateDefaultVMI returns handle 1L (null sentinel) if the artboard has no default ViewModel. Guard against passing this sentinel to bindViewModelInstance.
@mfazekas mfazekas force-pushed the feat/rive-ios-experimental branch from dcdde5e to 0a3d09c Compare March 31, 2026 05:33
@mfazekas mfazekas changed the title WIP feat: experimental iOS / Android POC feat: experimental Rive runtime backend (iOS + Android) Mar 31, 2026
@mfazekas mfazekas marked this pull request as ready for review March 31, 2026 07:31
@mfazekas mfazekas changed the title feat: experimental Rive runtime backend (iOS + Android) feat!: experimental Rive runtime backend (iOS + Android) Mar 31, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant