Skip to content

Commit bd22089

Browse files
mfazekasclaude
andauthored
feat: referenced assets (#16)
* feat: referencedAssets (ios) * fix: referencedAssets (ios) - missing files * feat: referencedAssets (ios) * nitrogen files * fix typecheck * fix typecheck * fix: allow useRiveFile to work with undefined input * feat(andorid): implement referencedAssets * feat(ios): referencedAssetLoader - update on native side * chore: added test for useRiveFile * fix(ios): refresh after updating the asset * fix: adderss race condition on doanlowd finished -> refresh * fix: js lint/test * remove rive view * added exmpla * update * fix: typecheck * fix(ios): prevent race condition in referenced asset loading Fixed race condition where views would refresh before asset downloads completed. This mirrors the Android fix in commit c5adec2. Changes: - ReferencedAssetLoader: Added completion handlers to entire async chain - Completion fires in both success and failure cases - HybridRiveFile: Use DispatchGroup to wait for all asset loads - refreshAfterAssetChange() only called after all downloads finish This ensures views display updated assets even with slow network. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * chore: remove rn82-example submodule 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * reactor: streamline rivefile factory * refactor: decouple riveFile and riveView * refactor: remove unused export * chore: refresh * removed unused buildFile --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 7ea6eb6 commit bd22089

53 files changed

Lines changed: 3441 additions & 1531 deletions

Some content is hidden

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

Rive.podspec

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ Pod::Spec.new do |s|
4343

4444
s.source_files = "ios/**/*.{h,m,mm,swift}"
4545

46+
s.public_header_files = ['ios/RCTSwiftLog.h']
4647
load 'nitrogen/generated/ios/Rive+autolinking.rb'
4748
add_nitrogen_files(s)
4849

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

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,23 @@ package com.margelo.nitro.rive
33
import androidx.annotation.Keep
44
import app.rive.runtime.kotlin.core.File
55
import com.facebook.proguard.annotations.DoNotStrip
6+
import com.margelo.nitro.NitroModules
7+
import java.lang.ref.WeakReference
8+
import kotlinx.coroutines.CoroutineScope
9+
import kotlinx.coroutines.Dispatchers
10+
import kotlinx.coroutines.SupervisorJob
11+
import kotlinx.coroutines.awaitAll
12+
import kotlinx.coroutines.cancel
13+
import kotlinx.coroutines.launch
614

715
@Keep
816
@DoNotStrip
917
class HybridRiveFile : HybridRiveFileSpec() {
1018
var riveFile: File? = null
19+
var referencedAssetCache: ReferencedAssetCache? = null
20+
var assetLoader: ReferencedAssetLoader? = null
21+
private val weakViews = mutableListOf<WeakReference<HybridRiveView>>()
22+
private val scope = CoroutineScope(Dispatchers.Main + SupervisorJob())
1123

1224
override val viewModelCount: Double?
1325
get() = riveFile?.viewModelCount?.toDouble()
@@ -37,8 +49,50 @@ class HybridRiveFile : HybridRiveFileSpec() {
3749
}
3850
}
3951

52+
fun registerView(view: HybridRiveView) {
53+
weakViews.add(WeakReference(view))
54+
}
55+
56+
fun unregisterView(view: HybridRiveView) {
57+
weakViews.removeAll { it.get() == view }
58+
}
59+
60+
private fun refreshAfterAssetChange() {
61+
weakViews.removeAll { it.get() == null }
62+
63+
for (weakView in weakViews) {
64+
weakView.get()?.refreshAfterAssetChange()
65+
}
66+
}
67+
68+
override fun updateReferencedAssets(referencedAssets: ReferencedAssetsType) {
69+
val assetsData = referencedAssets.data ?: return
70+
val cache = referencedAssetCache ?: return
71+
val loader = assetLoader ?: return
72+
val context = NitroModules.applicationContext ?: return
73+
74+
val loadJobs = mutableListOf<kotlinx.coroutines.Deferred<Unit>>()
75+
76+
for ((key, assetData) in assetsData) {
77+
val asset = cache[key] ?: continue
78+
loadJobs.add(loader.updateAsset(assetData, asset, context))
79+
}
80+
81+
if (loadJobs.isNotEmpty()) {
82+
scope.launch {
83+
loadJobs.awaitAll()
84+
refreshAfterAssetChange()
85+
}
86+
}
87+
}
88+
4089
override fun release() {
90+
scope.cancel()
91+
assetLoader?.dispose()
92+
assetLoader = null
4193
riveFile?.release()
4294
riveFile = null
95+
referencedAssetCache?.clear()
96+
referencedAssetCache = null
4397
}
4498
}

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

Lines changed: 49 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package com.margelo.nitro.rive
33
import android.annotation.SuppressLint
44
import androidx.annotation.Keep
55
import app.rive.runtime.kotlin.core.File
6+
import app.rive.runtime.kotlin.core.Rive
67
import com.facebook.proguard.annotations.DoNotStrip
78
import com.margelo.nitro.core.ArrayBuffer
89
import com.margelo.nitro.core.Promise
@@ -13,29 +14,54 @@ import java.io.File as JavaFile
1314
import java.net.URI
1415
import java.net.URL
1516

17+
data class FileAndCache(
18+
val file: File,
19+
val cache: ReferencedAssetCache,
20+
val loader: ReferencedAssetLoader?
21+
)
22+
1623
@Keep
1724
@DoNotStrip
1825
class HybridRiveFileFactory : HybridRiveFileFactorySpec() {
19-
override fun fromURL(url: String, loadCdn: Boolean): Promise<HybridRiveFileSpec> {
26+
private fun buildRiveFile(
27+
data: ByteArray,
28+
referencedAssets: ReferencedAssetsType?
29+
): FileAndCache {
30+
val cache = mutableMapOf<String, app.rive.runtime.kotlin.core.FileAsset>()
31+
val loader = ReferencedAssetLoader()
32+
val customLoader = loader.createCustomLoader(referencedAssets, cache)
33+
34+
// TODO: The File object in Android does not have the concept of loading CDN assets
35+
val riveFile = if (customLoader != null) {
36+
File(data, Rive.defaultRendererType, customLoader)
37+
} else {
38+
File(data)
39+
}
40+
41+
return FileAndCache(riveFile, cache, if (customLoader != null) loader else null)
42+
}
43+
44+
override fun fromURL(url: String, loadCdn: Boolean, referencedAssets: ReferencedAssetsType?): Promise<HybridRiveFileSpec> {
2045
return Promise.async {
2146
try {
22-
val riveFile = withContext(Dispatchers.IO) {
47+
val fileAndCache = withContext(Dispatchers.IO) {
2348
val urlObj = URL(url)
2449
val riveData = urlObj.readBytes()
25-
// TODO: The File object in Android does not have the concept of loading CDN assets
26-
File(riveData)
50+
buildRiveFile(riveData, referencedAssets)
2751
}
2852

2953
val hybridRiveFile = HybridRiveFile()
30-
hybridRiveFile.riveFile = riveFile
54+
hybridRiveFile.riveFile = fileAndCache.file
55+
hybridRiveFile.referencedAssetCache = fileAndCache.cache
56+
hybridRiveFile.assetLoader = fileAndCache.loader
3157
hybridRiveFile
3258
} catch (e: Exception) {
3359
throw Error("Failed to download Rive file: ${e.message}")
3460
}
3561
}
3662
}
3763

38-
override fun fromFileURL(fileURL: String, loadCdn: Boolean): Promise<HybridRiveFileSpec> {
64+
override fun fromFileURL(fileURL: String, loadCdn: Boolean, referencedAssets: ReferencedAssetsType?): Promise<HybridRiveFileSpec> {
3965
if (!fileURL.startsWith("file://")) {
4066
throw Error("fromFileURL: URL must be a file URL: $fileURL")
4167
}
@@ -45,14 +71,16 @@ class HybridRiveFileFactory : HybridRiveFileFactorySpec() {
4571
val uri = URI(fileURL)
4672
val path = uri.path ?: throw Error("fromFileURL: Invalid URL: $fileURL")
4773

48-
val riveFile = withContext(Dispatchers.IO) {
74+
val fileAndCache = withContext(Dispatchers.IO) {
4975
val file = JavaFile(path)
5076
val riveData = file.readBytes()
51-
File(riveData)
77+
buildRiveFile(riveData, referencedAssets)
5278
}
5379

5480
val hybridRiveFile = HybridRiveFile()
55-
hybridRiveFile.riveFile = riveFile
81+
hybridRiveFile.riveFile = fileAndCache.file
82+
hybridRiveFile.referencedAssetCache = fileAndCache.cache
83+
hybridRiveFile.assetLoader = fileAndCache.loader
5684
hybridRiveFile
5785
} catch (e: Exception) {
5886
throw Error("Failed to load Rive file: ${e.message}")
@@ -61,39 +89,43 @@ class HybridRiveFileFactory : HybridRiveFileFactorySpec() {
6189
}
6290

6391
@SuppressLint("DiscouragedApi")
64-
override fun fromResource(resource: String, loadCdn: Boolean): Promise<HybridRiveFileSpec> {
92+
override fun fromResource(resource: String, loadCdn: Boolean, referencedAssets: ReferencedAssetsType?): Promise<HybridRiveFileSpec> {
6593
return Promise.async {
6694
try {
6795
val context = NitroModules.applicationContext
6896
?: throw Error("Could not load Rive file ($resource) from resource. No application context.")
69-
val riveFile = withContext(Dispatchers.IO) {
97+
val fileAndCache = withContext(Dispatchers.IO) {
7098
val resourceId = context.resources.getIdentifier(resource, "raw", context.packageName)
7199
if (resourceId == 0) {
72100
throw Error("Could not find Rive file: $resource.riv")
73101
}
74102
val inputStream = context.resources.openRawResource(resourceId)
75103
val riveData = inputStream.readBytes()
76-
File(riveData)
104+
buildRiveFile(riveData, referencedAssets)
77105
}
78106

79107
val hybridRiveFile = HybridRiveFile()
80-
hybridRiveFile.riveFile = riveFile
108+
hybridRiveFile.riveFile = fileAndCache.file
109+
hybridRiveFile.referencedAssetCache = fileAndCache.cache
110+
hybridRiveFile.assetLoader = fileAndCache.loader
81111
hybridRiveFile
82112
} catch (e: Exception) {
83113
throw Error("Failed to load Rive file: ${e.message}")
84114
}
85115
}
86116
}
87117

88-
override fun fromBytes(bytes: ArrayBuffer, loadCdn: Boolean): Promise<HybridRiveFileSpec> {
89-
val buffer = bytes.getBuffer(false) // Use false to avoid creating a read-only buffer
118+
override fun fromBytes(bytes: ArrayBuffer, loadCdn: Boolean, referencedAssets: ReferencedAssetsType?): Promise<HybridRiveFileSpec> {
119+
val buffer = bytes.getBuffer(false)
90120
return Promise.async {
91121
try {
92122
val byteArray = ByteArray(buffer.remaining())
93123
buffer.get(byteArray)
94-
val riveFile = File(byteArray)
124+
val fileAndCache = buildRiveFile(byteArray, referencedAssets)
95125
val hybridRiveFile = HybridRiveFile()
96-
hybridRiveFile.riveFile = riveFile
126+
hybridRiveFile.riveFile = fileAndCache.file
127+
hybridRiveFile.referencedAssetCache = fileAndCache.cache
128+
hybridRiveFile.assetLoader = fileAndCache.loader
97129
hybridRiveFile
98130
} catch (e: Exception) {
99131
throw Error("Failed to load Rive file from bytes: ${e.message}")

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

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ class HybridRiveView(val context: ThemedReactContext) : HybridRiveViewSpec() {
2525
//region State
2626
override val view: RiveReactNativeView = RiveReactNativeView(context)
2727
private var needsReload = false
28+
private var registeredFile: HybridRiveFile? = null
2829
//endregion
2930

3031
//region View Props
@@ -46,6 +47,10 @@ class HybridRiveView(val context: ThemedReactContext) : HybridRiveViewSpec() {
4647
}
4748
override var file: HybridRiveFileSpec = HybridRiveFile()
4849
set(value) {
50+
if (field != value) {
51+
registeredFile?.unregisterView(this)
52+
registeredFile = null
53+
}
4954
changed(field, value) { field = it }
5055
}
5156
override var alignment: Alignment? = null
@@ -99,8 +104,13 @@ class HybridRiveView(val context: ThemedReactContext) : HybridRiveViewSpec() {
99104
//endregion
100105

101106
//region Update
107+
fun refreshAfterAssetChange() {
108+
afterUpdate()
109+
}
110+
102111
override fun afterUpdate() {
103-
val riveFile = (file as? HybridRiveFile)?.riveFile ?: return
112+
val hybridFile = file as? HybridRiveFile
113+
val riveFile = hybridFile?.riveFile ?: return
104114

105115
val config = ViewConfiguration(
106116
artboardName = artboardName,
@@ -113,6 +123,12 @@ class HybridRiveView(val context: ThemedReactContext) : HybridRiveViewSpec() {
113123
layoutScaleFactor = layoutScaleFactor?.toFloat() ?: DefaultConfiguration.LAYOUTSCALEFACTOR,
114124
)
115125
view.configure(config, needsReload)
126+
127+
if (needsReload && hybridFile != null) {
128+
hybridFile.registerView(this)
129+
registeredFile = hybridFile
130+
}
131+
116132
needsReload = false
117133
super.afterUpdate()
118134
}

0 commit comments

Comments
 (0)