diff --git a/Demo/swiftui/src/commonMain/kotlin/com/pureswift/swiftui/ImageCache.kt b/Demo/swiftui/src/commonMain/kotlin/com/pureswift/swiftui/ImageCache.kt new file mode 100644 index 0000000..2a06c1b --- /dev/null +++ b/Demo/swiftui/src/commonMain/kotlin/com/pureswift/swiftui/ImageCache.kt @@ -0,0 +1,28 @@ +package com.pureswift.swiftui + +import androidx.compose.ui.graphics.ImageBitmap +import java.util.Collections + +// A small process-wide cache of decoded images, keyed by URL. Without it, a +// re-keyed AsyncImage (URL change, a row scrolled off then back, a re-navigated +// screen) drops its bitmap and re-fetches over the network — the spinner flashes +// every time. A hit returns the decoded bitmap synchronously, so a URL seen +// before renders with no network and no spinner. +internal object ImageCache { + + private const val MAX_ENTRIES = 64 + + // access-ordered so the least-recently-used entry is evicted first + private val store = Collections.synchronizedMap( + object : LinkedHashMap(16, 0.75f, true) { + override fun removeEldestEntry(eldest: Map.Entry): Boolean = + size > MAX_ENTRIES + } + ) + + fun get(url: String): ImageBitmap? = store[url] + + fun put(url: String, bitmap: ImageBitmap) { + store[url] = bitmap + } +} diff --git a/Demo/swiftui/src/commonMain/kotlin/com/pureswift/swiftui/Render.kt b/Demo/swiftui/src/commonMain/kotlin/com/pureswift/swiftui/Render.kt index e2be34d..a577803 100644 --- a/Demo/swiftui/src/commonMain/kotlin/com/pureswift/swiftui/Render.kt +++ b/Demo/swiftui/src/commonMain/kotlin/com/pureswift/swiftui/Render.kt @@ -1314,11 +1314,19 @@ private fun RenderAsyncImage(node: ViewNode) { Text("[image]", modifier = node.composeModifiers()) return } - var bitmap by remember(url) { mutableStateOf(null) } + // Seed from the cache synchronously: a URL already fetched renders on the + // first frame with no spinner. Only a miss touches the network. + var bitmap by remember(url) { mutableStateOf(ImageCache.get(url)) } var failed by remember(url) { mutableStateOf(false) } LaunchedEffect(url) { + if (bitmap != null) return@LaunchedEffect val loaded = loadRemoteImage(url) - if (loaded != null) bitmap = loaded else failed = true + if (loaded != null) { + ImageCache.put(url, loaded) + bitmap = loaded + } else { + failed = true + } } val image = bitmap when {