mirror of
https://github.com/austinried/subtracks.git
synced 2026-11-10 15:11:58 +01:00
artwork: keep the cover request stable across recompositions
CoverArt keyed its remember blocks on the whole CoverArtRef, whose url is freshly salted on every call. A recomposition therefore built a new ImageRequest, Coil restarted the load and dropped the current drawable, so the image flashed back to the letter placeholder before resolving from cache. Key the request and the load-state remembers on the ref's stable cacheKey instead.
This commit is contained in:
@@ -87,9 +87,11 @@ fun CoverArt(
|
||||
colorFilter: ColorFilter? = null,
|
||||
) {
|
||||
val context = LocalPlatformContext.current
|
||||
var failed by remember(ref, thumbnailRef) { mutableStateOf(false) }
|
||||
var thumbnailLoaded by remember(ref, thumbnailRef) { mutableStateOf(false) }
|
||||
var thumbnailRatio by remember(ref, thumbnailRef) { mutableStateOf(thumbnailRef?.cacheKey?.let(ArtworkRatioCache::get)) }
|
||||
val refKey = ref?.cacheKey
|
||||
val thumbnailKey = thumbnailRef?.cacheKey
|
||||
var failed by remember(refKey, thumbnailKey) { mutableStateOf(false) }
|
||||
var thumbnailLoaded by remember(refKey, thumbnailKey) { mutableStateOf(false) }
|
||||
var thumbnailRatio by remember(refKey, thumbnailKey) { mutableStateOf(thumbnailKey?.let(ArtworkRatioCache::get)) }
|
||||
val letterScheme =
|
||||
if (showPlaceholder || (failed && !thumbnailLoaded)) {
|
||||
remember(name) { artworkColorsFromSeed(placeholderSeed(name)).scheme }
|
||||
@@ -102,7 +104,7 @@ fun CoverArt(
|
||||
.background(letterScheme?.primaryContainer ?: MaterialTheme.colorScheme.surfaceVariant)
|
||||
|
||||
if (square) {
|
||||
val model = remember(ref, thumbnailRef) { ref?.let { imageRequest(context, it, crossfade = true) } }
|
||||
val model = remember(refKey, thumbnailKey) { ref?.let { imageRequest(context, it, crossfade = true) } }
|
||||
Box(modifier.then(frameModifier)) {
|
||||
CoverArtContent(
|
||||
ref = ref,
|
||||
@@ -147,7 +149,7 @@ fun CoverArt(
|
||||
val painter =
|
||||
rememberAsyncImagePainter(
|
||||
model =
|
||||
remember(ref, thumbnailRef, target) {
|
||||
remember(refKey, thumbnailKey, target) {
|
||||
ref?.let {
|
||||
imageRequest(
|
||||
context,
|
||||
@@ -231,7 +233,7 @@ private fun BoxScope.CoverArtContent(
|
||||
}
|
||||
if (thumbnailRef != null && thumbnailRef != ref) {
|
||||
AsyncImage(
|
||||
model = remember(thumbnailRef) { imageRequest(context, thumbnailRef, crossfade = false) },
|
||||
model = remember(thumbnailRef.cacheKey) { imageRequest(context, thumbnailRef, crossfade = false) },
|
||||
contentDescription = null,
|
||||
contentScale = contentScale,
|
||||
colorFilter = colorFilter,
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
package com.subtracks.ui.components
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Color
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.test.junit4.v2.createAndroidComposeRule
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.test.core.app.ApplicationProvider
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import coil3.ImageLoader
|
||||
import coil3.SingletonImageLoader
|
||||
import coil3.annotation.DelicateCoilApi
|
||||
import coil3.asImage
|
||||
import coil3.test.FakeImageLoaderEngine
|
||||
import com.subtracks.data.model.CoverArtRef
|
||||
import org.junit.After
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.robolectric.annotation.GraphicsMode
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
|
||||
@OptIn(DelicateCoilApi::class)
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
@GraphicsMode(GraphicsMode.Mode.NATIVE)
|
||||
class CoverArtReloadTest {
|
||||
@get:Rule
|
||||
val composeRule = createAndroidComposeRule<ComponentActivity>()
|
||||
|
||||
private val requests = AtomicInteger(0)
|
||||
|
||||
@Before
|
||||
fun installImageLoader() {
|
||||
SingletonImageLoader.reset()
|
||||
val image = Bitmap.createBitmap(8, 8, Bitmap.Config.ARGB_8888).apply { eraseColor(Color.RED) }.asImage()
|
||||
val engine =
|
||||
FakeImageLoaderEngine
|
||||
.Builder()
|
||||
.requestTransformer {
|
||||
requests.incrementAndGet()
|
||||
it
|
||||
}.default(image)
|
||||
.build()
|
||||
SingletonImageLoader.setUnsafe(
|
||||
ImageLoader
|
||||
.Builder(ApplicationProvider.getApplicationContext<Context>())
|
||||
.components { add(engine) }
|
||||
.build(),
|
||||
)
|
||||
}
|
||||
|
||||
@After
|
||||
fun resetImageLoader() {
|
||||
SingletonImageLoader.reset()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun aNewSaltedUrlForTheSameArtworkDoesNotRestartTheLoad() {
|
||||
val refState = mutableStateOf(CoverArtRef(url = "http://host/art?id=1&s=one", cacheKey = "1:art:true"))
|
||||
composeRule.setContent {
|
||||
CoverArt(ref = refState.value, name = "Kid A", modifier = Modifier.size(64.dp))
|
||||
}
|
||||
composeRule.waitUntil(timeoutMillis = 5_000) { requests.get() == 1 }
|
||||
|
||||
refState.value = CoverArtRef(url = "http://host/art?id=1&s=two", cacheKey = "1:art:true")
|
||||
composeRule.waitForIdle()
|
||||
composeRule.mainClock.advanceTimeBy(500)
|
||||
composeRule.waitForIdle()
|
||||
|
||||
assertEquals(1, requests.get())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user