Skip to content

Commit

Permalink
Image Loading Core library (#96)
Browse files Browse the repository at this point in the history
* Extract common logic to new imageloading module
* Use Painters as the drawing primitive
* Add support for drawing drawables
* Add requestBuilder parameter to CoilImage
* Fix Coil package name
* Fix MaterialLoadingPainterWrapper
* Add GIF to sample
* Update docs
* Fix imageloading-core README
* Remove imageloading test dependencies
* Import BlendMode.toPorterDuffMode()
* Fix ImageLoad param order
* Add comment
* Update API
* Remove mockK
  • Loading branch information
chrisbanes committed Sep 25, 2020
1 parent c23db0c commit f905b05
Show file tree
Hide file tree
Showing 25 changed files with 1,108 additions and 547 deletions.
2 changes: 2 additions & 0 deletions .github/workflows/build.yml
Expand Up @@ -55,6 +55,8 @@ jobs:
runs-on: macOS-latest
needs: build
strategy:
# Allow tests to continue on other devices if they fail on one device.
fail-fast: false
matrix:
api-level: [23, 26, 29]
env:
Expand Down
Expand Up @@ -21,7 +21,7 @@ object Versions {
}

object Libs {
const val androidGradlePlugin = "com.android.tools.build:gradle:4.2.0-alpha11"
const val androidGradlePlugin = "com.android.tools.build:gradle:4.2.0-alpha12"

const val gradleMavenPublishPlugin = "com.vanniktech:gradle-maven-publish-plugin:0.13.0"

Expand Down Expand Up @@ -86,10 +86,13 @@ object Libs {
const val appcompat = "androidx.appcompat:appcompat:1.3.0-alpha02"
}

const val coil = "io.coil-kt:coil:1.0.0-rc3"
object Coil {
private const val version = "1.0.0-rc3"
const val coil = "io.coil-kt:coil:$version"
const val gif = "io.coil-kt:coil-gif:$version"
}

const val truth = "com.google.truth:truth:1.0.1"
const val mockk = "io.mockk:mockk-android:1.10.0"

const val mockWebServer = "com.squareup.okhttp3:mockwebserver:3.12.2"
}
18 changes: 9 additions & 9 deletions coil/README.md
Expand Up @@ -23,7 +23,7 @@ There is also a version of this function which accepts a Coil [`ImageRequest`](h

```kotlin
CoilImage(
request = GetRequest.Builder(ContextAmbient.current)
request = ImageRequest.Builder(ContextAmbient.current)
.data("https://loremflickr.com/300/300")
.transformations(CircleCropTransformation())
.build()
Expand Down Expand Up @@ -74,20 +74,24 @@ CoilImage(
data = "https://random.image",
) { imageState ->
when (imageState) {
is CoilImageState.Success -> {
is ImageLoadState.Success -> {
MaterialLoadingImage(
result = imageState,
fadeInEnabled = true,
fadeInDurationMs = 600,
)
}
is CoilImageState.Error -> /* TODO */
CoilImageState.Loading -> /* TODO */
CoilImageState.Empty -> /* TODO */
is ImageLoadState.Error -> /* TODO */
ImageLoadState.Loading -> /* TODO */
ImageLoadState.Empty -> /* TODO */
}
}
```

## GIFs

Accompanist Coil supports GIFs through Coil's own GIF support. Follow the [setup instructions](https://coil-kt.github.io/coil/gifs/) and it should just work.

## Download

```groovy
Expand All @@ -100,10 +104,6 @@ dependencies {
}
```

## Limitations

* Compose currently only supports static bitmap images, which means that we need to convert the resulting images to a `Bitmap`. This means that using things like Coil's [GIF support](https://coil-kt.github.io/coil/gifs/) will result in only the first frame being rendered, instead of animating.

Snapshots of the development version are available in [Sonatype's `snapshots` repository][snap]. These are updated on every commit.

### What's the goal of the library?
Expand Down
61 changes: 4 additions & 57 deletions coil/api/coil.api
@@ -1,65 +1,12 @@
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;ZLcoil/ImageLoader;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function2;Landroidx/compose/runtime/Composer;II)V
public static final fun CoilImage (Lcoil/request/ImageRequest;Landroidx/compose/ui/Modifier;Lcoil/ImageLoader;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function3;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;ZLcoil/ImageLoader;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function2;Landroidx/compose/runtime/Composer;II)V
public static final fun CoilImage (Ljava/lang/Object;Landroidx/compose/ui/Modifier;Lcoil/ImageLoader;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function3;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;ZLkotlin/jvm/functions/Function2;Lcoil/ImageLoader;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function2;Landroidx/compose/runtime/Composer;II)V
public static final fun CoilImage (Lcoil/request/ImageRequest;Landroidx/compose/ui/Modifier;Lkotlin/jvm/functions/Function2;Lcoil/ImageLoader;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function3;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;ZLkotlin/jvm/functions/Function2;Lcoil/ImageLoader;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function2;Landroidx/compose/runtime/Composer;II)V
public static final fun CoilImage (Ljava/lang/Object;Landroidx/compose/ui/Modifier;Lkotlin/jvm/functions/Function2;Lcoil/ImageLoader;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function3;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;ILcoil/ImageLoader;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function2;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;ILcoil/ImageLoader;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function2;Landroidx/compose/runtime/Composer;II)V
public static fun ErrorResult$annotations ()V
public static final fun MaterialLoadingImage (Landroidx/compose/ui/graphics/ImageAsset;Landroidx/compose/ui/Modifier;Landroidx/compose/ui/Alignment;Landroidx/compose/ui/layout/ContentScale;Landroidx/compose/ui/graphics/ColorFilter;Landroidx/compose/animation/core/AnimationClockObservable;ZILandroidx/compose/runtime/Composer;II)V
public static final fun MaterialLoadingImage (Ldev/chrisbanes/accompanist/coil/CoilImageState$Success;Landroidx/compose/ui/Modifier;Landroidx/compose/ui/Alignment;Landroidx/compose/ui/layout/ContentScale;Landroidx/compose/ui/graphics/ColorFilter;Landroidx/compose/animation/core/AnimationClockObservable;ZZILandroidx/compose/runtime/Composer;II)V
public static fun RequestResult$annotations ()V
public static fun SuccessResult$annotations ()V
}

public abstract class dev/chrisbanes/accompanist/coil/CoilImageState {
}

public final class dev/chrisbanes/accompanist/coil/CoilImageState$Empty : dev/chrisbanes/accompanist/coil/CoilImageState {
public static final field INSTANCE Ldev/chrisbanes/accompanist/coil/CoilImageState$Empty;
}

public final class dev/chrisbanes/accompanist/coil/CoilImageState$Error : dev/chrisbanes/accompanist/coil/CoilImageState {
public fun <init> (Landroidx/compose/ui/graphics/ImageAsset;Ljava/lang/Throwable;)V
public synthetic fun <init> (Lcoil/request/ErrorResult;JLkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun component1 ()Landroidx/compose/ui/graphics/ImageAsset;
public final fun component2 ()Ljava/lang/Throwable;
public final fun copy (Landroidx/compose/ui/graphics/ImageAsset;Ljava/lang/Throwable;)Ldev/chrisbanes/accompanist/coil/CoilImageState$Error;
public static synthetic fun copy$default (Ldev/chrisbanes/accompanist/coil/CoilImageState$Error;Landroidx/compose/ui/graphics/ImageAsset;Ljava/lang/Throwable;ILjava/lang/Object;)Ldev/chrisbanes/accompanist/coil/CoilImageState$Error;
public fun equals (Ljava/lang/Object;)Z
public final fun getImage ()Landroidx/compose/ui/graphics/ImageAsset;
public final fun getThrowable ()Ljava/lang/Throwable;
public fun hashCode ()I
public fun toString ()Ljava/lang/String;
}

public final class dev/chrisbanes/accompanist/coil/CoilImageState$Loading : dev/chrisbanes/accompanist/coil/CoilImageState {
public static final field INSTANCE Ldev/chrisbanes/accompanist/coil/CoilImageState$Loading;
}

public final class dev/chrisbanes/accompanist/coil/CoilImageState$Success : dev/chrisbanes/accompanist/coil/CoilImageState {
public fun <init> (Landroidx/compose/ui/graphics/ImageAsset;Lcoil/decode/DataSource;)V
public synthetic fun <init> (Lcoil/request/SuccessResult;JLkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun component1 ()Landroidx/compose/ui/graphics/ImageAsset;
public final fun component2 ()Lcoil/decode/DataSource;
public final fun copy (Landroidx/compose/ui/graphics/ImageAsset;Lcoil/decode/DataSource;)Ldev/chrisbanes/accompanist/coil/CoilImageState$Success;
public static synthetic fun copy$default (Ldev/chrisbanes/accompanist/coil/CoilImageState$Success;Landroidx/compose/ui/graphics/ImageAsset;Lcoil/decode/DataSource;ILjava/lang/Object;)Ldev/chrisbanes/accompanist/coil/CoilImageState$Success;
public fun equals (Ljava/lang/Object;)Z
public final fun getImage ()Landroidx/compose/ui/graphics/ImageAsset;
public final fun getSource ()Lcoil/decode/DataSource;
public fun hashCode ()I
public fun toString ()Ljava/lang/String;
}

public final class dev/chrisbanes/accompanist/coil/ImageLoadingColorMatrix : android/graphics/ColorMatrix {
public fun <init> ()V
public fun <init> (FFF)V
public synthetic fun <init> (FFFILkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun getAlphaFraction ()F
public final fun getBrightnessFraction ()F
public final fun getSaturationFraction ()F
public final fun setAlphaFraction (F)V
public final fun setBrightnessFraction (F)V
public final fun setSaturationFraction (F)V
}

11 changes: 3 additions & 8 deletions coil/build.gradle
Expand Up @@ -53,12 +53,6 @@ android {
}

packagingOptions {
def e = excludes
// AGP 4.1.0-alpha06 added an exclude which breaks Kotlin libraries. Remove the exclude
// until we upgrade to an AGP version with a fix
e.remove("/META-INF/*.kotlin_module")
excludes = e

// Some of the Coil META-INF files conflict with coroutines-test. Exclude them to enable
// our test APK to build (has no effect on our AARs)
excludes += "/META-INF/AL2.0"
Expand All @@ -79,7 +73,9 @@ afterEvaluate {
}

dependencies {
api Libs.coil
api project(':imageloading-core')

api Libs.Coil.coil

implementation Libs.AndroidX.coreKtx
implementation Libs.AndroidX.Compose.runtime
Expand All @@ -90,7 +86,6 @@ dependencies {

androidTestImplementation Libs.junit
androidTestImplementation Libs.truth
androidTestImplementation Libs.mockk

androidTestImplementation Libs.mockWebServer

Expand Down
Expand Up @@ -45,12 +45,13 @@ import androidx.ui.test.onNodeWithText
import coil.EventListener
import coil.ImageLoader
import coil.annotation.ExperimentalCoilApi
import coil.decode.Options
import coil.fetch.Fetcher
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 dev.chrisbanes.accompanist.imageloading.ImageLoadState
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.MutableStateFlow
Expand Down Expand Up @@ -94,8 +95,8 @@ class CoilTest {
}

@Test
fun onRequestCompleted() {
val results = ArrayList<CoilImageState>()
fun onRequestCompleted_fromImageRequest() {
val results = ArrayList<ImageLoadState>()
val latch = CountDownLatch(1)

composeTestRule.setContent {
Expand All @@ -115,7 +116,33 @@ class CoilTest {
composeTestRule.runOnIdle {
// And assert that we got a single successful result
assertThat(results).hasSize(1)
assertThat(results[0]).isInstanceOf(CoilImageState.Success::class.java)
assertThat(results[0]).isInstanceOf(ImageLoadState.Success::class.java)
}
}

@Test
fun onRequestCompleted_fromBuilder() {
val results = ArrayList<ImageLoadState>()
val latch = CountDownLatch(1)

composeTestRule.setContent {
CoilImage(
data = resourceUri(R.raw.sample),
requestBuilder = {
listener { _, _ -> latch.countDown() }
},
modifier = Modifier.preferredSize(128.dp, 128.dp),
onRequestCompleted = { results += it }
)
}

// Wait for the Coil request listener to release the latch
latch.await(5, TimeUnit.SECONDS)

composeTestRule.runOnIdle {
// And assert that we got a single successful result
assertThat(results).hasSize(1)
assertThat(results[0]).isInstanceOf(ImageLoadState.Success::class.java)
}
}

Expand Down Expand Up @@ -170,8 +197,15 @@ class CoilTest {
val context = InstrumentationRegistry.getInstrumentation().targetContext
val latch = CountDownLatch(1)

// Build a custom ImageLoader with a mocked EventListener
val eventListener = mockk<EventListener>(relaxed = true)
// Build a custom ImageLoader with a fake EventListener
val eventListener = object : EventListener {
var startCalled = 0
private set

override fun fetchStart(request: ImageRequest, fetcher: Fetcher<*>, options: Options) {
startCalled++
}
}
val imageLoader = ImageLoader.Builder(context)
.eventListener(eventListener)
.build()
Expand All @@ -189,8 +223,7 @@ class CoilTest {
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()) }
assertThat(eventListener.startCalled).isAtLeast(1)
}

@OptIn(ExperimentalCoroutinesApi::class)
Expand Down Expand Up @@ -319,7 +352,7 @@ class CoilTest {
@Test
fun content_error() {
val latch = CountDownLatch(1)
val states = ArrayList<CoilImageState>()
val states = ArrayList<ImageLoadState>()

composeTestRule.setContent {
CoilImage(
Expand All @@ -339,16 +372,16 @@ class CoilTest {

composeTestRule.runOnIdle {
assertThat(states).hasSize(3)
assertThat(states[0]).isEqualTo(CoilImageState.Empty)
assertThat(states[1]).isEqualTo(CoilImageState.Loading)
assertThat(states[2]).isInstanceOf(CoilImageState.Error::class.java)
assertThat(states[0]).isEqualTo(ImageLoadState.Empty)
assertThat(states[1]).isEqualTo(ImageLoadState.Loading)
assertThat(states[2]).isInstanceOf(ImageLoadState.Error::class.java)
}
}

@Test
fun content_success() {
val latch = CountDownLatch(1)
val states = ArrayList<CoilImageState>()
val states = ArrayList<ImageLoadState>()

composeTestRule.setContent {
CoilImage(
Expand All @@ -368,9 +401,9 @@ class CoilTest {

composeTestRule.runOnIdle {
assertThat(states).hasSize(3)
assertThat(states[0]).isEqualTo(CoilImageState.Empty)
assertThat(states[1]).isEqualTo(CoilImageState.Loading)
assertThat(states[2]).isInstanceOf(CoilImageState.Success::class.java)
assertThat(states[0]).isEqualTo(ImageLoadState.Empty)
assertThat(states[1]).isEqualTo(ImageLoadState.Loading)
assertThat(states[2]).isInstanceOf(ImageLoadState.Success::class.java)
}
}

Expand Down

0 comments on commit f905b05

Please sign in to comment.