Skip to content

Commit 42c36a9

Browse files
committed
feat(cache): updating to a basic url cache
1 parent 95b62b4 commit 42c36a9

7 files changed

Lines changed: 190 additions & 12 deletions

File tree

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ class HybridViewModelImageProperty(private val viewModelImage: ViewModelImagePro
1515
}
1616

1717
override fun addListener(onChanged: () -> Unit) {
18-
listeners.add(onChanged)
19-
ensureValueListenerJob(viewModelImage.valueFlow.map { })
18+
listeners.add { _ -> onChanged() }
19+
ensureValueListenerJob(viewModelImage.valueFlow.map { Unit })
2020
}
2121
}

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

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,24 @@ class ReferencedAssetLoader {
5252

5353
scope.launch {
5454
try {
55-
val bytes = dataSource.createLoader().load(dataSource)
55+
val bytes = when (dataSource) {
56+
is DataSource.Http -> {
57+
// Check cache first for URL assets
58+
val cachedData = URLAssetCache.getCachedData(dataSource.url)
59+
if (cachedData != null) {
60+
cachedData
61+
} else {
62+
// Download and cache
63+
val downloadedData = dataSource.createLoader().load(dataSource)
64+
URLAssetCache.saveToCache(dataSource.url, downloadedData)
65+
downloadedData
66+
}
67+
}
68+
else -> {
69+
// For non-URL assets, use the loader directly
70+
dataSource.createLoader().load(dataSource)
71+
}
72+
}
5673
withContext(Dispatchers.Main) {
5774
processAssetBytes(bytes, asset)
5875
deferred.complete(Unit)
@@ -82,6 +99,7 @@ class ReferencedAssetLoader {
8299
return object : FileAssetLoader() {
83100
override fun loadContents(asset: FileAsset, inBandBytes: ByteArray): Boolean {
84101
var key = asset.uniqueFilename.substringBeforeLast(".")
102+
cache[key] = asset
85103
var assetData = assetsData[key]
86104

87105
if (assetData == null) {
@@ -93,8 +111,6 @@ class ReferencedAssetLoader {
93111
return false
94112
}
95113

96-
cache[key] = asset
97-
98114
loadAsset(assetData, asset)
99115

100116
return true
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
package com.margelo.nitro.rive
2+
3+
import android.content.Context
4+
import android.util.Log
5+
import com.margelo.nitro.NitroModules
6+
import java.io.File
7+
import java.security.MessageDigest
8+
9+
object URLAssetCache {
10+
private const val CACHE_DIR_NAME = "rive_url_assets"
11+
private const val TAG = "URLAssetCache"
12+
13+
private fun getCacheDir(): File? {
14+
val context = NitroModules.applicationContext ?: return null
15+
val cacheDir = File(context.cacheDir, CACHE_DIR_NAME)
16+
if (!cacheDir.exists()) {
17+
cacheDir.mkdirs()
18+
}
19+
return cacheDir
20+
}
21+
22+
private fun urlToCacheKey(url: String): String {
23+
val digest = MessageDigest.getInstance("SHA-256")
24+
val hashBytes = digest.digest(url.toByteArray())
25+
return hashBytes.joinToString("") { "%02x".format(it) }
26+
}
27+
28+
private fun getCacheFile(url: String): File? {
29+
val cacheDir = getCacheDir() ?: return null
30+
val cacheKey = urlToCacheKey(url)
31+
return File(cacheDir, cacheKey)
32+
}
33+
34+
fun getCachedData(url: String): ByteArray? {
35+
return try {
36+
val cacheFile = getCacheFile(url) ?: return null
37+
if (cacheFile.exists() && cacheFile.length() > 0) {
38+
cacheFile.readBytes()
39+
} else {
40+
null
41+
}
42+
} catch (e: Exception) {
43+
Log.e(TAG, "Failed to read from cache: ${e.message}")
44+
null
45+
}
46+
}
47+
48+
fun saveToCache(url: String, data: ByteArray) {
49+
try {
50+
val cacheFile = getCacheFile(url) ?: return
51+
cacheFile.writeBytes(data)
52+
} catch (e: Exception) {
53+
Log.e(TAG, "Failed to save to cache: ${e.message}")
54+
}
55+
}
56+
57+
fun clearCache() {
58+
try {
59+
val cacheDir = getCacheDir() ?: return
60+
cacheDir.listFiles()?.forEach { it.delete() }
61+
} catch (e: Exception) {
62+
Log.e(TAG, "Failed to clear cache: ${e.message}")
63+
}
64+
}
65+
}

example/android/.kotlin/sessions/kotlin-compiler-12952147549113407283.salive

Whitespace-only changes.

ios/ReferencedAssetLoader.swift

Lines changed: 32 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,23 @@ final class ReferencedAssetLoader {
9393

9494
Task {
9595
do {
96-
let data = try await dataSource.createLoader().load(from: dataSource)
96+
let data: Data
97+
98+
// Check cache first for URL assets
99+
if case .http(let url) = dataSource {
100+
if let cachedData = URLAssetCache.getCachedData(for: url.absoluteString) {
101+
data = cachedData
102+
} else {
103+
// Download and cache
104+
let downloadedData = try await dataSource.createLoader().load(from: dataSource)
105+
URLAssetCache.saveToCache(downloadedData, for: url.absoluteString)
106+
data = downloadedData
107+
}
108+
} else {
109+
// For non-URL assets, use the loader directly
110+
data = try await dataSource.createLoader().load(from: dataSource)
111+
}
112+
97113
await MainActor.run {
98114
self.processAssetBytes(data, asset: asset, factory: factory, completion: completion)
99115
}
@@ -120,19 +136,28 @@ final class ReferencedAssetLoader {
120136
)
121137
-> LoadAsset?
122138
{
123-
guard let referencedAssets = referencedAssets, let referencedAssets = referencedAssets.data
139+
guard let referencedAssets = referencedAssets, let assetsData = referencedAssets.data
124140
else {
125141
return nil
126142
}
127143
return { (asset: RiveFileAsset, _: Data, factory: RiveFactory) -> Bool in
128-
let assetByUniqueName = referencedAssets[asset.uniqueName()]
129-
guard let assetData = assetByUniqueName ?? referencedAssets[asset.name()] else {
130-
return false
131-
}
132-
133144
cache.value[asset.uniqueName()] = asset
145+
cache.value[asset.name()] = asset
134146
factoryOut.value = factory
135147

148+
// Look up asset data by unique name or name
149+
var key = (asset.uniqueName() as NSString).deletingPathExtension
150+
var assetData = assetsData[key]
151+
152+
if assetData == nil {
153+
key = asset.name()
154+
assetData = assetsData[key]
155+
}
156+
157+
guard let assetData = assetData else {
158+
return false
159+
}
160+
136161
self.loadAssetInternal(
137162
source: assetData, asset: asset, factory: factory,
138163
completion: {

ios/URLAssetCache.swift

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import Foundation
2+
import CryptoKit
3+
4+
enum URLAssetCache {
5+
private static let cacheDirName = "rive_url_assets"
6+
7+
private static func getCacheDirectory() -> URL? {
8+
guard let cacheDir = FileManager.default.urls(
9+
for: .cachesDirectory,
10+
in: .userDomainMask
11+
).first else {
12+
return nil
13+
}
14+
let riveCacheDir = cacheDir.appendingPathComponent(cacheDirName)
15+
16+
// Create directory if it doesn't exist
17+
try? FileManager.default.createDirectory(
18+
at: riveCacheDir,
19+
withIntermediateDirectories: true,
20+
attributes: nil
21+
)
22+
23+
return riveCacheDir
24+
}
25+
26+
private static func urlToCacheKey(_ url: String) -> String {
27+
let data = Data(url.utf8)
28+
let hash = SHA256.hash(data: data)
29+
return hash.compactMap { String(format: "%02x", $0) }.joined()
30+
}
31+
32+
private static func getCacheFileURL(for url: String) -> URL? {
33+
guard let cacheDir = getCacheDirectory() else { return nil }
34+
let cacheKey = urlToCacheKey(url)
35+
return cacheDir.appendingPathComponent(cacheKey)
36+
}
37+
38+
static func getCachedData(for url: String) -> Data? {
39+
guard let cacheFileURL = getCacheFileURL(for: url) else { return nil }
40+
41+
do {
42+
let data = try Data(contentsOf: cacheFileURL)
43+
return data.isEmpty ? nil : data
44+
} catch {
45+
return nil
46+
}
47+
}
48+
49+
static func saveToCache(_ data: Data, for url: String) {
50+
guard let cacheFileURL = getCacheFileURL(for: url) else { return }
51+
52+
do {
53+
try data.write(to: cacheFileURL)
54+
} catch {
55+
// Silently fail - caching is best effort
56+
}
57+
}
58+
59+
static func clearCache() {
60+
guard let cacheDir = getCacheDirectory() else { return }
61+
62+
do {
63+
let files = try FileManager.default.contentsOfDirectory(at: cacheDir, includingPropertiesForKeys: nil)
64+
for file in files {
65+
try? FileManager.default.removeItem(at: file)
66+
}
67+
} catch {
68+
// Silently fail
69+
}
70+
}
71+
}

nitrogen/generated/android/c++/JHybridViewModelImagePropertySpec.cpp

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)