Skip to content

Commit 592b189

Browse files
mfazekasclaude
andauthored
feat: add dataBind prop to RiveView (#23)
* feat: add viewModelInstance to ViewProps, so we don't need to wait for ref * feat: getBoundViewModelInstance * dataBind 1st gen * fix: apply data binding changes in-place without reload - Remove needsReload trigger from dataBind property changes - Add in-place binding update methods for both platforms - Fix flicker when switching between binding modes iOS Changes: - Add applyDataBinding() method on RiveReactNativeView - Call applyDataBinding() in didSet instead of needsReload - Directly call bind/enableAutoBind/disableAutoBind on existing view Android Changes: - Add custom setter for dataBind property - Add configureDataBinding() method - Directly update viewModelInstance on existing state machine - Make riveAnimationView internal for access from HybridRiveView Result: - Smooth transitions between red/green/blue/none/auto modes - No visual flicker or reload when switching binding modes - Better performance (no view recreation) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * fix(ios): bind view model instance to both artboard and state machine - Add artboard.bind(viewModelInstance:) in addition to stateMachine binding - Required for visual changes to take effect when switching binding modes - Matches old rive-react-native implementation pattern 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * fix(ios): play after binding to refresh view - Add baseViewModel?.play() after binding view model instance - Forces view to refresh and show the new binding state - Helps ensure visual updates appear immediately 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * fix(ios): play after none and auto binding modes - Add baseViewModel?.play() for .none and .auto cases - Ensures view refreshes when switching to these modes - All binding modes now trigger play() for consistent behavior - Fix prettier formatting in example 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * fix(android): bind to artboard and play after binding changes - Add artboard.viewModelInstance binding in addition to stateMachine - Add view.play() call after all binding mode changes - Matches iOS implementation for consistent behavior - Ensures visual updates appear when switching modes 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * fix(android): trigger full reload on binding mode changes - Remove in-place configureDataBinding() method - Set needsReload = true and call afterUpdate() when dataBind changes - Matches old rive-react-native pattern for referencedAssets changes - Full reload via setRiveFile() with new binding configuration - iOS keeps play() approach for smoother experience 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * refactor(android): remove manual afterUpdate call from dataBind setter - Remove afterUpdate() call from custom setter - React Native automatically calls afterUpdate() after all props update - Prevents potential double-reload issues 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * fix(android): play state machine after binding mode reload - Add isFirstConfigure flag to track initial load - Call play(stateMachineName, isStateMachine: true) after reload - Only play on subsequent reloads, not on first configure - Ensures state machine restarts with new binding iOS note: iOS already calls baseViewModel.play() in applyDataBinding() which handles non-first updates through the didSet mechanism 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * fix(andorid): use ios like implementation * fix(andorid): use ios like implementation * removed unused files * refator: simplify changes * fix: make dataBind optional and default to auto * review fixes * renamed miklos_viewmodel to many_viewmodels * review fixes * fix: add logged to android * fix: dataBindingChanged * Apply suggestion from @mfazekas * fix: always apply data binding on initial update * fix(andorid): always apply data binding on initial update --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent a4ed4ec commit 592b189

46 files changed

Lines changed: 1433 additions & 352 deletions

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/HybridRiveView.kt

Lines changed: 74 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,42 @@
11
package com.margelo.nitro.rive
22

3+
import android.util.Log
34
import androidx.annotation.Keep
45
import com.facebook.proguard.annotations.DoNotStrip
56
import com.facebook.react.uimanager.ThemedReactContext
67
import com.margelo.nitro.core.Promise
8+
import com.rive.BindData
79
import com.rive.RiveReactNativeView
810
import com.rive.ViewConfiguration
911
import app.rive.runtime.kotlin.core.Fit as RiveFit
1012
import app.rive.runtime.kotlin.core.Alignment as RiveAlignment
1113
import kotlinx.coroutines.Dispatchers
1214
import kotlinx.coroutines.withContext
1315

16+
fun Variant_HybridViewModelInstanceSpec_DataBindMode_DataBindByName?.toBindData(): BindData {
17+
if (this == null) return BindData.Auto
18+
19+
return when (this) {
20+
is Variant_HybridViewModelInstanceSpec_DataBindMode_DataBindByName.First -> {
21+
val instance = (this.asFirstOrNull() as? HybridViewModelInstance)?.viewModelInstance
22+
?: throw Error("Invalid ViewModelInstance")
23+
BindData.Instance(instance)
24+
}
25+
is Variant_HybridViewModelInstanceSpec_DataBindMode_DataBindByName.Second -> {
26+
when (this.asSecondOrNull()) {
27+
DataBindMode.AUTO -> BindData.Auto
28+
DataBindMode.NONE -> BindData.None
29+
else -> BindData.None
30+
}
31+
}
32+
is Variant_HybridViewModelInstanceSpec_DataBindMode_DataBindByName.Third -> {
33+
val name = this.asThirdOrNull()?.byName ?: throw Error("Missing byName value")
34+
BindData.ByName(name)
35+
}
36+
}
37+
}
38+
1439
object DefaultConfiguration {
15-
const val AUTOBIND = false
1640
const val AUTOPLAY = true
1741
val FIT = RiveFit.CONTAIN
1842
val ALIGNMENT = RiveAlignment.CENTER
@@ -25,6 +49,8 @@ class HybridRiveView(val context: ThemedReactContext) : HybridRiveViewSpec() {
2549
//region State
2650
override val view: RiveReactNativeView = RiveReactNativeView(context)
2751
private var needsReload = false
52+
private var dataBindingChanged = false
53+
private var initialUpdate = true
2854
private var registeredFile: HybridRiveFile? = null
2955
//endregion
3056

@@ -41,10 +67,6 @@ class HybridRiveView(val context: ThemedReactContext) : HybridRiveViewSpec() {
4167
set(value) {
4268
changed(field, value) { field = it }
4369
}
44-
override var autoBind: Boolean? = null
45-
set(value) {
46-
changed(field, value) { field = it }
47-
}
4870
override var file: HybridRiveFileSpec = HybridRiveFile()
4971
set(value) {
5072
if (field != value) {
@@ -56,6 +78,13 @@ class HybridRiveView(val context: ThemedReactContext) : HybridRiveViewSpec() {
5678
override var alignment: Alignment? = null
5779
override var fit: Fit? = null
5880
override var layoutScaleFactor: Double? = null
81+
override var dataBind: Variant_HybridViewModelInstanceSpec_DataBindMode_DataBindByName? = null
82+
set(value) {
83+
if (field != value) {
84+
field = value
85+
dataBindingChanged = true
86+
}
87+
}
5988
//endregion
6089

6190
//region View Methods
@@ -73,6 +102,11 @@ class HybridRiveView(val context: ThemedReactContext) : HybridRiveViewSpec() {
73102
view.bindViewModelInstance(hybridVmi.viewModelInstance)
74103
}
75104

105+
override fun getViewModelInstance(): HybridViewModelInstanceSpec? {
106+
val viewModelInstance = view.getViewModelInstance() ?: return null
107+
return HybridViewModelInstance(viewModelInstance)
108+
}
109+
76110
override fun play() = executeOnUiThread { view.play() }
77111

78112
override fun pause() = executeOnUiThread { view.pause() }
@@ -109,28 +143,32 @@ class HybridRiveView(val context: ThemedReactContext) : HybridRiveViewSpec() {
109143
}
110144

111145
override fun afterUpdate() {
112-
val hybridFile = file as? HybridRiveFile
113-
val riveFile = hybridFile?.riveFile ?: return
114-
115-
val config = ViewConfiguration(
116-
artboardName = artboardName,
117-
stateMachineName = stateMachineName,
118-
autoPlay = autoPlay ?: DefaultConfiguration.AUTOPLAY,
119-
autoBind = autoBind ?: DefaultConfiguration.AUTOBIND,
120-
riveFile = riveFile,
121-
alignment = convertAlignment(alignment) ?: DefaultConfiguration.ALIGNMENT,
122-
fit = convertFit(fit) ?: DefaultConfiguration.FIT,
123-
layoutScaleFactor = layoutScaleFactor?.toFloat() ?: DefaultConfiguration.LAYOUTSCALEFACTOR,
124-
)
125-
view.configure(config, needsReload)
126-
127-
if (needsReload && hybridFile != null) {
128-
hybridFile.registerView(this)
129-
registeredFile = hybridFile
130-
}
131-
132-
needsReload = false
133-
super.afterUpdate()
146+
logged(TAG, "afterUpdate") {
147+
val hybridFile = file as? HybridRiveFile
148+
val riveFile = hybridFile?.riveFile ?: return@logged
149+
150+
val config = ViewConfiguration(
151+
artboardName = artboardName,
152+
stateMachineName = stateMachineName,
153+
autoPlay = autoPlay ?: DefaultConfiguration.AUTOPLAY,
154+
riveFile = riveFile,
155+
alignment = convertAlignment(alignment) ?: DefaultConfiguration.ALIGNMENT,
156+
fit = convertFit(fit) ?: DefaultConfiguration.FIT,
157+
layoutScaleFactor = layoutScaleFactor?.toFloat() ?: DefaultConfiguration.LAYOUTSCALEFACTOR,
158+
bindData = dataBind.toBindData()
159+
)
160+
view.configure(config, dataBindingChanged=dataBindingChanged, needsReload, initialUpdate= initialUpdate)
161+
162+
if (needsReload && hybridFile != null) {
163+
hybridFile.registerView(this)
164+
registeredFile = hybridFile
165+
}
166+
167+
needsReload = false
168+
dataBindingChanged = false
169+
initialUpdate = false
170+
super.afterUpdate()
171+
}
134172
}
135173
//endregion
136174

@@ -184,5 +222,14 @@ class HybridRiveView(val context: ThemedReactContext) : HybridRiveViewSpec() {
184222
Fit.LAYOUT -> RiveFit.LAYOUT
185223
}
186224
}
225+
226+
fun logged(tag: String, note: String? = null, fn: () -> Unit) {
227+
try {
228+
fn()
229+
} catch (e: Exception) {
230+
// TODO add onError callback
231+
Log.e("[RIVE]", "$tag ${note ?: ""} $e")
232+
}
233+
}
187234
//endregion
188235
}

android/src/main/java/com/rive/RiveReactNativeView.kt

Lines changed: 70 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,27 +14,35 @@ import app.rive.runtime.kotlin.core.SMIBoolean
1414
import app.rive.runtime.kotlin.core.SMIInput
1515
import app.rive.runtime.kotlin.core.SMINumber
1616
import app.rive.runtime.kotlin.core.ViewModelInstance
17+
import app.rive.runtime.kotlin.core.errors.ViewModelException
1718
import com.margelo.nitro.core.AnyMap
1819
import com.margelo.nitro.rive.EventPropertiesOutput
1920
import com.margelo.nitro.rive.EventPropertiesOutputExtensions as EPO
2021
import com.margelo.nitro.rive.RiveEventType
2122
import com.margelo.nitro.rive.UnifiedRiveEvent as RNEvent
2223
import kotlinx.coroutines.CompletableDeferred
2324

25+
sealed class BindData {
26+
data object None : BindData()
27+
data object Auto : BindData()
28+
data class Instance(val instance: ViewModelInstance) : BindData()
29+
data class ByName(val name: String) : BindData()
30+
}
31+
2432
data class ViewConfiguration(
2533
val artboardName: String?,
2634
val stateMachineName: String?,
27-
val autoBind: Boolean,
2835
val autoPlay: Boolean,
2936
val riveFile: File,
3037
val alignment: Alignment,
3138
val fit: Fit,
32-
val layoutScaleFactor: Float?
39+
val layoutScaleFactor: Float?,
40+
val bindData: BindData
3341
)
3442

3543
@SuppressLint("ViewConstructor")
3644
class RiveReactNativeView(context: ThemedReactContext) : FrameLayout(context) {
37-
private var riveAnimationView: RiveAnimationView? = null
45+
internal var riveAnimationView: RiveAnimationView? = null
3846
private var eventListeners: MutableList<RiveFileController.RiveEventListener> = mutableListOf()
3947
private val viewReadyDeferred = CompletableDeferred<Boolean>()
4048
private var _activeStateMachineName: String? = null
@@ -49,14 +57,14 @@ class RiveReactNativeView(context: ThemedReactContext) : FrameLayout(context) {
4957
return viewReadyDeferred.await()
5058
}
5159

52-
fun configure(config: ViewConfiguration, reload: Boolean = false) {
60+
fun configure(config: ViewConfiguration, dataBindingChanged: Boolean, reload: Boolean = false, initialUpdate: Boolean = false) {
5361
if (reload) {
5462
riveAnimationView?.setRiveFile(
5563
config.riveFile,
5664
artboardName = config.artboardName,
5765
stateMachineName = config.stateMachineName,
5866
autoplay = config.autoPlay,
59-
autoBind = config.autoBind,
67+
autoBind = config.bindData is BindData.Auto,
6068
alignment = config.alignment,
6169
fit = config.fit
6270
)
@@ -67,6 +75,11 @@ class RiveReactNativeView(context: ThemedReactContext) : FrameLayout(context) {
6775
// TODO: this seems to require a reload for the view to take the new value (bug on Android)
6876
riveAnimationView?.layoutScaleFactor = config.layoutScaleFactor
6977
}
78+
79+
if (dataBindingChanged || initialUpdate) {
80+
applyDataBinding(config.bindData)
81+
}
82+
7083
viewReadyDeferred.complete(true)
7184
}
7285

@@ -77,6 +90,58 @@ class RiveReactNativeView(context: ThemedReactContext) : FrameLayout(context) {
7790
}
7891
}
7992

93+
fun getViewModelInstance(): ViewModelInstance? {
94+
val stateMachines = riveAnimationView?.controller?.stateMachines
95+
return if (!stateMachines.isNullOrEmpty()) {
96+
stateMachines.first().viewModelInstance
97+
} else {
98+
null
99+
}
100+
}
101+
102+
fun applyDataBinding(bindData: BindData) {
103+
val stateMachines = riveAnimationView?.controller?.stateMachines
104+
if (stateMachines.isNullOrEmpty()) return
105+
106+
val stateMachine = stateMachines.first()
107+
108+
when (bindData) {
109+
is BindData.None -> {
110+
stateMachine.viewModelInstance = null
111+
}
112+
is BindData.Auto -> {
113+
val artboard = riveAnimationView?.controller?.activeArtboard
114+
val file = riveAnimationView?.controller?.file
115+
if (artboard != null && file != null) {
116+
try {
117+
file.defaultViewModelForArtboard(artboard)
118+
} catch (e: ViewModelException) {
119+
null
120+
}?.let {
121+
val instance = it.createDefaultInstance()
122+
stateMachine.viewModelInstance = instance
123+
}
124+
}
125+
}
126+
is BindData.Instance -> {
127+
stateMachine.viewModelInstance = bindData.instance
128+
}
129+
is BindData.ByName -> {
130+
val artboard = riveAnimationView?.controller?.activeArtboard
131+
val file = riveAnimationView?.controller?.file
132+
if (artboard != null && file != null) {
133+
val viewModel = file.defaultViewModelForArtboard(artboard)
134+
val instance = viewModel.createInstanceFromName(bindData.name)
135+
stateMachine.viewModelInstance = instance
136+
}
137+
}
138+
}
139+
140+
stateMachine.name.let { smName ->
141+
riveAnimationView?.play(smName, isStateMachine = true)
142+
}
143+
}
144+
80145
fun play() = riveAnimationView?.play()
81146

82147
fun pause() = riveAnimationView?.pause();
857 KB
Binary file not shown.

0 commit comments

Comments
 (0)