mirror of
https://github.com/austinried/subtracks.git
synced 2026-11-11 15:29:15 +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,
|
colorFilter: ColorFilter? = null,
|
||||||
) {
|
) {
|
||||||
val context = LocalPlatformContext.current
|
val context = LocalPlatformContext.current
|
||||||
var failed by remember(ref, thumbnailRef) { mutableStateOf(false) }
|
val refKey = ref?.cacheKey
|
||||||
var thumbnailLoaded by remember(ref, thumbnailRef) { mutableStateOf(false) }
|
val thumbnailKey = thumbnailRef?.cacheKey
|
||||||
var thumbnailRatio by remember(ref, thumbnailRef) { mutableStateOf(thumbnailRef?.cacheKey?.let(ArtworkRatioCache::get)) }
|
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 =
|
val letterScheme =
|
||||||
if (showPlaceholder || (failed && !thumbnailLoaded)) {
|
if (showPlaceholder || (failed && !thumbnailLoaded)) {
|
||||||
remember(name) { artworkColorsFromSeed(placeholderSeed(name)).scheme }
|
remember(name) { artworkColorsFromSeed(placeholderSeed(name)).scheme }
|
||||||
@@ -102,7 +104,7 @@ fun CoverArt(
|
|||||||
.background(letterScheme?.primaryContainer ?: MaterialTheme.colorScheme.surfaceVariant)
|
.background(letterScheme?.primaryContainer ?: MaterialTheme.colorScheme.surfaceVariant)
|
||||||
|
|
||||||
if (square) {
|
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)) {
|
Box(modifier.then(frameModifier)) {
|
||||||
CoverArtContent(
|
CoverArtContent(
|
||||||
ref = ref,
|
ref = ref,
|
||||||
@@ -147,7 +149,7 @@ fun CoverArt(
|
|||||||
val painter =
|
val painter =
|
||||||
rememberAsyncImagePainter(
|
rememberAsyncImagePainter(
|
||||||
model =
|
model =
|
||||||
remember(ref, thumbnailRef, target) {
|
remember(refKey, thumbnailKey, target) {
|
||||||
ref?.let {
|
ref?.let {
|
||||||
imageRequest(
|
imageRequest(
|
||||||
context,
|
context,
|
||||||
@@ -231,7 +233,7 @@ private fun BoxScope.CoverArtContent(
|
|||||||
}
|
}
|
||||||
if (thumbnailRef != null && thumbnailRef != ref) {
|
if (thumbnailRef != null && thumbnailRef != ref) {
|
||||||
AsyncImage(
|
AsyncImage(
|
||||||
model = remember(thumbnailRef) { imageRequest(context, thumbnailRef, crossfade = false) },
|
model = remember(thumbnailRef.cacheKey) { imageRequest(context, thumbnailRef, crossfade = false) },
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
contentScale = contentScale,
|
contentScale = contentScale,
|
||||||
colorFilter = colorFilter,
|
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