Skip to content

Commit

Permalink
Stop refetching on every size change by default
Browse files Browse the repository at this point in the history
Currently we invoke Coil on every size change, which is correct
but a lot of wasted work most of the time. We no longer re-fetch
on every size change. If developers want to customize this, they
can use the new `shouldRefetchOnSizeChange` lambda parameter
to allow re-fetching as they see fit.
  • Loading branch information
Chris Banes committed Jul 9, 2020
1 parent 50c4090 commit 17635e3
Show file tree
Hide file tree
Showing 3 changed files with 66 additions and 8 deletions.
Expand Up @@ -48,6 +48,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.test.TestCoroutineDispatcher
import kotlinx.coroutines.test.runBlockingTest
import kotlinx.coroutines.withTimeoutOrNull
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
Expand Down Expand Up @@ -175,6 +176,39 @@ class CoilTest {
loadCompleteSignal.close()
}

@OptIn(ExperimentalCoroutinesApi::class)
@Test
fun basicLoad_changeSize() {
val loadCompleteSignal = Channel<Unit>(Channel.UNLIMITED)
val sizeFlow = MutableStateFlow(128.dp)

composeTestRule.setContent {
val size = sizeFlow.collectAsState()
CoilImage(
data = resourceUri(R.drawable.blue_rectangle),
modifier = Modifier.preferredSize(size.value).testTag(CoilTestTags.Image),
onRequestCompleted = { loadCompleteSignal.offer(Unit) }
)
}

// Await the first load
runBlocking {
loadCompleteSignal.receive()
}

// Now change the size
sizeFlow.value = 256.dp

// Await the potential second load (which shouldn't come)
runBlocking {
val result = withTimeoutOrNull(3000) { loadCompleteSignal.receive() }
assertThat(result).isNull()
}

// Close the signal channel
loadCompleteSignal.close()
}

@Test
@SdkSuppress(minSdkVersion = 26) // captureToBitmap is SDK 26+
fun customGetPainter() {
Expand Down
32 changes: 24 additions & 8 deletions coil/src/main/java/dev/chrisbanes/accompanist/coil/Coil.kt
Expand Up @@ -58,6 +58,8 @@ import coil.request.GetRequestBuilder
* @param getFailurePainter Optional builder for the [Painter] to be used to draw the failure
* loading result. Passing in `null` will result in falling back to the default [Painter].
* @param loading Content to be displayed when the request is in progress.
* @param shouldRefetchOnSizeChange Lambda which will be invoked when the size changes, allowing
* optional re-fetching of the image. Return true to re-fetch the image.
* @param onRequestCompleted Listener which will be called when the loading request has finished.
*/
@Composable
Expand All @@ -70,6 +72,7 @@ fun CoilImage(
getSuccessPainter: @Composable ((SuccessResult) -> Painter)? = null,
getFailurePainter: @Composable ((ErrorResult) -> Painter?)? = null,
loading: @Composable (() -> Unit)? = null,
shouldRefetchOnSizeChange: (currentResult: RequestResult, size: IntSize) -> Boolean = defaultRefetchOnSizeChangeLambda,
onRequestCompleted: (RequestResult) -> Unit = emptySuccessLambda
) {
CoilImage(
Expand All @@ -89,6 +92,7 @@ fun CoilImage(
getSuccessPainter = getSuccessPainter,
getFailurePainter = getFailurePainter,
loading = loading,
shouldRefetchOnSizeChange = shouldRefetchOnSizeChange,
modifier = modifier
)
}
Expand All @@ -110,6 +114,8 @@ fun CoilImage(
* @param getFailurePainter Optional builder for the [Painter] to be used to draw the failure
* loading result. Passing in `null` will result in falling back to the default [Painter].
* @param loading Content to be displayed when the request is in progress.
* @param shouldRefetchOnSizeChange Lambda which will be invoked when the size changes, allowing
* optional re-fetching of the image. Return true to re-fetch the image.
* @param onRequestCompleted Listener which will be called when the loading request has finished.
*/
@Composable
Expand All @@ -122,9 +128,16 @@ fun CoilImage(
getSuccessPainter: @Composable ((SuccessResult) -> Painter)? = null,
getFailurePainter: @Composable ((ErrorResult) -> Painter?)? = null,
loading: @Composable (() -> Unit)? = null,
shouldRefetchOnSizeChange: (currentResult: RequestResult, size: IntSize) -> Boolean = defaultRefetchOnSizeChangeLambda,
onRequestCompleted: (RequestResult) -> Unit = emptySuccessLambda
) {
var result by stateFor<RequestResult?>(request.data) { null }
// GetRequest does not support object equality (as of Coil v0.10.1) so we can not key any
// remember calls using the request itself. For now we just use the [data] field, but
// ideally this should use [request] to track changes in size, transformations, etc too.
// See: https://github.com/coil-kt/coil/issues/405
val requestKey = request.data ?: request

var result by stateFor<RequestResult?>(requestKey) { null }

// This may look a little weird, but allows the launchInComposition callback to always
// invoke the last provided [onRequestCompleted].
Expand All @@ -139,11 +152,7 @@ fun CoilImage(
val callback = state { onRequestCompleted }
callback.value = onRequestCompleted

// GetRequest does not support object equality (as of Coil v0.10.1) so we can not key the
// remember() using the request itself. For now we just use the [data] field, but
// ideally this should use [request] to track changes in size, transformations, etc too.
// See: https://github.com/coil-kt/coil/issues/405
val requestActor = remember(request.data) { CoilRequestActor(request) }
val requestActor = remember(requestKey) { CoilRequestActor(request) }

launchInComposition(requestActor) {
// Launch the Actor
Expand Down Expand Up @@ -176,9 +185,14 @@ fun CoilImage(
else -> null
}

// Update the modifier, so that we listen for size changes to invoke the actor
val mod = modifier.onSizeChanged { size ->
// When the size changes, send it to the request actor
requestActor.send(size)
val r = result
if (r == null || shouldRefetchOnSizeChange(r, size)) {
// If we don't have a result yet, or the size has changed and we should re-fetch,
// send it to the request actor
requestActor.send(size)
}
}

if (painter == null) {
Expand Down Expand Up @@ -295,6 +309,8 @@ internal fun defaultSuccessPainterGetter(result: SuccessResult): Painter {

internal val emptySuccessLambda: (RequestResult) -> Unit = {}

internal val defaultRefetchOnSizeChangeLambda: (RequestResult, IntSize) -> Boolean = { _, _ -> false }

internal fun Drawable.toImageAsset(fallbackSize: IntSize = IntSize.Zero): ImageAsset {
return toBitmap(
width = if (intrinsicWidth > 0) intrinsicWidth else fallbackSize.width,
Expand Down
Expand Up @@ -67,6 +67,8 @@ private const val DefaultTransitionDuration = 1000
* @param crossfadeDuration The duration of the crossfade animation in milliseconds.
* @param getFailurePainter Optional builder for the [Painter] to be used to draw the failure
* loading result. Passing in `null` will result in falling back to the default [Painter].
* @param shouldRefetchOnSizeChange Lambda which will be invoked when the size changes, allowing
* optional re-fetching of the image. Return true to re-fetch the image.
* @param onRequestCompleted Listener which will be called when the loading request has finished.
*/
@Composable
Expand All @@ -78,6 +80,7 @@ fun CoilImageWithCrossfade(
crossfadeDuration: Int = DefaultTransitionDuration,
getFailurePainter: @Composable ((ErrorResult) -> Painter?)? = null,
loading: @Composable (() -> Unit)? = null,
shouldRefetchOnSizeChange: (currentResult: RequestResult, size: IntSize) -> Boolean = defaultRefetchOnSizeChangeLambda,
onRequestCompleted: (RequestResult) -> Unit = emptySuccessLambda
) {
CoilImage(
Expand All @@ -88,6 +91,7 @@ fun CoilImageWithCrossfade(
getFailurePainter = getFailurePainter,
loading = loading,
modifier = modifier,
shouldRefetchOnSizeChange = shouldRefetchOnSizeChange,
onRequestCompleted = onRequestCompleted
)
}
Expand All @@ -109,6 +113,8 @@ fun CoilImageWithCrossfade(
* @param crossfadeDuration The duration of the crossfade animation in milliseconds.
* @param getFailurePainter Optional builder for the [Painter] to be used to draw the failure
* loading result. Passing in `null` will result in falling back to the default [Painter].
* @param shouldRefetchOnSizeChange Lambda which will be invoked when the size changes, allowing
* optional re-fetching of the image. Return true to re-fetch the image.
* @param onRequestCompleted Listener which will be called when the loading request has finished.
*/
@Composable
Expand All @@ -120,6 +126,7 @@ fun CoilImageWithCrossfade(
crossfadeDuration: Int = DefaultTransitionDuration,
getFailurePainter: @Composable ((ErrorResult) -> Painter?)? = null,
loading: @Composable (() -> Unit)? = null,
shouldRefetchOnSizeChange: (currentResult: RequestResult, size: IntSize) -> Boolean = defaultRefetchOnSizeChangeLambda,
onRequestCompleted: (RequestResult) -> Unit = emptySuccessLambda
) {
CoilImage(
Expand All @@ -129,6 +136,7 @@ fun CoilImageWithCrossfade(
getSuccessPainter = { crossfadePainter(it, durationMs = crossfadeDuration) },
getFailurePainter = getFailurePainter,
loading = loading,
shouldRefetchOnSizeChange = shouldRefetchOnSizeChange,
modifier = modifier,
onRequestCompleted = onRequestCompleted
)
Expand Down

0 comments on commit 17635e3

Please sign in to comment.