Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
}
}
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)) })
}
}
19 changes: 5 additions & 14 deletions ios/HybridViewModelTriggerProperty.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,27 +4,18 @@ import RiveRuntime
class HybridViewModelTriggerProperty: HybridViewModelTriggerPropertySpec, ValuedPropertyProtocol {
internal var property: TriggerPropertyType!
lazy var helper = PropertyListenerHelper(property: property!)

private var listenerIds: [UUID] = []

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

init(property: TriggerPropertyType) {
self.property = property
super.init()
}

/// ⚠️ DO NOT REMOVE
/// Nitro requires a parameterless initializer for JS bridging.
/// This is invoked automatically during hybrid module construction.
/// Internally we always use `init(property:)`
override init() {
super.init()
}


func trigger() {
property.trigger()
}

func addListener(onChanged: @escaping () -> Void) throws -> () -> Void {
try addListener(onChanged: { _ in onChanged() })
}
}
4 changes: 0 additions & 4 deletions nitro.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,10 +49,6 @@
"swift": "HybridViewModelEnumProperty",
"kotlin": "HybridViewModelEnumProperty"
},
"ViewModelTriggerProperty": {
"swift": "HybridViewModelTriggerProperty",
"kotlin": "HybridViewModelTriggerProperty"
},
"ViewModelImageProperty": {
"swift": "HybridViewModelImageProperty",
"kotlin": "HybridViewModelImageProperty"
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading