Skip to content

Commit 58df7da

Browse files
mfazekasclaude
andauthored
fix(android): prevent crash from double release during navigation (#46)
The Rive Android SDK's RiveViewLifecycleObserver releases dependencies on fragment lifecycle onDestroy. During React Navigation screen transitions, fragments are destroyed before views are actually disposed, causing release() to be called when refs are already at 0. Crash: java.lang.IllegalArgumentException: Failed requirement. at app.rive.runtime.kotlin.controllers.RiveFileController.release() Fix ports the proven willDispose pattern from rive-react-native: - ReactNativeRiveViewLifecycleObserver: overrides onDestroy to skip auto-release, adds explicit dispose() method - ReactNativeRiveAnimationView: custom RiveAnimationView that uses our lifecycle observer - RiveReactNativeView: adds willDispose flag, only cleans up in onDetachedFromWindow when flag is set - RiveViewManager: extends HybridRiveViewManager to call dispose() in onDropViewInstance (when React Native removes the view) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude <noreply@anthropic.com>
1 parent 29de2ca commit 58df7da

4 files changed

Lines changed: 60 additions & 6 deletions

File tree

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
package com.margelo.nitro.rive
22

3-
import android.util.Log
43
import androidx.annotation.Keep
54
import com.facebook.proguard.annotations.DoNotStrip
65
import com.facebook.react.uimanager.ThemedReactContext
@@ -148,6 +147,7 @@ class HybridRiveView(val context: ThemedReactContext) : HybridRiveViewSpec() {
148147
afterUpdate()
149148
}
150149

150+
151151
override fun afterUpdate() {
152152
logged(TAG, "afterUpdate") {
153153
val hybridFile = file as? HybridRiveFile

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

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,11 @@ import com.facebook.react.bridge.ReactApplicationContext
66
import com.facebook.react.module.model.ReactModuleInfoProvider
77
import com.facebook.react.uimanager.ViewManager
88
import com.margelo.nitro.rive.riveOnLoad
9-
import com.margelo.nitro.rive.views.HybridRiveViewManager
109

1110
class RivePackage : BaseReactPackage() {
1211
override fun createViewManagers(reactContext: ReactApplicationContext): List<ViewManager<in Nothing, in Nothing>> {
1312
val viewManagers: MutableList<ViewManager<*, *>> = ArrayList()
14-
viewManagers.add(HybridRiveViewManager())
13+
viewManagers.add(RiveViewManager())
1514
return viewManagers
1615
}
1716
override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? {

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

Lines changed: 47 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,13 @@ package com.rive
22

33
import android.annotation.SuppressLint
44
import android.widget.FrameLayout
5-
import com.facebook.react.uimanager.ThemedReactContext
5+
import androidx.lifecycle.LifecycleObserver
6+
import androidx.lifecycle.LifecycleOwner
67
import app.rive.runtime.kotlin.RiveAnimationView
8+
import app.rive.runtime.kotlin.RiveViewLifecycleObserver
79
import app.rive.runtime.kotlin.controllers.RiveFileController
10+
import app.rive.runtime.kotlin.core.RefCount
11+
import com.facebook.react.uimanager.ThemedReactContext
812
import app.rive.runtime.kotlin.core.Alignment
913
import app.rive.runtime.kotlin.core.File
1014
import app.rive.runtime.kotlin.core.Fit
@@ -40,18 +44,58 @@ data class ViewConfiguration(
4044
val bindData: BindData
4145
)
4246

47+
class ReactNativeRiveViewLifecycleObserver(dependencies: MutableList<RefCount>) :
48+
RiveViewLifecycleObserver(dependencies) {
49+
@SuppressLint("MissingSuperCall")
50+
override fun onDestroy(owner: LifecycleOwner) {
51+
owner.lifecycle.removeObserver(this)
52+
}
53+
54+
fun dispose() {
55+
dependencies.forEach { it.release() }
56+
dependencies.clear()
57+
}
58+
}
59+
60+
@SuppressLint("ViewConstructor")
61+
class ReactNativeRiveAnimationView(context: ThemedReactContext) : RiveAnimationView(context) {
62+
fun dispose() {
63+
(lifecycleObserver as ReactNativeRiveViewLifecycleObserver).dispose()
64+
}
65+
66+
@SuppressLint("VisibleForTests")
67+
override fun createObserver(): LifecycleObserver {
68+
return ReactNativeRiveViewLifecycleObserver(
69+
listOfNotNull(controller, rendererAttributes.assetLoader).toMutableList()
70+
)
71+
}
72+
}
73+
4374
@SuppressLint("ViewConstructor")
4475
class RiveReactNativeView(context: ThemedReactContext) : FrameLayout(context) {
45-
internal var riveAnimationView: RiveAnimationView? = null
76+
internal var riveAnimationView: ReactNativeRiveAnimationView? = null
4677
private var eventListeners: MutableList<RiveFileController.RiveEventListener> = mutableListOf()
4778
private val viewReadyDeferred = CompletableDeferred<Boolean>()
4879
private var _activeStateMachineName: String? = null
80+
private var willDispose = false
4981

5082
init {
51-
riveAnimationView = RiveAnimationView(context)
83+
riveAnimationView = ReactNativeRiveAnimationView(context)
5284
addView(riveAnimationView)
5385
}
5486

87+
fun dispose() {
88+
willDispose = true
89+
}
90+
91+
override fun onDetachedFromWindow() {
92+
if (willDispose) {
93+
riveAnimationView?.dispose()
94+
removeEventListeners()
95+
}
96+
super.onDetachedFromWindow()
97+
}
98+
5599
//region Public Methods (API)
56100
suspend fun awaitViewReady(): Boolean {
57101
return viewReadyDeferred.await()
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package com.rive
2+
3+
import android.view.View
4+
import com.margelo.nitro.rive.views.HybridRiveViewManager
5+
6+
class RiveViewManager : HybridRiveViewManager() {
7+
override fun onDropViewInstance(view: View) {
8+
(view as? RiveReactNativeView)?.dispose()
9+
super.onDropViewInstance(view)
10+
}
11+
}

0 commit comments

Comments
 (0)