From 71317baa210139e22b23ff704bfc7827b749ce72 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mikl=C3=B3s=20Fazekas?= Date: Mon, 8 Dec 2025 17:24:18 +0100 Subject: [PATCH 1/3] feat: addListener returns removal function for granular listener control --- .../nitro/rive/BaseHybridViewModelProperty.kt | 16 ++-- .../rive/BaseHybridViewModelPropertyImpl.kt | 67 +++++++++----- .../com/margelo/nitro/rive/HybridRiveFile.kt | 2 +- .../rive/HybridViewModelBooleanProperty.kt | 5 +- .../rive/HybridViewModelColorProperty.kt | 5 +- .../nitro/rive/HybridViewModelEnumProperty.kt | 5 +- .../rive/HybridViewModelImageProperty.kt | 5 +- .../rive/HybridViewModelNumberProperty.kt | 5 +- .../rive/HybridViewModelStringProperty.kt | 5 +- .../rive/HybridViewModelTriggerProperty.kt | 5 +- example/src/pages/RiveDataBindingExample.tsx | 92 +++++++++++++++++-- ios/BaseHybridViewModelProperty.swift | 19 ++-- ios/HybridViewModelColorProperty.swift | 5 +- ios/HybridViewModelImageProperty.swift | 8 +- ios/HybridViewModelNumberProperty.swift | 7 +- ios/HybridViewModelTriggerProperty.swift | 19 +--- nitro.json | 4 - .../JHybridViewModelBooleanPropertySpec.cpp | 18 +++- .../JHybridViewModelBooleanPropertySpec.hpp | 2 +- .../c++/JHybridViewModelColorPropertySpec.cpp | 18 +++- .../c++/JHybridViewModelColorPropertySpec.hpp | 2 +- .../c++/JHybridViewModelEnumPropertySpec.cpp | 18 +++- .../c++/JHybridViewModelEnumPropertySpec.hpp | 2 +- .../c++/JHybridViewModelImagePropertySpec.cpp | 20 +++- .../c++/JHybridViewModelImagePropertySpec.hpp | 2 +- .../JHybridViewModelNumberPropertySpec.cpp | 18 +++- .../JHybridViewModelNumberPropertySpec.hpp | 2 +- .../JHybridViewModelStringPropertySpec.cpp | 18 +++- .../JHybridViewModelStringPropertySpec.hpp | 2 +- .../JHybridViewModelTriggerPropertySpec.cpp | 15 ++- .../JHybridViewModelTriggerPropertySpec.hpp | 2 +- .../HybridViewModelBooleanPropertySpec.kt | 6 +- .../rive/HybridViewModelColorPropertySpec.kt | 6 +- .../rive/HybridViewModelEnumPropertySpec.kt | 6 +- .../rive/HybridViewModelImagePropertySpec.kt | 6 +- .../rive/HybridViewModelNumberPropertySpec.kt | 6 +- .../rive/HybridViewModelStringPropertySpec.kt | 6 +- .../HybridViewModelTriggerPropertySpec.kt | 6 +- nitrogen/generated/android/riveOnLoad.cpp | 12 +-- .../generated/ios/RNRive-Swift-Cxx-Bridge.hpp | 9 ++ nitrogen/generated/ios/RNRiveAutolinking.mm | 8 -- .../generated/ios/RNRiveAutolinking.swift | 15 --- ...ybridViewModelBooleanPropertySpecSwift.hpp | 4 +- .../HybridViewModelColorPropertySpecSwift.hpp | 4 +- .../HybridViewModelEnumPropertySpecSwift.hpp | 4 +- .../HybridViewModelImagePropertySpecSwift.hpp | 4 +- ...HybridViewModelNumberPropertySpecSwift.hpp | 4 +- ...HybridViewModelStringPropertySpecSwift.hpp | 4 +- ...ybridViewModelTriggerPropertySpecSwift.hpp | 4 +- .../HybridViewModelBooleanPropertySpec.swift | 2 +- ...bridViewModelBooleanPropertySpec_cxx.swift | 12 ++- .../HybridViewModelColorPropertySpec.swift | 2 +- ...HybridViewModelColorPropertySpec_cxx.swift | 12 ++- .../HybridViewModelEnumPropertySpec.swift | 2 +- .../HybridViewModelEnumPropertySpec_cxx.swift | 12 ++- .../HybridViewModelImagePropertySpec.swift | 2 +- ...HybridViewModelImagePropertySpec_cxx.swift | 12 ++- .../HybridViewModelNumberPropertySpec.swift | 2 +- ...ybridViewModelNumberPropertySpec_cxx.swift | 12 ++- .../HybridViewModelStringPropertySpec.swift | 2 +- ...ybridViewModelStringPropertySpec_cxx.swift | 12 ++- .../HybridViewModelTriggerPropertySpec.swift | 2 +- ...bridViewModelTriggerPropertySpec_cxx.swift | 12 ++- .../HybridViewModelBooleanPropertySpec.hpp | 2 +- .../c++/HybridViewModelColorPropertySpec.hpp | 2 +- .../c++/HybridViewModelEnumPropertySpec.hpp | 2 +- .../c++/HybridViewModelImagePropertySpec.hpp | 2 +- .../c++/HybridViewModelNumberPropertySpec.hpp | 2 +- .../c++/HybridViewModelStringPropertySpec.hpp | 2 +- .../HybridViewModelTriggerPropertySpec.hpp | 2 +- src/hooks/useRiveProperty.ts | 2 +- src/specs/ViewModel.nitro.ts | 30 +++--- 72 files changed, 425 insertions(+), 246 deletions(-) diff --git a/android/src/main/java/com/margelo/nitro/rive/BaseHybridViewModelProperty.kt b/android/src/main/java/com/margelo/nitro/rive/BaseHybridViewModelProperty.kt index 1cadc666..1d8f4663 100644 --- a/android/src/main/java/com/margelo/nitro/rive/BaseHybridViewModelProperty.kt +++ b/android/src/main/java/com/margelo/nitro/rive/BaseHybridViewModelProperty.kt @@ -9,12 +9,14 @@ import kotlinx.coroutines.flow.Flow @Keep @DoNotStrip interface BaseHybridViewModelProperty { - val scope: CoroutineScope? - val job: Job? - val listeners: MutableList<(T) -> Unit> + val scope: CoroutineScope? + val job: Job? + val listeners: MutableMap Unit> - fun ensureValueListenerJob(valueFlow: Flow, drop: Int = 0) - fun onChanged(value: T) - fun removeListeners() - fun dispose() + fun ensureValueListenerJob(valueFlow: Flow, drop: Int = 0) + fun onChanged(value: T) + fun addListenerInternal(callback: (T) -> Unit): () -> Unit + fun removeListener(id: String) + fun removeListeners() + fun dispose() } diff --git a/android/src/main/java/com/margelo/nitro/rive/BaseHybridViewModelPropertyImpl.kt b/android/src/main/java/com/margelo/nitro/rive/BaseHybridViewModelPropertyImpl.kt index 6ad3a968..39dbf76a 100644 --- a/android/src/main/java/com/margelo/nitro/rive/BaseHybridViewModelPropertyImpl.kt +++ b/android/src/main/java/com/margelo/nitro/rive/BaseHybridViewModelPropertyImpl.kt @@ -9,42 +9,61 @@ import kotlinx.coroutines.cancel import kotlinx.coroutines.launch import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.drop +import java.lang.ref.WeakReference +import java.util.UUID @Keep @DoNotStrip class BaseHybridViewModelPropertyImpl : BaseHybridViewModelProperty { - override var scope: CoroutineScope? = null - override var job: Job? = null - override val listeners = mutableListOf<(T) -> Unit>() + override var scope: CoroutineScope? = null + override var job: Job? = null + override val listeners = mutableMapOf Unit>() override fun ensureValueListenerJob(valueFlow: Flow, drop: Int) { - if (scope == null) { - scope = CoroutineScope(Dispatchers.Default) - } - if (job == null) { - job = scope?.launch { - valueFlow.drop(drop).collect { value -> - onChanged(value) - } - } + if (scope == null) { + scope = CoroutineScope(Dispatchers.Default) + } + if (job == null) { + job = scope?.launch { + valueFlow.drop(drop).collect { value -> + onChanged(value) } + } } + } - override fun onChanged(value: T) { - listeners.forEach { listener -> - listener(value) - } + override fun onChanged(value: T) { + listeners.values.forEach { listener -> + listener(value) } + } - override fun removeListeners() { - listeners.clear() - job?.cancel() - scope?.cancel() - job = null - scope = null + override fun addListenerInternal(callback: (T) -> Unit): () -> Unit { + val id = UUID.randomUUID().toString() + listeners[id] = callback + val weakSelf = WeakReference(this) + return { + weakSelf.get()?.removeListener(id) } + } - override fun dispose() { - removeListeners() + override fun removeListener(id: String) { + listeners.remove(id) + if (listeners.isEmpty()) { + job?.cancel() + job = null } + } + + override fun removeListeners() { + listeners.clear() + job?.cancel() + scope?.cancel() + job = null + scope = null + } + + override fun dispose() { + removeListeners() + } } diff --git a/android/src/main/java/com/margelo/nitro/rive/HybridRiveFile.kt b/android/src/main/java/com/margelo/nitro/rive/HybridRiveFile.kt index a4343bda..dec767de 100644 --- a/android/src/main/java/com/margelo/nitro/rive/HybridRiveFile.kt +++ b/android/src/main/java/com/margelo/nitro/rive/HybridRiveFile.kt @@ -75,7 +75,7 @@ class HybridRiveFile : HybridRiveFileSpec() { for ((key, assetData) in assetsData) { val asset = cache[key] ?: continue - loadJobs.add(loader.updateAsset(assetData, asset, context)) + loadJobs.add(loader.updateAsset(assetData, asset)) } if (loadJobs.isNotEmpty()) { diff --git a/android/src/main/java/com/margelo/nitro/rive/HybridViewModelBooleanProperty.kt b/android/src/main/java/com/margelo/nitro/rive/HybridViewModelBooleanProperty.kt index 85b43cd1..5a82de36 100644 --- a/android/src/main/java/com/margelo/nitro/rive/HybridViewModelBooleanProperty.kt +++ b/android/src/main/java/com/margelo/nitro/rive/HybridViewModelBooleanProperty.kt @@ -15,8 +15,9 @@ class HybridViewModelBooleanProperty(private val viewModelBoolean: ViewModelBool viewModelBoolean.value = value } - override fun addListener(onChanged: (value: Boolean) -> Unit) { - listeners.add(onChanged) + override fun addListener(onChanged: (value: Boolean) -> Unit): () -> Unit { + val remover = addListenerInternal(onChanged) ensureValueListenerJob(viewModelBoolean.valueFlow) + return remover } } diff --git a/android/src/main/java/com/margelo/nitro/rive/HybridViewModelColorProperty.kt b/android/src/main/java/com/margelo/nitro/rive/HybridViewModelColorProperty.kt index 73e4cb94..9bd765f7 100644 --- a/android/src/main/java/com/margelo/nitro/rive/HybridViewModelColorProperty.kt +++ b/android/src/main/java/com/margelo/nitro/rive/HybridViewModelColorProperty.kt @@ -15,8 +15,9 @@ class HybridViewModelColorProperty(private val viewModelColor: ViewModelColorPro viewModelColor.value = value.toInt() } - override fun addListener(onChanged: (value: Double) -> Unit) { - listeners.add { intValue: Int -> onChanged(intValue.toDouble()) } + override fun addListener(onChanged: (value: Double) -> Unit): () -> Unit { + val remover = addListenerInternal { intValue: Int -> onChanged(intValue.toDouble()) } ensureValueListenerJob(viewModelColor.valueFlow) + return remover } } diff --git a/android/src/main/java/com/margelo/nitro/rive/HybridViewModelEnumProperty.kt b/android/src/main/java/com/margelo/nitro/rive/HybridViewModelEnumProperty.kt index ae536e5c..ec7554bc 100644 --- a/android/src/main/java/com/margelo/nitro/rive/HybridViewModelEnumProperty.kt +++ b/android/src/main/java/com/margelo/nitro/rive/HybridViewModelEnumProperty.kt @@ -15,8 +15,9 @@ class HybridViewModelEnumProperty(private val viewModelEnum: ViewModelEnumProper viewModelEnum.value = value } - override fun addListener(onChanged: (value: String) -> Unit) { - listeners.add(onChanged) + override fun addListener(onChanged: (value: String) -> Unit): () -> Unit { + val remover = addListenerInternal(onChanged) ensureValueListenerJob(viewModelEnum.valueFlow) + return remover } } diff --git a/android/src/main/java/com/margelo/nitro/rive/HybridViewModelImageProperty.kt b/android/src/main/java/com/margelo/nitro/rive/HybridViewModelImageProperty.kt index 708ef977..3504debb 100644 --- a/android/src/main/java/com/margelo/nitro/rive/HybridViewModelImageProperty.kt +++ b/android/src/main/java/com/margelo/nitro/rive/HybridViewModelImageProperty.kt @@ -14,8 +14,9 @@ class HybridViewModelImageProperty(private val viewModelImage: ViewModelImagePro viewModelImage.set((image as? HybridRiveImage)?.renderImage) } - override fun addListener(onChanged: () -> Unit) { - listeners.add(onChanged) + override fun addListener(onChanged: () -> Unit): () -> Unit { + val remover = addListenerInternal { _ -> onChanged() } ensureValueListenerJob(viewModelImage.valueFlow.map { }) + return remover } } diff --git a/android/src/main/java/com/margelo/nitro/rive/HybridViewModelNumberProperty.kt b/android/src/main/java/com/margelo/nitro/rive/HybridViewModelNumberProperty.kt index a6753cf4..fc647ec1 100644 --- a/android/src/main/java/com/margelo/nitro/rive/HybridViewModelNumberProperty.kt +++ b/android/src/main/java/com/margelo/nitro/rive/HybridViewModelNumberProperty.kt @@ -16,8 +16,9 @@ class HybridViewModelNumberProperty(private val viewModelNumber: ViewModelNumber viewModelNumber.value = value.toFloat() } - override fun addListener(onChanged: (value: Double) -> Unit) { - listeners.add(onChanged) + override fun addListener(onChanged: (value: Double) -> Unit): () -> Unit { + val remover = addListenerInternal(onChanged) ensureValueListenerJob(viewModelNumber.valueFlow.map { it.toDouble() }) + return remover } } diff --git a/android/src/main/java/com/margelo/nitro/rive/HybridViewModelStringProperty.kt b/android/src/main/java/com/margelo/nitro/rive/HybridViewModelStringProperty.kt index bdf783ca..02c69e8c 100644 --- a/android/src/main/java/com/margelo/nitro/rive/HybridViewModelStringProperty.kt +++ b/android/src/main/java/com/margelo/nitro/rive/HybridViewModelStringProperty.kt @@ -15,8 +15,9 @@ class HybridViewModelStringProperty(private val viewModelString: ViewModelString viewModelString.value = value } - override fun addListener(onChanged: (value: String) -> Unit) { - listeners.add(onChanged) + override fun addListener(onChanged: (value: String) -> Unit): () -> Unit { + val remover = addListenerInternal(onChanged) ensureValueListenerJob(viewModelString.valueFlow) + return remover } } diff --git a/android/src/main/java/com/margelo/nitro/rive/HybridViewModelTriggerProperty.kt b/android/src/main/java/com/margelo/nitro/rive/HybridViewModelTriggerProperty.kt index 9e95a652..b50dce55 100644 --- a/android/src/main/java/com/margelo/nitro/rive/HybridViewModelTriggerProperty.kt +++ b/android/src/main/java/com/margelo/nitro/rive/HybridViewModelTriggerProperty.kt @@ -13,9 +13,10 @@ class HybridViewModelTriggerProperty(private val viewModelTrigger: ViewModelTrig viewModelTrigger.trigger() } - override fun addListener(onChanged: () -> Unit) { - listeners.add { _ -> onChanged() } + override fun addListener(onChanged: () -> Unit): () -> Unit { + val remover = addListenerInternal { _ -> onChanged() } // We drop the first value as a trigger has no initial value ensureValueListenerJob(viewModelTrigger.valueFlow, 1) + return remover } } diff --git a/example/src/pages/RiveDataBindingExample.tsx b/example/src/pages/RiveDataBindingExample.tsx index 2526ac00..c753c4f6 100644 --- a/example/src/pages/RiveDataBindingExample.tsx +++ b/example/src/pages/RiveDataBindingExample.tsx @@ -1,5 +1,11 @@ -import { View, Text, StyleSheet, ActivityIndicator } from 'react-native'; -import { useEffect, useMemo } from 'react'; +import { + View, + Text, + StyleSheet, + ActivityIndicator, + Button, +} from 'react-native'; +import { useEffect, useMemo, useState, useRef } from 'react'; import { Fit, RiveView, @@ -92,15 +98,67 @@ function DataBindingExample({ setBarColor('#0000FF'); }, [setBarColor, setButtonText]); + // Direct addListener usage (without hooks) + const [coinValue, setCoinValue] = useState(null); + const [isListening, setIsListening] = useState(true); + const removeListenerRef = useRef<(() => void) | null>(null); + + useEffect(() => { + const coinProperty = instance.numberProperty('Coin/Item_Value'); + if (!coinProperty) return; + + // Add listener and store the remover function + removeListenerRef.current = coinProperty.addListener((value) => { + console.log('Coin value changed:', value); + setCoinValue(value); + }); + + return () => { + // Clean up on unmount + removeListenerRef.current?.(); + }; + }, [instance]); + + const toggleListener = () => { + if (isListening && removeListenerRef.current) { + // Remove the listener by calling the remover function + removeListenerRef.current(); + removeListenerRef.current = null; + setIsListening(false); + } else if (!isListening) { + // Re-add the listener + const coinProperty = instance.numberProperty('Coin/Item_Value'); + if (coinProperty) { + removeListenerRef.current = coinProperty.addListener((value) => { + console.log('Coin value changed:', value); + setCoinValue(value); + }); + setIsListening(true); + } + } + }; + return ( - + + + + Coin Value: {coinValue ?? 'N/A'}{' '} + {isListening ? '(listening)' : '(paused)'} + +