diff --git a/buildSrc/src/main/java/dev/chrisbanes/accompanist/buildsrc/dependencies.kt b/buildSrc/src/main/java/dev/chrisbanes/accompanist/buildsrc/dependencies.kt index 5f8d0f3f2..a1bea9d54 100644 --- a/buildSrc/src/main/java/dev/chrisbanes/accompanist/buildsrc/dependencies.kt +++ b/buildSrc/src/main/java/dev/chrisbanes/accompanist/buildsrc/dependencies.kt @@ -80,4 +80,5 @@ object Libs { const val coil = "io.coil-kt:coil:1.0.0-rc1" const val truth = "com.google.truth:truth:1.0.1" + const val mockk = "io.mockk:mockk-android:1.10.0" } diff --git a/coil/api/coil.api b/coil/api/coil.api index e2da7e657..e1dd4ba5a 100644 --- a/coil/api/coil.api +++ b/coil/api/coil.api @@ -1,8 +1,8 @@ public final class dev/chrisbanes/accompanist/coil/CoilImage { - public static final fun CoilImage (Lcoil/request/ImageRequest;Landroidx/compose/ui/Modifier;Landroidx/compose/ui/Alignment;Landroidx/compose/ui/layout/ContentScale;Landroidx/compose/ui/graphics/ColorFilter;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;Landroidx/compose/runtime/Composer;II)V - public static final fun CoilImage (Ljava/lang/Object;Landroidx/compose/ui/Modifier;Landroidx/compose/ui/Alignment;Landroidx/compose/ui/layout/ContentScale;Landroidx/compose/ui/graphics/ColorFilter;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;Landroidx/compose/runtime/Composer;II)V - public static final fun CoilImageWithCrossfade (Lcoil/request/ImageRequest;Landroidx/compose/ui/Modifier;Landroidx/compose/ui/Alignment;Landroidx/compose/ui/layout/ContentScale;ILkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;Landroidx/compose/runtime/Composer;II)V - public static final fun CoilImageWithCrossfade (Ljava/lang/Object;Landroidx/compose/ui/Modifier;Landroidx/compose/ui/Alignment;Landroidx/compose/ui/layout/ContentScale;ILkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;Landroidx/compose/runtime/Composer;II)V + public static final fun CoilImage (Lcoil/request/ImageRequest;Landroidx/compose/ui/Modifier;Landroidx/compose/ui/Alignment;Landroidx/compose/ui/layout/ContentScale;Landroidx/compose/ui/graphics/ColorFilter;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function2;Lcoil/ImageLoader;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;Landroidx/compose/runtime/Composer;II)V + public static final fun CoilImage (Ljava/lang/Object;Landroidx/compose/ui/Modifier;Landroidx/compose/ui/Alignment;Landroidx/compose/ui/layout/ContentScale;Landroidx/compose/ui/graphics/ColorFilter;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function2;Lcoil/ImageLoader;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;Landroidx/compose/runtime/Composer;II)V + public static final fun CoilImageWithCrossfade (Lcoil/request/ImageRequest;Landroidx/compose/ui/Modifier;Landroidx/compose/ui/Alignment;Landroidx/compose/ui/layout/ContentScale;ILkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function2;Lcoil/ImageLoader;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;Landroidx/compose/runtime/Composer;II)V + public static final fun CoilImageWithCrossfade (Ljava/lang/Object;Landroidx/compose/ui/Modifier;Landroidx/compose/ui/Alignment;Landroidx/compose/ui/layout/ContentScale;ILkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function2;Lcoil/ImageLoader;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;Landroidx/compose/runtime/Composer;II)V } public final class dev/chrisbanes/accompanist/coil/ErrorResult : dev/chrisbanes/accompanist/coil/RequestResult { diff --git a/coil/build.gradle b/coil/build.gradle index a284aed77..1842d93f5 100644 --- a/coil/build.gradle +++ b/coil/build.gradle @@ -91,6 +91,7 @@ dependencies { androidTestImplementation Libs.junit androidTestImplementation Libs.truth + androidTestImplementation Libs.mockk androidTestImplementation Libs.Coroutines.test diff --git a/coil/src/androidTest/java/dev/chrisbanes/accompanist/coil/CoilTest.kt b/coil/src/androidTest/java/dev/chrisbanes/accompanist/coil/CoilTest.kt index 5b9bccfc6..5000222ca 100644 --- a/coil/src/androidTest/java/dev/chrisbanes/accompanist/coil/CoilTest.kt +++ b/coil/src/androidTest/java/dev/chrisbanes/accompanist/coil/CoilTest.kt @@ -31,6 +31,7 @@ import androidx.compose.ui.unit.dp import androidx.core.net.toUri import androidx.test.filters.LargeTest import androidx.test.filters.SdkSuppress +import androidx.test.platform.app.InstrumentationRegistry import androidx.ui.test.assertHeightIsAtLeast import androidx.ui.test.assertHeightIsEqualTo import androidx.ui.test.assertIsDisplayed @@ -42,10 +43,15 @@ import androidx.ui.test.createComposeRule import androidx.ui.test.onNodeWithTag import androidx.ui.test.onNodeWithText import androidx.ui.test.runOnIdle +import coil.EventListener +import coil.ImageLoader +import coil.annotation.ExperimentalCoilApi import coil.request.CachePolicy import coil.request.ImageRequest import com.google.common.truth.Truth.assertThat import dev.chrisbanes.accompanist.coil.test.R +import io.mockk.mockk +import io.mockk.verify import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.MutableStateFlow @@ -137,6 +143,35 @@ class CoilTest { .assertPixels { Color.Red } } + @OptIn(ExperimentalCoilApi::class) + @Test + fun basicLoad_customImageLoader() { + val context = InstrumentationRegistry.getInstrumentation().targetContext + val latch = CountDownLatch(1) + + // Build a custom ImageLoader with a mocked EventListener + val eventListener = mockk(relaxed = true) + val imageLoader = ImageLoader.Builder(context) + .eventListener(eventListener) + .build() + + composeTestRule.setContent { + CoilImage( + data = resourceUri(R.drawable.red_rectangle), + modifier = Modifier.preferredSize(128.dp, 128.dp), + imageLoader = imageLoader, + onRequestCompleted = { latch.countDown() } + ) + } + + // Wait for the onRequestCompleted to release the latch + latch.await(5, TimeUnit.SECONDS) + + // Verify that our eventListener was invoked + verify(atLeast = 1) { eventListener.fetchStart(any(), any(), any()) } + verify(atLeast = 1) { eventListener.fetchEnd(any(), any(), any(), any()) } + } + @OptIn(ExperimentalCoroutinesApi::class) @Test @SdkSuppress(minSdkVersion = 26) // captureToBitmap is SDK 26+ diff --git a/coil/src/main/java/dev/chrisbanes/accompanist/coil/Coil.kt b/coil/src/main/java/dev/chrisbanes/accompanist/coil/Coil.kt index a957192ee..7a4da6068 100644 --- a/coil/src/main/java/dev/chrisbanes/accompanist/coil/Coil.kt +++ b/coil/src/main/java/dev/chrisbanes/accompanist/coil/Coil.kt @@ -44,6 +44,7 @@ import androidx.compose.ui.platform.ContextAmbient import androidx.compose.ui.unit.IntSize import androidx.core.graphics.drawable.toBitmap import coil.Coil +import coil.ImageLoader import coil.decode.DataSource import coil.request.ImageRequest import coil.request.ImageResult @@ -64,6 +65,8 @@ import coil.request.ImageResult * @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 imageLoader The [ImageLoader] to use when requesting the image. Defaults to [Coil]'s + * default image loader. * @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. @@ -78,6 +81,7 @@ fun CoilImage( getSuccessPainter: @Composable ((SuccessResult) -> Painter)? = null, getFailurePainter: @Composable ((ErrorResult) -> Painter?)? = null, loading: @Composable (() -> Unit)? = null, + imageLoader: ImageLoader = Coil.imageLoader(ContextAmbient.current), shouldRefetchOnSizeChange: (currentResult: RequestResult, size: IntSize) -> Boolean = defaultRefetchOnSizeChangeLambda, onRequestCompleted: (RequestResult) -> Unit = emptySuccessLambda ) { @@ -99,6 +103,7 @@ fun CoilImage( getSuccessPainter = getSuccessPainter, getFailurePainter = getFailurePainter, loading = loading, + imageLoader = imageLoader, shouldRefetchOnSizeChange = shouldRefetchOnSizeChange, modifier = modifier ) @@ -121,6 +126,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 imageLoader The [ImageLoader] to use when requesting the image. Defaults to [Coil]'s + * default image loader. * @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. @@ -135,6 +142,7 @@ fun CoilImage( getSuccessPainter: @Composable ((SuccessResult) -> Painter)? = null, getFailurePainter: @Composable ((ErrorResult) -> Painter?)? = null, loading: @Composable (() -> Unit)? = null, + imageLoader: ImageLoader = Coil.imageLoader(ContextAmbient.current), shouldRefetchOnSizeChange: (currentResult: RequestResult, size: IntSize) -> Boolean = defaultRefetchOnSizeChangeLambda, onRequestCompleted: (RequestResult) -> Unit = emptySuccessLambda ) { @@ -153,7 +161,9 @@ fun CoilImage( val callback = remember { mutableStateOf(onRequestCompleted, referentialEqualityPolicy()) } callback.value = onRequestCompleted - val requestActor = remember(request) { CoilRequestActor(request) } + val requestActor = remember(imageLoader, request) { + CoilRequestActor(imageLoader, request) + } launchInComposition(requestActor) { // Launch the Actor @@ -234,6 +244,7 @@ private const val UNSPECIFIED = -1 private data class MutableRef(var value: T) private fun CoilRequestActor( + imageLoader: ImageLoader, request: ImageRequest ) = RequestActor { size -> when { @@ -256,7 +267,7 @@ private fun CoilRequestActor( } }?.let { transformedRequest -> // Now execute the request in Coil... - Coil.imageLoader(transformedRequest.context) + imageLoader .execute(transformedRequest) .toResult(size) .also { diff --git a/coil/src/main/java/dev/chrisbanes/accompanist/coil/Crossfade.kt b/coil/src/main/java/dev/chrisbanes/accompanist/coil/Crossfade.kt index 335d0982b..992bbd61b 100644 --- a/coil/src/main/java/dev/chrisbanes/accompanist/coil/Crossfade.kt +++ b/coil/src/main/java/dev/chrisbanes/accompanist/coil/Crossfade.kt @@ -42,11 +42,13 @@ import androidx.compose.ui.graphics.drawscope.drawCanvas import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.AnimationClockAmbient +import androidx.compose.ui.platform.ContextAmbient import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.toSize import androidx.core.util.Pools import coil.Coil +import coil.ImageLoader import coil.decode.DataSource import coil.request.ImageRequest @@ -69,6 +71,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 imageLoader The [ImageLoader] to use when requesting the image. Defaults to [Coil]'s + * default image loader. * @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. @@ -82,6 +86,7 @@ fun CoilImageWithCrossfade( crossfadeDuration: Int = DefaultTransitionDuration, getFailurePainter: @Composable ((ErrorResult) -> Painter?)? = null, loading: @Composable (() -> Unit)? = null, + imageLoader: ImageLoader = Coil.imageLoader(ContextAmbient.current), shouldRefetchOnSizeChange: (currentResult: RequestResult, size: IntSize) -> Boolean = defaultRefetchOnSizeChangeLambda, onRequestCompleted: (RequestResult) -> Unit = emptySuccessLambda ) { @@ -93,6 +98,7 @@ fun CoilImageWithCrossfade( getFailurePainter = getFailurePainter, loading = loading, modifier = modifier, + imageLoader = imageLoader, shouldRefetchOnSizeChange = shouldRefetchOnSizeChange, onRequestCompleted = onRequestCompleted ) @@ -116,6 +122,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 imageLoader The [ImageLoader] to use when requesting the image. Defaults to [Coil]'s + * default image loader. * @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. @@ -129,6 +137,7 @@ fun CoilImageWithCrossfade( crossfadeDuration: Int = DefaultTransitionDuration, getFailurePainter: @Composable ((ErrorResult) -> Painter?)? = null, loading: @Composable (() -> Unit)? = null, + imageLoader: ImageLoader = Coil.imageLoader(ContextAmbient.current), shouldRefetchOnSizeChange: (currentResult: RequestResult, size: IntSize) -> Boolean = defaultRefetchOnSizeChangeLambda, onRequestCompleted: (RequestResult) -> Unit = emptySuccessLambda ) { @@ -139,6 +148,7 @@ fun CoilImageWithCrossfade( getSuccessPainter = { crossfadePainter(it, durationMs = crossfadeDuration) }, getFailurePainter = getFailurePainter, loading = loading, + imageLoader = imageLoader, shouldRefetchOnSizeChange = shouldRefetchOnSizeChange, modifier = modifier, onRequestCompleted = onRequestCompleted