Skip to content

Commit b92d071

Browse files
authored
feat: add RiveImages.loadFromURLAsync for dynamic image loading (#39)
* feat(ios): add RiveImages.loadFromURLAsync for dynamic image loading - Add RiveImage nitro spec and factory for loading images from URLs - Implement iOS native support using RiveRenderImage - Support passing RiveImage directly to referencedAssets - Add Suspense example demonstrating async image loading * feat: android implementation of RiveImage * review suggestions * remove default ctor from HybridRiveImage * refactor: use HTTPLoader for loading files for http * test: added error boundary to suspense example * refactor: throw our MalformedURLException * fix: fix linter error ImageURLS * tweak swift lint * fix: revert rive file parse back to io threads * kotlin:lint fixes * chore: disable ktlint for generated nitrogen files
1 parent 12aa483 commit b92d071

56 files changed

Lines changed: 1949 additions & 51 deletions

File tree

Some content is hidden

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

.editorconfig

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,3 +32,6 @@ ktlint_standard_import-ordering = disabled
3232
ktlint_standard_string-template-indent = disabled
3333
ktlint_standard_backing-property-naming = disabled
3434
ktlint_standard_no-consecutive-comments = disabled
35+
36+
[nitrogen/generated/**/*.kt]
37+
ktlint = disabled

.swiftlint.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,5 +18,7 @@ disabled_rules:
1818
- trailing_whitespace
1919
- todo
2020
- unused_optional_binding
21+
- opening_brace
22+
- trailing_comma
2123

2224
reporter: github-actions-logging
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package com.margelo.nitro.rive
2+
3+
import kotlinx.coroutines.Dispatchers
4+
import kotlinx.coroutines.withContext
5+
import java.net.HttpURLConnection
6+
import java.net.MalformedURLException
7+
import java.net.URL
8+
9+
sealed class HTTPLoaderException(message: String) : Exception(message) {
10+
class InvalidURL(url: String) : HTTPLoaderException("Invalid URL: $url")
11+
class HttpError(val statusCode: Int, url: String) :
12+
HTTPLoaderException("HTTP error $statusCode for $url")
13+
}
14+
15+
object HTTPLoader {
16+
suspend fun downloadBytes(url: String): ByteArray = withContext(Dispatchers.IO) {
17+
val urlObj = try {
18+
URL(url)
19+
} catch (e: MalformedURLException) {
20+
throw HTTPLoaderException.InvalidURL(url)
21+
}
22+
val connection = urlObj.openConnection() as HttpURLConnection
23+
24+
try {
25+
connection.requestMethod = "GET"
26+
val statusCode = connection.responseCode
27+
28+
if (statusCode !in 200..299) {
29+
throw HTTPLoaderException.HttpError(statusCode, url)
30+
}
31+
32+
connection.inputStream.use { it.readBytes() }
33+
} finally {
34+
connection.disconnect()
35+
}
36+
}
37+
}

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

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ import kotlinx.coroutines.Dispatchers
1212
import kotlinx.coroutines.withContext
1313
import java.io.File as JavaFile
1414
import java.net.URI
15-
import java.net.URL
1615

1716
data class FileAndCache(
1817
val file: File,
@@ -45,8 +44,7 @@ class HybridRiveFileFactory : HybridRiveFileFactorySpec() {
4544
return Promise.async {
4645
try {
4746
val fileAndCache = withContext(Dispatchers.IO) {
48-
val urlObj = URL(url)
49-
val riveData = urlObj.readBytes()
47+
val riveData = HTTPLoader.downloadBytes(url)
5048
buildRiveFile(riveData, referencedAssets)
5149
}
5250

@@ -56,7 +54,7 @@ class HybridRiveFileFactory : HybridRiveFileFactorySpec() {
5654
hybridRiveFile.assetLoader = fileAndCache.loader
5755
hybridRiveFile
5856
} catch (e: Exception) {
59-
throw Error("Failed to download Rive file: ${e.message}")
57+
throw Error("Failed to download Rive file: ${e.message}", e)
6058
}
6159
}
6260
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package com.margelo.nitro.rive
2+
3+
import androidx.annotation.Keep
4+
import app.rive.runtime.kotlin.core.RiveRenderImage
5+
import com.facebook.proguard.annotations.DoNotStrip
6+
7+
@Keep
8+
@DoNotStrip
9+
class HybridRiveImage(
10+
val renderImage: RiveRenderImage,
11+
private val dataSize: Int
12+
) : HybridRiveImageSpec() {
13+
override val byteSize: Double
14+
get() = dataSize.toDouble()
15+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package com.margelo.nitro.rive
2+
3+
import androidx.annotation.Keep
4+
import app.rive.runtime.kotlin.core.RiveRenderImage
5+
import com.facebook.proguard.annotations.DoNotStrip
6+
import com.margelo.nitro.core.Promise
7+
8+
@Keep
9+
@DoNotStrip
10+
class HybridRiveImageFactory : HybridRiveImageFactorySpec() {
11+
override fun loadFromURLAsync(url: String): Promise<HybridRiveImageSpec> {
12+
return Promise.async {
13+
try {
14+
val imageData = HTTPLoader.downloadBytes(url)
15+
val renderImage = RiveRenderImage.fromEncoded(imageData)
16+
HybridRiveImage(renderImage, imageData.size)
17+
} catch (e: Exception) {
18+
throw Exception("Failed to load image from URL: $url - ${e.message}", e)
19+
}
20+
}
21+
}
22+
}

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

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ class ReferencedAssetLoader {
8585
file.readBytes()
8686
}
8787
"http", "https" -> {
88-
URL(url).readBytes()
88+
HTTPLoader.downloadBytes(url)
8989
}
9090
else -> {
9191
logError("Unsupported URL scheme: ${uri.scheme}")
@@ -181,14 +181,28 @@ class ReferencedAssetLoader {
181181

182182
private fun processAssetBytes(bytes: ByteArray, asset: FileAsset) {
183183
when (asset) {
184-
is ImageAsset -> asset.image = RiveRenderImage.make(bytes)
184+
is ImageAsset -> asset.image = RiveRenderImage.fromEncoded(bytes)
185185
is FontAsset -> asset.font = RiveFont.make(bytes)
186186
is AudioAsset -> asset.audio = RiveAudio.make(bytes)
187187
}
188188
}
189189

190+
private fun handlePreloadedImage(image: HybridRiveImageSpec, asset: FileAsset) {
191+
if (asset is ImageAsset && image is HybridRiveImage) {
192+
asset.image = image.renderImage
193+
}
194+
}
195+
190196
private fun loadAsset(assetData: ResolvedReferencedAsset, asset: FileAsset, context: Context): Deferred<Unit> {
191197
val deferred = CompletableDeferred<Unit>()
198+
199+
// Check for pre-loaded image first
200+
if (assetData.image != null) {
201+
handlePreloadedImage(assetData.image, asset)
202+
deferred.complete(Unit)
203+
return deferred
204+
}
205+
192206
val listener: (ByteArray?) -> Unit = { bytes ->
193207
if (bytes != null) {
194208
processAssetBytes(bytes, asset)

0 commit comments

Comments
 (0)