Skip to content

Commit b3b5dcd

Browse files
authored
fix: [android] pick source loader from URI scheme (#52)
* fix(android): pick source loader from URI scheme Android passed every source to DotLottieSource.Url, so anything that wasn't an http(s) URL silently failed: local require() in release builds and file:// URIs from expo-asset. Debug worked only because Metro serves bundled assets over http. Resolve the loader from the URI like iOS does (Url / Asset / Data / Res), falling back to Url. Fixes #50
1 parent 8e76dcb commit b3b5dcd

3 files changed

Lines changed: 150 additions & 1 deletion

File tree

android/src/main/java/com/dotlottiereactnative/DotlottieReactNativeView.kt

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
package com.dotlottiereactnative
22

33
import android.graphics.Color
4+
import android.net.Uri
45
import android.widget.FrameLayout
56
import androidx.compose.runtime.Composable
67
import androidx.compose.runtime.mutableStateOf
8+
import androidx.compose.runtime.remember
79
import androidx.compose.ui.platform.ComposeView
810
import com.dotlottie.dlplayer.Mode
911
import com.facebook.react.bridge.Arguments
@@ -114,7 +116,7 @@ class DotlottieReactNativeView(context: ThemedReactContext) : FrameLayout(contex
114116
@Composable
115117
fun DotLottieContent() {
116118
animationUrl.value?.let { url ->
117-
val source = DotLottieSource.Url(url)
119+
val source = remember(url) { resolveDotLottieSource(url) }
118120

119121
if (useOpenGLRenderer) {
120122
DotLottieGLAnimation(
@@ -150,6 +152,41 @@ class DotlottieReactNativeView(context: ThemedReactContext) : FrameLayout(contex
150152
}
151153
}
152154

155+
156+
private fun resolveDotLottieSource(url: String): DotLottieSource {
157+
return when {
158+
url.startsWith("http://") || url.startsWith("https://") ->
159+
DotLottieSource.Url(url)
160+
161+
url.startsWith("file:///android_asset/") ->
162+
DotLottieSource.Asset(url.removePrefix("file:///android_asset/"))
163+
164+
url.startsWith("file://") || url.startsWith("content://") ->
165+
readSourceBytes(url)?.let { DotLottieSource.Data(it) } ?: DotLottieSource.Url(url)
166+
167+
else -> resolveResourceSource(url) ?: DotLottieSource.Url(url)
168+
}
169+
}
170+
171+
private fun resolveResourceSource(name: String): DotLottieSource? {
172+
val resId =
173+
reactContext.resources.getIdentifier(name, "raw", reactContext.packageName)
174+
.takeIf { it != 0 }
175+
?: reactContext.resources
176+
.getIdentifier(name, "drawable", reactContext.packageName)
177+
.takeIf { it != 0 }
178+
return resId?.let { DotLottieSource.Res(it) }
179+
}
180+
181+
private fun readSourceBytes(uri: String): ByteArray? {
182+
return try {
183+
reactContext.contentResolver.openInputStream(Uri.parse(uri))?.use { it.readBytes() }
184+
} catch (e: Exception) {
185+
android.util.Log.e("DotLottie", "Failed to read source bytes from $uri: ${e.message}")
186+
null
187+
}
188+
}
189+
153190
// Renderer is locked on first set because switching between GL and Canvas
154191
// surfaces at runtime is not supported by the underlying SDK.
155192
fun setUseOpenGLRenderer(value: Boolean) {

example/App.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
import { LifecycleExample } from './examples/LifecycleExample';
1111
import { StateMachineExample } from './examples/StateMachineExample';
1212
import { MultipleAnimationsTest } from './examples/MultipleAnimationsTest';
13+
import { SourceLoadingExample } from './examples/SourceLoadingExample';
1314

1415
type ExampleDescriptor = {
1516
key: string;
@@ -19,6 +20,13 @@ type ExampleDescriptor = {
1920
};
2021

2122
const EXAMPLES: ExampleDescriptor[] = [
23+
{
24+
key: 'source-loading',
25+
title: 'Source Loading (issue #50)',
26+
description:
27+
'Verifies local require() and remote URL sources render, incl. Android release builds.',
28+
Component: SourceLoadingExample,
29+
},
2230
{
2331
key: 'multiple-animations',
2432
title: 'Multiple Animations Test',
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
import { useState } from 'react';
2+
import { ScrollView, StyleSheet, Text, View } from 'react-native';
3+
import { DotLottie } from '@lottiefiles/dotlottie-react-native';
4+
5+
type LoadState = 'pending' | 'loaded' | 'error';
6+
7+
const STATUS_LABEL: Record<LoadState, string> = {
8+
pending: '… loading',
9+
loaded: '✅ loaded',
10+
error: '❌ load error',
11+
};
12+
13+
function SourceCase({
14+
title,
15+
source,
16+
}: {
17+
title: string;
18+
source: number | { uri: string };
19+
}) {
20+
const [state, setState] = useState<LoadState>('pending');
21+
22+
return (
23+
<View style={styles.card}>
24+
<Text style={styles.cardTitle}>{title}</Text>
25+
<Text
26+
style={[
27+
styles.status,
28+
state === 'loaded' && styles.statusOk,
29+
state === 'error' && styles.statusErr,
30+
]}
31+
>
32+
{STATUS_LABEL[state]}
33+
</Text>
34+
<View style={styles.animationBox}>
35+
<DotLottie
36+
source={source}
37+
style={styles.animation}
38+
autoplay
39+
loop
40+
onLoad={() => setState('loaded')}
41+
onLoadError={() => setState('error')}
42+
/>
43+
</View>
44+
</View>
45+
);
46+
}
47+
48+
export function SourceLoadingExample() {
49+
return (
50+
<ScrollView contentContainerStyle={styles.container}>
51+
<SourceCase
52+
title="Local require() — regressed on Android release"
53+
source={require('../assets/star-rating.lottie')}
54+
/>
55+
<SourceCase
56+
title="Remote HTTPS URL"
57+
source={{
58+
uri: 'https://lottie.host/50f65a48-2f50-432f-b47a-4e6b271625c0/O4jvHbwypB.lottie',
59+
}}
60+
/>
61+
</ScrollView>
62+
);
63+
}
64+
65+
const styles = StyleSheet.create({
66+
container: {
67+
padding: 16,
68+
gap: 16,
69+
},
70+
card: {
71+
backgroundColor: '#fff',
72+
borderRadius: 12,
73+
padding: 16,
74+
gap: 8,
75+
},
76+
cardTitle: {
77+
fontSize: 16,
78+
fontWeight: '600',
79+
color: '#000',
80+
},
81+
status: {
82+
fontSize: 14,
83+
color: '#888',
84+
},
85+
statusOk: {
86+
color: '#1a7f37',
87+
fontWeight: '600',
88+
},
89+
statusErr: {
90+
color: '#cf222e',
91+
fontWeight: '600',
92+
},
93+
animationBox: {
94+
height: 200,
95+
alignItems: 'center',
96+
justifyContent: 'center',
97+
backgroundColor: '#f6f8fa',
98+
borderRadius: 8,
99+
},
100+
animation: {
101+
width: 180,
102+
height: 180,
103+
},
104+
});

0 commit comments

Comments
 (0)