Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,14 @@ import kotlinx.coroutines.flow.Flow
@Keep
@DoNotStrip
interface BaseHybridViewModelProperty<T> {
val scope: CoroutineScope?
val job: Job?
val listeners: MutableList<(T) -> Unit>
val scope: CoroutineScope?
val job: Job?
val listeners: MutableMap<String, (T) -> Unit>

fun ensureValueListenerJob(valueFlow: Flow<T>, drop: Int = 0)
fun onChanged(value: T)
fun removeListeners()
fun dispose()
fun ensureValueListenerJob(valueFlow: Flow<T>, drop: Int = 0)
fun onChanged(value: T)
fun addListenerInternal(callback: (T) -> Unit): () -> Unit
fun removeListener(id: String)
fun removeListeners()
fun dispose()
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<T> : BaseHybridViewModelProperty<T> {
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<String, (T) -> Unit>()

override fun ensureValueListenerJob(valueFlow: Flow<T>, 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()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
92 changes: 82 additions & 10 deletions example/src/pages/RiveDataBindingExample.tsx
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -92,15 +98,67 @@ function DataBindingExample({
setBarColor('#0000FF');
}, [setBarColor, setButtonText]);

// Direct addListener usage (without hooks)
const [coinValue, setCoinValue] = useState<number | null>(null);
Comment thread
mfazekas marked this conversation as resolved.
Outdated
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 (
<RiveView
style={styles.rive}
autoPlay={true}
dataBind={instance}
fit={Fit.Layout}
layoutScaleFactor={1}
file={file}
/>
<View style={styles.flex}>
<View style={styles.listenerDemo}>
<Text style={styles.listenerText}>
Coin Value: {coinValue ?? 'N/A'}{' '}
{isListening ? '(listening)' : '(paused)'}
</Text>
<Button
title={isListening ? 'Remove Listener' : 'Add Listener'}
onPress={toggleListener}
/>
</View>
<RiveView
style={styles.rive}
autoPlay={true}
dataBind={instance}
fit={Fit.Layout}
layoutScaleFactor={1}
file={file}
/>
</View>
);
}

Expand All @@ -119,6 +177,9 @@ const styles = StyleSheet.create({
flex: 1,
backgroundColor: '#f5f5f5',
},
flex: {
flex: 1,
},
rive: {
flex: 1,
width: '100%',
Expand All @@ -129,4 +190,15 @@ const styles = StyleSheet.create({
textAlign: 'center',
padding: 20,
},
listenerDemo: {
padding: 16,
backgroundColor: '#e8e8e8',
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
},
listenerText: {
fontSize: 16,
fontWeight: '500',
},
});
19 changes: 13 additions & 6 deletions ios/BaseHybridViewModelProperty.swift
Original file line number Diff line number Diff line change
Expand Up @@ -65,11 +65,18 @@ class PropertyListenerHelper<PropertyType: RivePropertyWithListeners> {
self.property = property
}

/// Adds a listener to the property and automatically tracks its ID for cleanup
func addListener(_ callback: @escaping (PropertyType.ListenerValueType) -> Void) {
guard let property = property else { return }
/// Adds a listener to the property and returns a removal function for cleanup
func addListener(_ callback: @escaping (PropertyType.ListenerValueType) -> Void) -> () -> Void {
guard let property = property else {
return {}
}
let id = property.addListener(callback)
listenerIds.append(id)
return { [weak self, weak property] in
guard let property = property else { return }
property.removeListener(id)
self?.listenerIds.removeAll { $0 == id }
}
}

func removeListeners() throws {
Expand All @@ -94,7 +101,7 @@ protocol ValuedPropertyProtocol<ValueType> {
var property: PropertyType! { get }
var helper: PropertyListenerHelper<PropertyType> { get }

func addListener(onChanged: @escaping (ValueType) -> Void) throws
func addListener(onChanged: @escaping (ValueType) -> Void) throws -> () -> Void
func removeListeners() throws
func dispose() throws
}
Expand All @@ -112,7 +119,7 @@ extension ValuedPropertyProtocol {

/// Automatic addListener() ONLY when ListenerValueType == ValueType (no conversion needed)
extension ValuedPropertyProtocol where PropertyType.ListenerValueType == ValueType {
func addListener(onChanged: @escaping (ValueType) -> Void) throws {
helper.addListener(onChanged) // Types match, just forward directly!
func addListener(onChanged: @escaping (ValueType) -> Void) throws -> () -> Void {
return helper.addListener(onChanged)
}
}
5 changes: 2 additions & 3 deletions ios/HybridViewModelColorProperty.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,8 @@ class HybridViewModelColorProperty: HybridViewModelColorPropertySpec, ValuedProp
}
}

// Custom addListener because we need to convert UIColor → Double
func addListener(onChanged: @escaping (Double) -> Void) throws {
helper.addListener { (color: UIColor) in
func addListener(onChanged: @escaping (Double) -> Void) throws -> () -> Void {
return helper.addListener { (color: UIColor) in
onChanged(color.toHexDouble())
}
}
Expand Down
8 changes: 4 additions & 4 deletions ios/HybridViewModelImageProperty.swift
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import RiveRuntime

class HybridViewModelImageProperty: HybridViewModelImagePropertySpec, ValuedPropertyProtocol {
func addListener(onChanged: @escaping () -> Void) throws {
try addListener(onChanged: { _ in onChanged() })
}

var property: ImagePropertyType!
lazy var helper = PropertyListenerHelper(property: property!)

func addListener(onChanged: @escaping () -> Void) throws -> () -> Void {
return helper.addListener { _ in onChanged() }
}

init(property: ImagePropertyType) {
self.property = property
super.init()
Expand Down
7 changes: 2 additions & 5 deletions ios/HybridViewModelNumberProperty.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,7 @@ class HybridViewModelNumberProperty: HybridViewModelNumberPropertySpec, ValuedPr
}
}

// Custom addListener needed because ListenerValueType (Float) != ValueType (Double)
func addListener(onChanged: @escaping (Double) -> Void) throws {
helper.addListener { (value: Float) in
onChanged(Double(value))
}
func addListener(onChanged: @escaping (Double) -> Void) throws -> () -> Void {
return helper.addListener({ floatValue in onChanged(Double(floatValue)) })
}
}
Loading
Loading