Skip to content

Commit 5807def

Browse files
committed
feat: addListener returns removal function for granular listener control
1 parent 1f325e7 commit 5807def

68 files changed

Lines changed: 449 additions & 211 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

android/src/main/java/com/margelo/nitro/rive/BaseHybridViewModelProperty.kt

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,14 @@ import kotlinx.coroutines.flow.Flow
99
@Keep
1010
@DoNotStrip
1111
interface BaseHybridViewModelProperty<T> {
12-
val scope: CoroutineScope?
13-
val job: Job?
14-
val listeners: MutableList<(T) -> Unit>
12+
val scope: CoroutineScope?
13+
val job: Job?
14+
val listeners: MutableMap<String, (T) -> Unit>
1515

16-
fun ensureValueListenerJob(valueFlow: Flow<T>, drop: Int = 0)
17-
fun onChanged(value: T)
18-
fun removeListeners()
19-
fun dispose()
16+
fun ensureValueListenerJob(valueFlow: Flow<T>, drop: Int = 0)
17+
fun onChanged(value: T)
18+
fun addListenerInternal(callback: (T) -> Unit): () -> Unit
19+
fun removeListener(id: String)
20+
fun removeListeners()
21+
fun dispose()
2022
}

android/src/main/java/com/margelo/nitro/rive/BaseHybridViewModelPropertyImpl.kt

Lines changed: 43 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -9,42 +9,61 @@ import kotlinx.coroutines.cancel
99
import kotlinx.coroutines.launch
1010
import kotlinx.coroutines.flow.Flow
1111
import kotlinx.coroutines.flow.drop
12+
import java.lang.ref.WeakReference
13+
import java.util.UUID
1214

1315
@Keep
1416
@DoNotStrip
1517
class BaseHybridViewModelPropertyImpl<T> : BaseHybridViewModelProperty<T> {
16-
override var scope: CoroutineScope? = null
17-
override var job: Job? = null
18-
override val listeners = mutableListOf<(T) -> Unit>()
18+
override var scope: CoroutineScope? = null
19+
override var job: Job? = null
20+
override val listeners = mutableMapOf<String, (T) -> Unit>()
1921

2022
override fun ensureValueListenerJob(valueFlow: Flow<T>, drop: Int) {
21-
if (scope == null) {
22-
scope = CoroutineScope(Dispatchers.Default)
23-
}
24-
if (job == null) {
25-
job = scope?.launch {
26-
valueFlow.drop(drop).collect { value ->
27-
onChanged(value)
28-
}
29-
}
23+
if (scope == null) {
24+
scope = CoroutineScope(Dispatchers.Default)
25+
}
26+
if (job == null) {
27+
job = scope?.launch {
28+
valueFlow.drop(drop).collect { value ->
29+
onChanged(value)
3030
}
31+
}
3132
}
33+
}
3234

33-
override fun onChanged(value: T) {
34-
listeners.forEach { listener ->
35-
listener(value)
36-
}
35+
override fun onChanged(value: T) {
36+
listeners.values.forEach { listener ->
37+
listener(value)
3738
}
39+
}
3840

39-
override fun removeListeners() {
40-
listeners.clear()
41-
job?.cancel()
42-
scope?.cancel()
43-
job = null
44-
scope = null
41+
override fun addListenerInternal(callback: (T) -> Unit): () -> Unit {
42+
val id = UUID.randomUUID().toString()
43+
listeners[id] = callback
44+
val weakSelf = WeakReference(this)
45+
return {
46+
weakSelf.get()?.removeListener(id)
4547
}
48+
}
4649

47-
override fun dispose() {
48-
removeListeners()
50+
override fun removeListener(id: String) {
51+
listeners.remove(id)
52+
if (listeners.isEmpty()) {
53+
job?.cancel()
54+
job = null
4955
}
56+
}
57+
58+
override fun removeListeners() {
59+
listeners.clear()
60+
job?.cancel()
61+
scope?.cancel()
62+
job = null
63+
scope = null
64+
}
65+
66+
override fun dispose() {
67+
removeListeners()
68+
}
5069
}

android/src/main/java/com/margelo/nitro/rive/HybridViewModelBooleanProperty.kt

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,9 @@ class HybridViewModelBooleanProperty(private val viewModelBoolean: ViewModelBool
1515
viewModelBoolean.value = value
1616
}
1717

18-
override fun addListener(onChanged: (value: Boolean) -> Unit) {
19-
listeners.add(onChanged)
18+
override fun addListener(onChanged: (value: Boolean) -> Unit): () -> Unit {
19+
val remover = addListenerInternal(onChanged)
2020
ensureValueListenerJob(viewModelBoolean.valueFlow)
21+
return remover
2122
}
2223
}

android/src/main/java/com/margelo/nitro/rive/HybridViewModelColorProperty.kt

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,9 @@ class HybridViewModelColorProperty(private val viewModelColor: ViewModelColorPro
1515
viewModelColor.value = value.toInt()
1616
}
1717

18-
override fun addListener(onChanged: (value: Double) -> Unit) {
19-
listeners.add { intValue: Int -> onChanged(intValue.toDouble()) }
18+
override fun addListener(onChanged: (value: Double) -> Unit): () -> Unit {
19+
val remover = addListenerInternal { intValue: Int -> onChanged(intValue.toDouble()) }
2020
ensureValueListenerJob(viewModelColor.valueFlow)
21+
return remover
2122
}
2223
}

android/src/main/java/com/margelo/nitro/rive/HybridViewModelEnumProperty.kt

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,9 @@ class HybridViewModelEnumProperty(private val viewModelEnum: ViewModelEnumProper
1515
viewModelEnum.value = value
1616
}
1717

18-
override fun addListener(onChanged: (value: String) -> Unit) {
19-
listeners.add(onChanged)
18+
override fun addListener(onChanged: (value: String) -> Unit): () -> Unit {
19+
val remover = addListenerInternal(onChanged)
2020
ensureValueListenerJob(viewModelEnum.valueFlow)
21+
return remover
2122
}
2223
}

android/src/main/java/com/margelo/nitro/rive/HybridViewModelImageProperty.kt

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,9 @@ class HybridViewModelImageProperty(private val viewModelImage: ViewModelImagePro
1414
viewModelImage.set((image as? HybridRiveImage)?.renderImage)
1515
}
1616

17-
override fun addListener(onChanged: () -> Unit) {
18-
listeners.add(onChanged)
17+
override fun addListener(onChanged: () -> Unit): () -> Unit {
18+
val remover = addListenerInternal { _ -> onChanged() }
1919
ensureValueListenerJob(viewModelImage.valueFlow.map { })
20+
return remover
2021
}
2122
}

android/src/main/java/com/margelo/nitro/rive/HybridViewModelNumberProperty.kt

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,9 @@ class HybridViewModelNumberProperty(private val viewModelNumber: ViewModelNumber
1616
viewModelNumber.value = value.toFloat()
1717
}
1818

19-
override fun addListener(onChanged: (value: Double) -> Unit) {
20-
listeners.add(onChanged)
19+
override fun addListener(onChanged: (value: Double) -> Unit): () -> Unit {
20+
val remover = addListenerInternal(onChanged)
2121
ensureValueListenerJob(viewModelNumber.valueFlow.map { it.toDouble() })
22+
return remover
2223
}
2324
}

android/src/main/java/com/margelo/nitro/rive/HybridViewModelStringProperty.kt

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,9 @@ class HybridViewModelStringProperty(private val viewModelString: ViewModelString
1515
viewModelString.value = value
1616
}
1717

18-
override fun addListener(onChanged: (value: String) -> Unit) {
19-
listeners.add(onChanged)
18+
override fun addListener(onChanged: (value: String) -> Unit): () -> Unit {
19+
val remover = addListenerInternal(onChanged)
2020
ensureValueListenerJob(viewModelString.valueFlow)
21+
return remover
2122
}
2223
}

android/src/main/java/com/margelo/nitro/rive/HybridViewModelTriggerProperty.kt

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,10 @@ class HybridViewModelTriggerProperty(private val viewModelTrigger: ViewModelTrig
1313
viewModelTrigger.trigger()
1414
}
1515

16-
override fun addListener(onChanged: () -> Unit) {
17-
listeners.add { _ -> onChanged() }
16+
override fun addListener(onChanged: () -> Unit): () -> Unit {
17+
val remover = addListenerInternal { _ -> onChanged() }
1818
// We drop the first value as a trigger has no initial value
1919
ensureValueListenerJob(viewModelTrigger.valueFlow, 1)
20+
return remover
2021
}
2122
}

example/src/pages/RiveDataBindingExample.tsx

Lines changed: 82 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
1-
import { View, Text, StyleSheet, ActivityIndicator } from 'react-native';
2-
import { useEffect, useMemo } from 'react';
1+
import {
2+
View,
3+
Text,
4+
StyleSheet,
5+
ActivityIndicator,
6+
Button,
7+
} from 'react-native';
8+
import { useEffect, useMemo, useState, useRef } from 'react';
39
import {
410
Fit,
511
RiveView,
@@ -92,15 +98,67 @@ function DataBindingExample({
9298
setBarColor('#0000FF');
9399
}, [setBarColor, setButtonText]);
94100

101+
// Direct addListener usage (without hooks)
102+
const [coinValue, setCoinValue] = useState<number | null>(null);
103+
const [isListening, setIsListening] = useState(true);
104+
const removeListenerRef = useRef<(() => void) | null>(null);
105+
106+
useEffect(() => {
107+
const coinProperty = instance.numberProperty('Coin/Item_Value');
108+
if (!coinProperty) return;
109+
110+
// Add listener and store the remover function
111+
removeListenerRef.current = coinProperty.addListener((value) => {
112+
console.log('Coin value changed:', value);
113+
setCoinValue(value);
114+
});
115+
116+
return () => {
117+
// Clean up on unmount
118+
removeListenerRef.current?.();
119+
};
120+
}, [instance]);
121+
122+
const toggleListener = () => {
123+
if (isListening && removeListenerRef.current) {
124+
// Remove the listener by calling the remover function
125+
removeListenerRef.current();
126+
removeListenerRef.current = null;
127+
setIsListening(false);
128+
} else if (!isListening) {
129+
// Re-add the listener
130+
const coinProperty = instance.numberProperty('Coin/Item_Value');
131+
if (coinProperty) {
132+
removeListenerRef.current = coinProperty.addListener((value) => {
133+
console.log('Coin value changed:', value);
134+
setCoinValue(value);
135+
});
136+
setIsListening(true);
137+
}
138+
}
139+
};
140+
95141
return (
96-
<RiveView
97-
style={styles.rive}
98-
autoPlay={true}
99-
dataBind={instance}
100-
fit={Fit.Layout}
101-
layoutScaleFactor={1}
102-
file={file}
103-
/>
142+
<View style={styles.flex}>
143+
<View style={styles.listenerDemo}>
144+
<Text style={styles.listenerText}>
145+
Coin Value: {coinValue ?? 'N/A'}{' '}
146+
{isListening ? '(listening)' : '(paused)'}
147+
</Text>
148+
<Button
149+
title={isListening ? 'Remove Listener' : 'Add Listener'}
150+
onPress={toggleListener}
151+
/>
152+
</View>
153+
<RiveView
154+
style={styles.rive}
155+
autoPlay={true}
156+
dataBind={instance}
157+
fit={Fit.Layout}
158+
layoutScaleFactor={1}
159+
file={file}
160+
/>
161+
</View>
104162
);
105163
}
106164

@@ -119,6 +177,9 @@ const styles = StyleSheet.create({
119177
flex: 1,
120178
backgroundColor: '#f5f5f5',
121179
},
180+
flex: {
181+
flex: 1,
182+
},
122183
rive: {
123184
flex: 1,
124185
width: '100%',
@@ -129,4 +190,15 @@ const styles = StyleSheet.create({
129190
textAlign: 'center',
130191
padding: 20,
131192
},
193+
listenerDemo: {
194+
padding: 16,
195+
backgroundColor: '#e8e8e8',
196+
flexDirection: 'row',
197+
alignItems: 'center',
198+
justifyContent: 'space-between',
199+
},
200+
listenerText: {
201+
fontSize: 16,
202+
fontWeight: '500',
203+
},
132204
});

0 commit comments

Comments
 (0)