Skip to content

Commit 618c21f

Browse files
t0maborokkafar
andauthored
feat(Android, Tabs): Update approach for loading external sources for tab icons (#3216)
## Description In the initial approach, we used Coil image loading library. However, after the 1st release, Coil has introduced several issues that had an impact on the stability and maintainability. After reevaluating other options, we've decided to try migrating to Fresco. This choice aligns better because React Native also relies on Fresco internally. Using Fresco should improve consistency with the RN and reduce our maintenance overhead. One known limitation is that Fresco doesn't support loading images in SVG format. We’ll need to explore alternative strategies to adapt. **Note**: Slightly depends on: #3214 Fresco doesn't support SVGs, so we decided to handle them by passing assets via the `AndroidStudio` as vector icons - this approach simplifies a lot of things, especially that ``` PlatformIconAndroid = | { type: 'drawableResourceAndroid'; name: string; } ``` will cover SVGs, so we can omit adding a support here. I'll ensure to mention there that SVG should be passed with `drawableResourceAndroid` only. ## Changes - Removed Coil - Replaced logic for Coil ImageLoader with Fresco APIs - Extracted ImageLoader to a separate file ## Test code and steps to reproduce Tested with BottomTabs example. ## Checklist - [x] Included code example that can be used to test this change - [x] Ensured that CI passes --------- Co-authored-by: Kacper Kafara <[email protected]>
1 parent 469fc70 commit 618c21f

File tree

3 files changed

+105
-95
lines changed

3 files changed

+105
-95
lines changed

android/build.gradle

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -246,12 +246,6 @@ dependencies {
246246
implementation 'com.google.android.material:material:1.12.0'
247247
implementation "androidx.core:core-ktx:1.8.0"
248248

249-
def COIL_VERSION = "3.0.4"
250-
251-
implementation("io.coil-kt.coil3:coil:${COIL_VERSION}")
252-
implementation("io.coil-kt.coil3:coil-network-okhttp:${COIL_VERSION}")
253-
implementation("io.coil-kt.coil3:coil-svg:${COIL_VERSION}")
254-
255249
constraints {
256250
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1") {
257251
because("on older React Native versions this dependency conflicts with react-native-screens")

android/src/main/java/com/swmansion/rnscreens/gamma/tabs/TabScreenViewManager.kt

Lines changed: 9 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,7 @@
11
package com.swmansion.rnscreens.gamma.tabs
22

3-
import android.content.Context
4-
import android.graphics.drawable.Drawable
5-
import android.util.Log
6-
import coil3.ImageLoader
7-
import coil3.asDrawable
8-
import coil3.request.ImageRequest
9-
import coil3.svg.SvgDecoder
3+
import android.os.Handler
4+
import android.os.Looper
105
import com.facebook.react.bridge.Dynamic
116
import com.facebook.react.bridge.ReadableMap
127
import com.facebook.react.module.annotations.ReactModule
@@ -21,6 +16,7 @@ import com.swmansion.rnscreens.gamma.tabs.event.TabScreenDidAppearEvent
2116
import com.swmansion.rnscreens.gamma.tabs.event.TabScreenDidDisappearEvent
2217
import com.swmansion.rnscreens.gamma.tabs.event.TabScreenWillAppearEvent
2318
import com.swmansion.rnscreens.gamma.tabs.event.TabScreenWillDisappearEvent
19+
import com.swmansion.rnscreens.gamma.tabs.image.loadTabImage
2420
import com.swmansion.rnscreens.utils.RNSLog
2521

2622
@ReactModule(name = TabScreenViewManager.REACT_CLASS)
@@ -31,18 +27,9 @@ class TabScreenViewManager :
3127

3228
override fun getName() = REACT_CLASS
3329

34-
var imageLoader: ImageLoader? = null
35-
3630
var context: ThemedReactContext? = null
3731

3832
override fun createViewInstance(reactContext: ThemedReactContext): TabScreen {
39-
imageLoader =
40-
ImageLoader
41-
.Builder(reactContext)
42-
.components {
43-
add(SvgDecoder.Factory())
44-
}.build()
45-
context = reactContext
4633
RNSLog.d(REACT_CLASS, "createViewInstance")
4734
return TabScreen(reactContext)
4835
}
@@ -206,86 +193,19 @@ class TabScreenViewManager :
206193
value: ReadableMap?,
207194
) {
208195
val uri = value?.getString("uri")
209-
210196
if (uri != null) {
211197
val context = view.context
212-
val source = resolveSource(context, uri)
213-
214-
if (source != null) {
215-
loadUsingCoil(context, source) {
216-
view.icon = it
198+
loadTabImage(context, uri) { drawable ->
199+
// Since image loading might happen on a background thread
200+
// ref. https://frescolib.org/docs/intro-image-pipeline.html
201+
// We should schedule rendering the result on the UI thread
202+
Handler(Looper.getMainLooper()).post {
203+
view.icon = drawable
217204
}
218205
}
219206
}
220207
}
221208

222-
private fun loadUsingCoil(
223-
context: Context,
224-
source: RNSImageSource,
225-
onLoad: (img: Drawable) -> Unit,
226-
) {
227-
val data =
228-
when (source) {
229-
is RNSImageSource.DrawableRes -> source.resId
230-
is RNSImageSource.UriString -> source.uri
231-
}
232-
233-
val request =
234-
ImageRequest
235-
.Builder(context)
236-
.data(data)
237-
.target { drawable ->
238-
val stateDrawable = drawable.asDrawable(context.resources)
239-
onLoad(stateDrawable)
240-
}.listener(
241-
onError = { _, result ->
242-
Log.e("[RNScreens]", "Error loading image: $data", result.throwable)
243-
},
244-
onCancel = {
245-
Log.w("[RNScreens]", "Image loading request cancelled: $data")
246-
},
247-
).build()
248-
249-
imageLoader?.enqueue(request)
250-
}
251-
252-
private fun resolveSource(
253-
context: Context,
254-
uri: String,
255-
): RNSImageSource? {
256-
// In release builds, assets are coming with bundle and we need to work with resource id.
257-
// In debug, metro is responsible for handling assets via http.
258-
// At the moment, we're supporting images (drawable) and SVG icons (raw).
259-
// For any other type, we may consider adding a support in the future if needed.
260-
if (uri.startsWith("_")) {
261-
val drawableResId = context.resources.getIdentifier(uri, "drawable", context.packageName)
262-
if (drawableResId != 0) {
263-
return RNSImageSource.DrawableRes(drawableResId)
264-
}
265-
266-
val rawResId = context.resources.getIdentifier(uri, "raw", context.packageName)
267-
if (rawResId != 0) {
268-
return RNSImageSource.DrawableRes(rawResId)
269-
}
270-
271-
Log.e("[RNScreens]", "Resource not found in drawable or raw: $uri")
272-
return null
273-
}
274-
275-
// If asset isn't included in android source directories and we're loading it from given path.
276-
return RNSImageSource.UriString(uri)
277-
}
278-
279-
private sealed class RNSImageSource {
280-
data class DrawableRes(
281-
val resId: Int,
282-
) : RNSImageSource()
283-
284-
data class UriString(
285-
val uri: String,
286-
) : RNSImageSource()
287-
}
288-
289209
companion object {
290210
const val REACT_CLASS = "RNSBottomTabsScreen"
291211
}
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
package com.swmansion.rnscreens.gamma.tabs.image
2+
3+
import android.content.Context
4+
import android.graphics.drawable.Drawable
5+
import android.util.Log
6+
import androidx.core.graphics.drawable.toDrawable
7+
import androidx.core.net.toUri
8+
import com.facebook.common.executors.CallerThreadExecutor
9+
import com.facebook.common.references.CloseableReference
10+
import com.facebook.datasource.BaseDataSubscriber
11+
import com.facebook.datasource.DataSource
12+
import com.facebook.drawee.backends.pipeline.Fresco
13+
import com.facebook.imagepipeline.image.CloseableImage
14+
import com.facebook.imagepipeline.image.CloseableStaticBitmap
15+
import com.facebook.imagepipeline.request.ImageRequestBuilder
16+
17+
internal fun loadTabImage(
18+
context: Context,
19+
uri: String,
20+
onLoaded: (Drawable) -> Unit,
21+
) {
22+
val source = resolveTabImageSource(context, uri) ?: return
23+
val finalUri =
24+
when (source) {
25+
is RNSImageSource.DrawableRes -> {
26+
"res://${context.packageName}/${source.resId}".toUri()
27+
}
28+
is RNSImageSource.UriString -> {
29+
source.uri.toUri()
30+
}
31+
}
32+
33+
val imageRequest =
34+
ImageRequestBuilder
35+
.newBuilderWithSource(finalUri)
36+
.build()
37+
38+
val dataSource = Fresco.getImagePipeline().fetchDecodedImage(imageRequest, context)
39+
dataSource.subscribe(
40+
object : BaseDataSubscriber<CloseableReference<CloseableImage>>() {
41+
override fun onNewResultImpl(dataSource: DataSource<CloseableReference<CloseableImage>?>) {
42+
if (!dataSource.isFinished) return
43+
val imageReference = dataSource.result ?: return
44+
val closeableImage = imageReference.get()
45+
46+
if (closeableImage is CloseableStaticBitmap) {
47+
val bitmap = closeableImage.underlyingBitmap
48+
val drawable = bitmap.toDrawable(context.resources)
49+
onLoaded(drawable)
50+
}
51+
52+
imageReference.close()
53+
}
54+
55+
override fun onFailureImpl(dataSource: DataSource<CloseableReference<CloseableImage>?>) {
56+
Log.e("[RNScreens]", "Error loading image: $uri", dataSource.failureCause)
57+
}
58+
},
59+
CallerThreadExecutor.getInstance(),
60+
)
61+
}
62+
63+
private fun resolveTabImageSource(
64+
context: Context,
65+
uri: String,
66+
): RNSImageSource? {
67+
// In release builds, assets are coming with bundle and we need to work with resource id.
68+
// In debug, metro is responsible for handling assets via http.
69+
// At the moment, we're supporting images (drawable) and SVG icons (raw).
70+
// For any other type, we may consider adding a support in the future if needed.
71+
if (uri.startsWith("_")) {
72+
val drawableResId = context.resources.getIdentifier(uri, "drawable", context.packageName)
73+
if (drawableResId != 0) {
74+
return RNSImageSource.DrawableRes(drawableResId)
75+
}
76+
val rawResId = context.resources.getIdentifier(uri, "raw", context.packageName)
77+
if (rawResId != 0) {
78+
return RNSImageSource.DrawableRes(rawResId)
79+
}
80+
Log.e("[RNScreens]", "Resource not found in drawable or raw: $uri")
81+
return null
82+
}
83+
84+
// If asset isn't included in android source directories and we're loading it from given path.
85+
return RNSImageSource.UriString(uri)
86+
}
87+
88+
private sealed class RNSImageSource {
89+
data class DrawableRes(
90+
val resId: Int,
91+
) : RNSImageSource()
92+
93+
data class UriString(
94+
val uri: String,
95+
) : RNSImageSource()
96+
}

0 commit comments

Comments
 (0)