Skip to content

Commit

Permalink
Add a Compose API for Glide.
Browse files Browse the repository at this point in the history
This is relatively early stage at this point. The API is likely to change in
breaking ways. Ideally the migrations are not large, but I'd expect a
few iterations.

There's already a TODO to improve the Prealoader API and make the
GlideImage API less reliant on GlideBuilder.

Early feedback and usage would be welcome, with the caveat that it'll
mean some migration work down the road.

PiperOrigin-RevId: 475696354
  • Loading branch information
sjudd authored and glide-copybara-robot committed Sep 21, 2022
1 parent 790c351 commit 8bef56e
Show file tree
Hide file tree
Showing 18 changed files with 857 additions and 11 deletions.
2 changes: 1 addition & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,6 @@ subprojects { project ->
url "https://oss.sonatype.org/content/repositories/snapshots"
}
gradlePluginPortal()

}

afterEvaluate {
Expand Down Expand Up @@ -154,6 +153,7 @@ subprojects { project ->
abortOnError false
}

// We don't need a BuildConfig constants class.
buildFeatures {
buildConfig = false
}
Expand Down
3 changes: 3 additions & 0 deletions gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -48,12 +48,14 @@ ANDROID_X_ANNOTATION_VERSION=1.3.0
ANDROID_X_APPCOMPAT_VERSION=1.3.1
ANDROID_X_BENCHMARK_VERSION=1.1.0
ANDROID_X_CARDVIEW_VERSION=1.0.0
ANDROID_X_COMPOSE_VERSION=1.2.1
ANDROID_X_CONCURRENT_FUTURES_VERSION=1.1.0
ANDROID_X_CORE_VERSION=1.6.0
ANDROID_X_EXIF_INTERFACE_VERSION=1.3.3
ANDROID_X_FRAGMENT_VERSION=1.3.6
ANDROID_X_RECYCLERVIEW_VERSION=1.2.1
ANDROID_X_TEST_CORE_VERSION=1.4.0
ANDROID_X_TEST_ESPRESSO_VERSION=3.4.0
ANDROID_X_TEST_JUNIT_VERSION=1.1.3
ANDROID_X_TEST_RULES_VERSION=1.4.0
ANDROID_X_TEST_RUNNER_VERSION=1.4.0
Expand All @@ -71,6 +73,7 @@ JETBRAINS_KOTLIN_VERSION=1.7.0
JETBRAINS_KOTLIN_TEST_VERSION=1.7.0

## Other dependency versions
ACCOMPANIEST_VERSION=0.25.1
ANDROID_GRADLE_VERSION=7.2.1
AUTO_SERVICE_VERSION=1.0-rc3
KOTLIN_COMPILE_TESTING_VERSION=1.4.9
Expand Down
11 changes: 11 additions & 0 deletions integration/compose/api/compose.api
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
public abstract interface annotation class com/bumptech/glide/integration/compose/ExperimentalGlideComposeApi : java/lang/annotation/Annotation {
}

public final class com/bumptech/glide/integration/compose/GlideImageKt {
public static final fun GlideImage (Ljava/lang/Object;Ljava/lang/String;Landroidx/compose/ui/Modifier;Landroidx/compose/ui/Alignment;Landroidx/compose/ui/layout/ContentScale;FLandroidx/compose/ui/graphics/ColorFilter;Lkotlin/jvm/functions/Function1;Landroidx/compose/runtime/Composer;II)V
}

public final class com/bumptech/glide/integration/compose/PreloadKt {
public static final fun GlideLazyListPreloader-u6VnWhU (Landroidx/compose/foundation/lazy/LazyListState;Ljava/util/List;JILjava/lang/Integer;Lkotlin/jvm/functions/Function2;Landroidx/compose/runtime/Composer;II)V
}

63 changes: 63 additions & 0 deletions integration/compose/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
plugins {
id 'com.android.library'
id 'kotlin-android'
}

android {
compileSdk 32

defaultConfig {
minSdk 21
targetSdk 32

testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}

buildFeatures {
compose = true
}

buildTypes {
release {
minifyEnabled false
}
}

composeOptions {
kotlinCompilerExtensionVersion '1.2.0'
}

compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}

kotlinOptions {
jvmTarget = '1.8'
}
}

// Enable strict mode, but exclude tests.
tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile) {
if (!it.name.contains("Test")) {
kotlinOptions.freeCompilerArgs += "-Xexplicit-api=strict"
}
}

dependencies {
implementation project(':library')
implementation project(':integration:ktx')
implementation(project(':integration:recyclerview')) {
transitive = false
}
implementation "androidx.compose.foundation:foundation:$ANDROID_X_COMPOSE_VERSION"
implementation "androidx.compose.ui:ui:$ANDROID_X_COMPOSE_VERSION"
implementation "com.google.accompanist:accompanist-drawablepainter:$ACCOMPANIEST_VERSION"
implementation "androidx.core:core-ktx:$ANDROID_X_CORE_KTX_VERSION"
debugImplementation "androidx.compose.ui:ui-test-manifest:$ANDROID_X_COMPOSE_VERSION"
androidTestImplementation "junit:junit:$JUNIT_VERSION"
androidTestImplementation "androidx.compose.ui:ui-test-junit4:$ANDROID_X_COMPOSE_VERSION"
androidTestImplementation "androidx.test.espresso:espresso-core:$ANDROID_X_TEST_ESPRESSO_VERSION"
androidTestImplementation "androidx.test.espresso.idling:idling-concurrent:$ANDROID_X_TEST_ESPRESSO_VERSION"
androidTestImplementation "androidx.test.ext:junit:$ANDROID_X_TEST_JUNIT_VERSION"
}
9 changes: 9 additions & 0 deletions integration/compose/gradle.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
POM_NAME=Glide Compose Integration
POM_ARTIFACT_ID=compose
POM_PACKAGING=aar
POM_DESCRIPTION=An integration library to integrate with Jetpack Compose

VERSION_MAJOR=1
VERSION_MINOR=0
VERSION_PATCH=0
VERSION_NAME=1.0.0-alpha.0-SNAPSHOT
51 changes: 51 additions & 0 deletions integration/compose/rules.bzl
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
"""
Workaround for the lack of kt_android_library_test_rule (b/243549140).
"""

load("//tools/build_defs/kotlin:rules.bzl", "kt_android_library")
load("//tools/build_defs/android:rules.bzl", "android_library_test")

def kt_android_library_test(name, size, srcs, custom_package, manifest, manifest_values, deps, target_devices, test_class):
"""A simple equivalent of android_library_test that works with Kotlin.
This is not well generalized. A better solution is b/243549140, which would
mean adding a real kt_android_library_test to Android's test_macros:
http://google3/tools/build_defs/android/dev/test_macros.bzl;l=17;rcl=470614953
While this is only used in one place and we could theoretically move a bunch
of constants out of the test rule into this one, it seems better not to do
so. Leaving the constant values in the calling BUILD file should make a
migration to a real kt_android_library_test rule easier in the future.
Args:
name: The test name
size: The test size, probably large
srcs: The test library source set
custom_package: The test library and android_library_test package
manifest: The android_library_test manifest
manifest_values: The android_library_test manifest values
deps: the test library and android_library_test dependencies
target_devices: the target devices passed to android_library_test
test_class: the test class for the android_library_test
"""
library_attrs = {}
library_attrs["srcs"] = srcs
library_attrs["deps"] = deps
library_attrs["testonly"] = 1
library_attrs["custom_package"] = custom_package

libname = name + "_lib"

test_attrs = {}
test_attrs["deps"] = [":" + libname]
test_attrs["size"] = size
test_attrs["manifest"] = manifest
test_attrs["multidex"] = "legacy"

test_attrs["target_devices"] = target_devices
test_attrs["manifest"] = manifest
test_attrs["manifest_values"] = manifest_values
test_attrs["test_class"] = test_class

kt_android_library(libname, **library_attrs)
android_library_test(name, **test_attrs)
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
@file:OptIn(ExperimentalGlideComposeApi::class, InternalGlideApi::class)

package com.bumptech.glide.integration.compose

import android.content.Context
import android.graphics.drawable.BitmapDrawable
import android.graphics.drawable.Drawable
import androidx.compose.foundation.layout.size
import androidx.compose.runtime.MutableState
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.semantics.SemanticsPropertyKey
import androidx.compose.ui.test.SemanticsMatcher
import androidx.compose.ui.test.assert
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithContentDescription
import androidx.compose.ui.unit.dp
import androidx.test.core.app.ApplicationProvider
import com.bumptech.glide.Glide
import com.bumptech.glide.integration.ktx.InternalGlideApi
import com.bumptech.glide.integration.ktx.Size
import com.bumptech.glide.load.engine.executor.GlideIdlingResourceInit
import java.util.concurrent.atomic.AtomicReference
import org.junit.Before
import org.junit.Rule
import org.junit.Test

class GlideComposeTest {
private val context: Context = ApplicationProvider.getApplicationContext()
@get:Rule val composeRule = createComposeRule()

@Before
fun setUp() {
GlideIdlingResourceInit.initGlide(composeRule)
}

@Test
fun glideImage_noModifierSize_resourceDrawable_displaysDrawable() {
val description = "test"
val resourceId = android.R.drawable.star_big_on
composeRule.setContent { GlideImage(model = resourceId, contentDescription = description) }

composeRule.waitForIdle()

val expectedSize = resourceId.bitmapSize()
composeRule
.onNodeWithContentDescription(description)
.assert(expectDisplayedDrawableSize(expectedSize))
}

@Test
fun glideImage_withSizeLargerThanImage_noTransformSet_doesNotUpscaleImage() {
val description = "test"
val resourceId = android.R.drawable.star_big_on
composeRule.setContent {
GlideImage(
model = resourceId,
contentDescription = description,
modifier = Modifier.size(300.dp, 300.dp)
)
}

composeRule.waitForIdle()

val expectedSize = resourceId.bitmapSize()
composeRule
.onNodeWithContentDescription(description)
.assert(expectDisplayedDrawableSize(expectedSize))
}

@Test
fun glideImage_withSizeLargerThanImage_upscaleTransformSet_upscalesImage() {
val viewDimension = 300
val description = "test"
val sizeRef = AtomicReference<Size>()
composeRule.setContent {
GlideImage(
model = android.R.drawable.star_big_on,
requestBuilderTransform = { it.fitCenter() },
contentDescription = description,
modifier = Modifier.size(viewDimension.dp, viewDimension.dp)
)

with(LocalDensity.current) {
val pixels = viewDimension.dp.roundToPx()
sizeRef.set(Size(pixels, pixels))
}
}

composeRule.waitForIdle()

val pixels = sizeRef.get()
composeRule
.onNodeWithContentDescription(description)
.assert(expectDisplayedDrawableSize(pixels))
}

@Test
fun glideImage_withThumbnail_prefersFullSizeImage() {
val description = "test"
val thumbnailDrawable = context.getDrawable(android.R.drawable.star_big_off)
val fullsizeDrawable = context.getDrawable(android.R.drawable.star_big_on)

val fullsizeBitmap = (fullsizeDrawable as BitmapDrawable).bitmap

composeRule.setContent {
GlideImage(
model = fullsizeDrawable,
requestBuilderTransform = { it.thumbnail(Glide.with(context).load(thumbnailDrawable)) },
contentDescription = description,
)
}

composeRule.waitForIdle()

composeRule
.onNodeWithContentDescription(description)
.assert(expectDisplayedDrawable(fullsizeBitmap) { (it as BitmapDrawable).bitmap })
}

private fun Int.bitmapSize() = context.resources.getDrawable(this, context.theme).size()
}

private fun Drawable.size() = (this as BitmapDrawable).bitmap.let { Size(it.width, it.height) }

private fun expectDisplayedDrawableSize(widthPixels: Int, heightPixels: Int): SemanticsMatcher =
expectDisplayedDrawable(Size(widthPixels, heightPixels)) { it?.size() }

private fun expectDisplayedDrawableSize(expectedSize: Size): SemanticsMatcher =
expectDisplayedDrawable(expectedSize) { it?.size() }

private fun <ValueT> expectDisplayedDrawable(
expectedValue: ValueT,
transform: (Drawable?) -> ValueT
): SemanticsMatcher = expectStateValue(DisplayedDrawableKey, expectedValue) { transform(it) }

private fun <ValueT, TransformedValueT> expectStateValue(
key: SemanticsPropertyKey<MutableState<ValueT?>>,
expectedValue: TransformedValueT,
transform: (ValueT?) -> TransformedValueT?
): SemanticsMatcher =
SemanticsMatcher("${key.name} = '$expectedValue'") {
val value = transform(it.config.getOrElseNullable(key) { null }?.value)
if (value != expectedValue) {
throw AssertionError("Expected: $expectedValue, but was: $value")
}
true
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package com.bumptech.glide.load.engine.executor

import androidx.compose.ui.test.IdlingResource
import androidx.compose.ui.test.junit4.ComposeTestRule
import androidx.test.core.app.ApplicationProvider
import androidx.test.espresso.idling.concurrent.IdlingThreadPoolExecutor
import com.bumptech.glide.Glide
import com.bumptech.glide.GlideBuilder
import java.util.concurrent.LinkedBlockingQueue
import java.util.concurrent.TimeUnit

object GlideIdlingResourceInit {

fun initGlide(composeRule: ComposeTestRule) {
val executor =
IdlingThreadPoolExecutor(
"glide_test_thread",
/* corePoolSize= */ 1,
/* maximumPoolSize= */ 1,
/* keepAliveTime= */ 1,
TimeUnit.SECONDS,
LinkedBlockingQueue()
) { Thread(it) }
composeRule.registerIdlingResource(
object : IdlingResource {
override val isIdleNow: Boolean
get() = executor.isIdleNow
}
)
val glideExecutor = GlideExecutor(executor)
Glide.init(
ApplicationProvider.getApplicationContext(),
GlideBuilder()
.setSourceExecutor(glideExecutor)
.setAnimationExecutor(glideExecutor)
.setDiskCacheExecutor(glideExecutor)
)
}
}
5 changes: 5 additions & 0 deletions integration/compose/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.bumptech.glide.integration.compose">
<uses-sdk android:minSdkVersion="21" />
</manifest>
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.bumptech.glide.integration.compose

@RequiresOptIn(
level = RequiresOptIn.Level.ERROR,
message =
"Glide's Compose integration is experimental. APIs may change or be removed without" +
" warning."
)
@Retention(AnnotationRetention.BINARY)
@kotlin.annotation.Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION)
public annotation class ExperimentalGlideComposeApi
Loading

0 comments on commit 8bef56e

Please sign in to comment.