Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
1397728
fix(ios): make play/pause control the experimental render loop
mfazekas Jun 9, 2026
3c10b9f
fix(android): make play/pause control the experimental render loop
mfazekas Jun 10, 2026
b40058e
deprecate reset() on experimental backend
mfazekas Jun 10, 2026
ab35d66
trim comments and remove redundant jsdoc
mfazekas Jun 10, 2026
05c4e71
fix(android): byName dataBind uses artboard default ViewModel, not vm…
mfazekas Jun 10, 2026
416374d
fix lint
mfazekas Jun 10, 2026
32fee85
fix(android): snapshot listeners before iterating to avoid Concurrent…
mfazekas Jun 10, 2026
33fa8ca
fix(android): use ConcurrentHashMap for listeners to prevent CME
mfazekas Jun 10, 2026
b224687
Revert "fix(android): use ConcurrentHashMap for listeners to prevent …
mfazekas Jun 10, 2026
67e0294
Revert "fix(android): snapshot listeners before iterating to avoid Co…
mfazekas Jun 10, 2026
af7ed83
ci: add Metro health check to debug steps
mfazekas Jun 10, 2026
f381a1d
fix(android): @Volatile on paused flag for cross-thread visibility
mfazekas Jun 10, 2026
e721645
Revert "fix(android): @Volatile on paused flag for cross-thread visib…
mfazekas Jun 10, 2026
be7718a
fix(android): @Volatile on paused for cross-thread visibility
mfazekas Jun 10, 2026
f0f4143
ci: pre-install NDK with retry to avoid corrupt archive download
mfazekas Jun 10, 2026
c8c2817
chore: remove tracked Kotlin build artifact, add .kotlin/ to .gitignore
mfazekas Jun 10, 2026
f8aa8ef
fix(test): remove stateMachineName from byName test to prevent awaitV…
mfazekas Jun 10, 2026
ca36721
fix(test): guard byName test to experimental Android only to prevent …
mfazekas Jun 10, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,11 @@ jobs:
if: env.turbo_cache_hit != 1
run: |
/bin/bash -c "yes | $ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager --licenses > /dev/null"
for i in 1 2 3; do
$ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager "ndk;27.1.12297006" && break
echo "NDK install attempt $i failed, retrying..."
sleep 10
done

- name: Cache Gradle
if: env.turbo_cache_hit != 1
Expand Down Expand Up @@ -387,6 +392,10 @@ jobs:
- name: Debug - Check for console logs
if: failure() || cancelled()
run: |
echo "=== Metro health ==="
curl -s --max-time 5 http://localhost:8081/status || echo "Metro not responding"
echo "=== Metro process ==="
lsof -i:8081 | head -5 || echo "Nothing on port 8081"
echo "=== Simulator logs (last 5m) ==="
xcrun simctl spawn booted log show --predicate 'processImagePath CONTAINS "RiveExample"' --last 5m --style compact 2>&1 | tail -200 || echo "No logs found"
echo "=== System log for Metro/Node ==="
Expand All @@ -413,6 +422,11 @@ jobs:
- name: Finalize Android SDK
run: |
/bin/bash -c "yes | $ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager --licenses > /dev/null"
for i in 1 2 3; do
$ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager "ndk;27.1.12297006" && break
echo "NDK install attempt $i failed, retrying..."
sleep 10
done

- name: Cache Gradle
uses: actions/cache@v4
Expand Down Expand Up @@ -582,6 +596,10 @@ jobs:
- name: Debug - Check for console logs
if: failure()
run: |
echo "=== Metro health ==="
curl -s --max-time 5 http://localhost:8081/status || echo "Metro not responding"
echo "=== Metro process ==="
lsof -i:8081 | head -5 || echo "Nothing on port 8081"
echo "=== Checking simulator logs for errors ==="
xcrun simctl spawn booted log show --predicate 'processImagePath CONTAINS "RiveExample"' --last 5m --style compact 2>&1 | tail -200 || echo "No logs found"

Expand All @@ -606,6 +624,11 @@ jobs:
- name: Finalize Android SDK
run: |
/bin/bash -c "yes | $ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager --licenses > /dev/null"
for i in 1 2 3; do
$ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager "ndk;27.1.12297006" && break
echo "NDK install attempt $i failed, retrying..."
sleep 10
done

- name: Enable legacy Rive backend
run: |
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -93,3 +93,6 @@ example/src/reproducers/local/*

# Test coverage
coverage/

# Kotlin build artifacts
**/.kotlin/
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,13 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.drop
import java.lang.ref.WeakReference
import java.util.UUID
import java.util.concurrent.ConcurrentHashMap

@Keep
@DoNotStrip
class BaseHybridViewModelPropertyImpl<T> : BaseHybridViewModelProperty<T> {
override var scope: CoroutineScope? = null
override var job: Job? = null
override val listeners = ConcurrentHashMap<String, (T) -> Unit>()
override val listeners = mutableMapOf<String, (T) -> Unit>()

override fun ensureValueListenerJob(valueFlow: Flow<T>, drop: Int) {
if (scope == null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,7 @@ class HybridViewModelTriggerProperty(

override fun addListener(onChanged: () -> Unit): () -> Unit {
val remover = addListenerInternal { _ -> onChanged() }
// Triggers use drop=0: getTriggerFlow has replay=0 and emits nothing on
// subscription, so there is no initial value to skip.
// drop=0: getTriggerFlow (replay=0) emits nothing on subscription, unlike number/boolean flows.
ensureValueListenerJob(instance.getTriggerFlow(path), 0)
return remover
}
Expand Down
46 changes: 28 additions & 18 deletions android/src/new/java/com/rive/RiveReactNativeView.kt
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import app.rive.core.RiveSurface
import app.rive.core.StateMachineHandle
import com.facebook.react.uimanager.ThemedReactContext
import com.margelo.nitro.rive.RiveErrorLogger
import com.margelo.nitro.rive.RiveLog
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
Expand Down Expand Up @@ -77,6 +78,9 @@ class RiveReactNativeView(context: ThemedReactContext) : FrameLayout(context) {
private var lastFrameTimeNs = 0L
private var frameCount = 0L

@Volatile
private var paused = false

private val textureView = TextureView(context).apply {
layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
surfaceTextureListener = object : TextureView.SurfaceTextureListener {
Expand Down Expand Up @@ -129,13 +133,13 @@ class RiveReactNativeView(context: ThemedReactContext) : FrameLayout(context) {

if (worker != null && art != null && sm != null && rs != null) {
try {
worker.advanceStateMachine(sm, deltaTime)
if (!paused) {
worker.advanceStateMachine(sm, deltaTime)
}
worker.draw(art, sm, rs, activeFit)
frameCount++
// Signal readiness only once the state machine is actually running
// (surface created + first frame drawn), so callers awaiting
// awaitViewReady() before firing triggers don't fire too early.
if (frameCount == 1L) {
val isFirstFrame = frameCount == 1L
if (isFirstFrame) {
viewReadyDeferred.complete(true)
}
} catch (e: Exception) {
Expand Down Expand Up @@ -200,6 +204,7 @@ class RiveReactNativeView(context: ThemedReactContext) : FrameLayout(context) {

Log.d(TAG, "configure: artboard=${artboardHandle != null} sm=${stateMachineHandle != null} surface=${riveSurface != null}")

paused = !config.autoPlay
startRenderLoop()
}

Expand All @@ -208,7 +213,6 @@ class RiveReactNativeView(context: ThemedReactContext) : FrameLayout(context) {
if (dataBindingChanged || initialUpdate) {
applyDataBinding(config.bindData, config.riveFile)
}

}

private fun resizeArtboardIfLayout() {
Expand Down Expand Up @@ -312,14 +316,11 @@ class RiveReactNativeView(context: ThemedReactContext) : FrameLayout(context) {
}
is BindData.ByName -> {
try {
val vmNames = kotlinx.coroutines.runBlocking { riveFile.getViewModelNames() }
if (vmNames.isNotEmpty()) {
val vmSource = ViewModelSource.Named(vmNames.first())
val source = vmSource.namedInstance(bindData.name)
val instance = ViewModelInstance.fromFile(riveFile, source)
boundInstance = instance
bindInstanceToStateMachine(instance)
}
val art = artboard ?: return
val source = ViewModelSource.DefaultForArtboard(art).namedInstance(bindData.name)
val instance = ViewModelInstance.fromFile(riveFile, source)
boundInstance = instance
bindInstanceToStateMachine(instance)
} catch (e: Exception) {
Log.e(TAG, "Failed to create named instance", e)
}
Expand All @@ -337,13 +338,22 @@ class RiveReactNativeView(context: ThemedReactContext) : FrameLayout(context) {
}
}

fun play() { /* controlled by render loop */ }
fun play() {
paused = false
}

fun pause() { /* controlled by render loop */ }
fun pause() {
paused = true
}

fun reset() { /* controlled by render loop */ }
// Deprecated: the experimental Rive runtime has no reset primitive.
fun reset() {
RiveLog.e(TAG, "reset() is deprecated and not supported on the experimental backend")
}

fun playIfNeeded() { /* controlled by render loop */ }
fun playIfNeeded() {
paused = false
}

fun setNumberInputValue(name: String, value: Double, path: String?) {
throw UnsupportedOperationException("SMI inputs not supported in experimental API")
Expand Down
Loading
Loading