Skip to content

Commit 931906d

Browse files
Migrate to coil 3 & ditch compose imageloader library (#90)
* Migrate to coil 3 & ditch compose imageloader * Remove compose imageLoader rejection version policy * Update README * Don't use hardware bitmaps when generating color palette from an image
1 parent 257ace4 commit 931906d

File tree

17 files changed

+91
-259
lines changed

17 files changed

+91
-259
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ Or from [github releases](https://github.com/mr3y-the-programmer/Ludi/releases)
5454

5555
[Compose Multiplatform](https://github.com/JetBrains/compose-multiplatform) for building shared UI.
5656

57-
[Coil](https://github.com/coil-kt/coil), [compose-imageloader](https://github.com/qdsfdhvh/compose-imageloader) for fetching & displaying images.
57+
[Coil 3](https://github.com/coil-kt/coil) for fetching & displaying images.
5858

5959
[kmpalette](https://github.com/jordond/kmpalette) for generating color palettes from images.
6060

gradle/libs.versions.toml

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,9 @@ views-material = "com.google.android.material:material:1.12.0"
3535
splash-screen = "androidx.core:core-splashscreen:1.0.1"
3636
androidx-annotations = "androidx.annotation:annotation:1.8.2"
3737
leakcanary = "com.squareup.leakcanary:leakcanary-android:2.14"
38-
coil = "io.coil-kt:coil-compose:2.7.0"
39-
compose-imageloader = "io.github.qdsfdhvh:image-loader:1.6.8"
38+
coil = "io.coil-kt.coil3:coil:3.0.0-alpha10"
39+
coil-compose = "io.coil-kt.coil3:coil-compose:3.0.0-alpha10"
40+
coil-network-ktor = "io.coil-kt.coil3:coil-network-ktor2:3.0.0-alpha10"
4041
compose-richeditor = "com.mohamedrejeb.richeditor:richeditor-compose:1.0.0-rc07"
4142
kermit = "co.touchlab:kermit:2.0.4"
4243
firebase-bom = "com.google.firebase:firebase-bom:33.3.0"
@@ -80,6 +81,7 @@ turbine = "app.cash.turbine:turbine:1.1.0"
8081
robolectric = "org.robolectric:robolectric:4.13"
8182
guava = "com.google.guava:guava:33.3.0-android"
8283
test-parameter-injector = "com.google.testparameterinjector:test-parameter-injector:1.17"
84+
kotlinx-coroutines-swing = "org.jetbrains.kotlinx:kotlinx-coroutines-swing:1.9.0"
8385
kotlinx-coroutines-test = "org.jetbrains.kotlinx:kotlinx-coroutines-test:1.9.0"
8486
kotlinx-serialization = "org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.3"
8587

settings.gradle.kts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ refreshVersions {
2727
// Recent versions of ktlint gradle plugin changed the default
2828
// code convention style which affects nearly all files in the codebase,
2929
// so, for now we are rejecting updates, as we are fine with the current style.
30-
val blacklist = listOf("org.jlleitschuh.gradle.ktlint", "io.github.qdsfdhvh")
30+
val blacklist = listOf("org.jlleitschuh.gradle.ktlint")
3131
candidate.stabilityLevel.isLessStableThan(current.stabilityLevel) || moduleId.group in blacklist || candidate.value.endsWith("-jre") || candidate.value.endsWith("-1.8.20")
3232
}
3333
}

shared/build.gradle.kts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,10 @@ kotlin {
9696
implementation(compose.compiler.auto)
9797
implementation(compose.runtime)
9898
implementation(libs.compose.richeditor)
99+
// Coil
100+
implementation(libs.coil)
101+
implementation(libs.coil.compose)
102+
implementation(libs.coil.network.ktor)
99103
// Palette
100104
implementation(libs.kmpalette.core)
101105
// Material3 WindowSizeClass
@@ -147,8 +151,6 @@ kotlin {
147151
implementation(libs.sqldelight.android)
148152
// Unbundled sqlite
149153
implementation(libs.sqlite.android)
150-
// Coil
151-
implementation(libs.coil)
152154
implementation(libs.androidx.core.ktx)
153155
// Chrome custom tabs
154156
implementation(libs.androidx.browser)
@@ -188,7 +190,7 @@ kotlin {
188190

189191
// UI
190192
implementation(compose.desktop.common)
191-
implementation(libs.compose.imageloader)
193+
implementation(libs.kotlinx.coroutines.swing)
192194
}
193195
}
194196

Lines changed: 6 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -1,60 +1,11 @@
11
package com.mr3y.ludi.shared.ui.components
22

3-
import androidx.compose.runtime.Composable
4-
import androidx.compose.ui.Alignment
5-
import androidx.compose.ui.Modifier
6-
import androidx.compose.ui.graphics.FilterQuality
3+
import androidx.compose.ui.graphics.ImageBitmap
74
import androidx.compose.ui.graphics.asImageBitmap
8-
import androidx.compose.ui.graphics.painter.Painter
9-
import androidx.compose.ui.layout.ContentScale
10-
import androidx.compose.ui.platform.LocalContext
11-
import androidx.core.graphics.drawable.toBitmap
12-
import coil.compose.AsyncImagePainter
13-
import coil.request.ImageRequest
14-
import coil.request.SuccessResult
5+
import coil3.Bitmap
6+
import coil3.request.ImageRequest
7+
import coil3.request.allowHardware
158

16-
@Composable
17-
actual fun AsyncImage(
18-
url: String?,
19-
contentDescription: String?,
20-
modifier: Modifier,
21-
placeholder: Painter?,
22-
error: Painter?,
23-
onState: ((State) -> Unit)?,
24-
contentScale: ContentScale,
25-
alignment: Alignment,
26-
customMemoryCacheKey: String?,
27-
filterQuality: FilterQuality
28-
) {
29-
val context = LocalContext.current
30-
coil.compose.AsyncImage(
31-
model = ImageRequest.Builder(context)
32-
.data(url)
33-
.memoryCacheKey(customMemoryCacheKey)
34-
.build(),
35-
contentDescription = contentDescription,
36-
transform = { state ->
37-
when (state) {
38-
is AsyncImagePainter.State.Loading -> AsyncImagePainter.State.Loading(painter = placeholder)
39-
is AsyncImagePainter.State.Error -> AsyncImagePainter.State.Error(painter = error, result = state.result)
40-
else -> state
41-
}
42-
},
43-
onState = { coilState ->
44-
when (coilState) {
45-
is AsyncImagePainter.State.Empty -> onState?.invoke(State.Empty)
46-
is AsyncImagePainter.State.Loading -> onState?.invoke(State.Loading(coilState.painter))
47-
is AsyncImagePainter.State.Success -> onState?.invoke(State.Success(painter = coilState.painter, result = coilState.result.toOurSuccessResult()))
48-
is AsyncImagePainter.State.Error -> onState?.invoke(State.Error(painter = coilState.painter, result = ErrorResult(coilState.result.throwable)))
49-
}
50-
},
51-
contentScale = contentScale,
52-
alignment = alignment,
53-
filterQuality = filterQuality,
54-
modifier = modifier
55-
)
56-
}
9+
actual fun ImageRequest.Builder.platformSpecificConfig(allowHardware: Boolean): ImageRequest.Builder = this.allowHardware(allowHardware)
5710

58-
private fun SuccessResult.toOurSuccessResult(): com.mr3y.ludi.shared.ui.components.SuccessResult {
59-
return SuccessResult(this.drawable.toBitmap().asImageBitmap())
60-
}
11+
actual fun Bitmap.asImageBitmap(): ImageBitmap = this.asImageBitmap()
Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,3 @@
11
package com.mr3y.ludi.shared.ui.resources
22

3-
import androidx.compose.runtime.Composable
4-
import androidx.compose.runtime.CompositionLocalProvider
5-
6-
@Composable
7-
actual fun ProvideCompositionLocalValues(content: @Composable () -> Unit) = CompositionLocalProvider(content = content)
8-
93
actual fun isDesktopPlatform() = false

shared/src/commonMain/kotlin/com/mr3y/ludi/shared/App.kt

Lines changed: 17 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -7,33 +7,36 @@ import androidx.compose.runtime.Composable
77
import androidx.compose.runtime.CompositionLocalProvider
88
import androidx.compose.ui.Modifier
99
import cafe.adriel.voyager.navigator.Navigator
10+
import coil3.ImageLoader
11+
import coil3.annotation.ExperimentalCoilApi
12+
import coil3.compose.setSingletonImageLoaderFactory
1013
import com.mr3y.ludi.shared.ui.adaptive.LocalWindowSizeClass
11-
import com.mr3y.ludi.shared.ui.resources.ProvideCompositionLocalValues
1214
import com.mr3y.ludi.shared.ui.screens.home.HomeScreen
1315
import com.mr3y.ludi.shared.ui.screens.onboarding.OnboardingScreen
1416
import com.mr3y.ludi.shared.ui.theme.LudiTheme
1517

16-
@OptIn(ExperimentalMaterial3WindowSizeClassApi::class)
18+
@OptIn(ExperimentalMaterial3WindowSizeClassApi::class, ExperimentalCoilApi::class)
1719
@Composable
1820
fun App(
1921
isDarkTheme: Boolean,
2022
useDynamicColor: Boolean,
2123
showOnboardingScreen: Boolean,
2224
modifier: Modifier = Modifier
2325
) {
24-
ProvideCompositionLocalValues {
25-
LudiTheme(
26-
darkTheme = isDarkTheme,
27-
dynamicColor = useDynamicColor
26+
LudiTheme(
27+
darkTheme = isDarkTheme,
28+
dynamicColor = useDynamicColor
29+
) {
30+
setSingletonImageLoaderFactory { context ->
31+
ImageLoader.Builder(context).build()
32+
}
33+
CompositionLocalProvider(
34+
LocalWindowSizeClass provides calculateWindowSizeClass()
2835
) {
29-
CompositionLocalProvider(
30-
LocalWindowSizeClass provides calculateWindowSizeClass()
31-
) {
32-
if (showOnboardingScreen) {
33-
Navigator(screen = OnboardingScreen)
34-
} else {
35-
HomeScreen(modifier = modifier.fillMaxSize())
36-
}
36+
if (showOnboardingScreen) {
37+
Navigator(screen = OnboardingScreen)
38+
} else {
39+
HomeScreen(modifier = modifier.fillMaxSize())
3740
}
3841
}
3942
}

shared/src/commonMain/kotlin/com/mr3y/ludi/shared/ui/components/LudiAsyncImage.kt

Lines changed: 31 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -7,44 +7,48 @@ import androidx.compose.ui.graphics.FilterQuality
77
import androidx.compose.ui.graphics.ImageBitmap
88
import androidx.compose.ui.graphics.painter.Painter
99
import androidx.compose.ui.layout.ContentScale
10+
import coil3.Bitmap
11+
import coil3.compose.AsyncImagePainter
12+
import coil3.compose.LocalPlatformContext
13+
import coil3.request.ImageRequest
1014

1115
@Composable
12-
expect fun AsyncImage(
16+
fun LudiAsyncImage(
1317
url: String?,
1418
contentDescription: String?,
1519
modifier: Modifier = Modifier,
1620
placeholder: Painter? = null,
1721
error: Painter? = null,
18-
onState: ((State) -> Unit)? = null,
22+
onState: ((AsyncImagePainter.State) -> Unit)? = null,
1923
contentScale: ContentScale = ContentScale.Fit,
2024
alignment: Alignment = Alignment.Center,
2125
customMemoryCacheKey: String? = null,
26+
allowBitmapHardware: Boolean = true,
2227
filterQuality: FilterQuality = FilterQuality.Low
23-
)
24-
25-
sealed interface State {
26-
27-
val painter: Painter?
28-
29-
data object Empty : State {
30-
override val painter: Painter? get() = null
31-
}
32-
33-
data class Loading(override val painter: Painter?) : State
34-
35-
data class Success(
36-
override val painter: Painter,
37-
val result: SuccessResult
38-
) : State
39-
40-
data class Error(
41-
override val painter: Painter?,
42-
val result: ErrorResult
43-
) : State
28+
) {
29+
val context = LocalPlatformContext.current
30+
coil3.compose.AsyncImage(
31+
model = ImageRequest.Builder(context)
32+
.data(url)
33+
.memoryCacheKey(customMemoryCacheKey)
34+
.platformSpecificConfig(allowBitmapHardware)
35+
.build(),
36+
contentDescription = contentDescription,
37+
transform = { state ->
38+
when (state) {
39+
is AsyncImagePainter.State.Loading -> AsyncImagePainter.State.Loading(painter = placeholder)
40+
is AsyncImagePainter.State.Error -> AsyncImagePainter.State.Error(painter = error, result = state.result)
41+
else -> state
42+
}
43+
},
44+
onState = onState,
45+
contentScale = contentScale,
46+
alignment = alignment,
47+
filterQuality = filterQuality,
48+
modifier = modifier
49+
)
4450
}
4551

46-
sealed interface ImageResult
47-
48-
data class SuccessResult(val bitmap: ImageBitmap) : ImageResult
52+
expect fun ImageRequest.Builder.platformSpecificConfig(allowHardware: Boolean): ImageRequest.Builder
4953

50-
data class ErrorResult(val error: Throwable?) : ImageResult
54+
expect fun Bitmap.asImageBitmap(): ImageBitmap
Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,3 @@
11
package com.mr3y.ludi.shared.ui.resources
22

3-
import androidx.compose.runtime.Composable
4-
5-
/**
6-
* Each platform may want to provide specific CompositionLocals
7-
*/
8-
@Composable
9-
expect fun ProvideCompositionLocalValues(content: @Composable () -> Unit)
10-
113
expect fun isDesktopPlatform(): Boolean

shared/src/commonMain/kotlin/com/mr3y/ludi/shared/ui/screens/deals/DealAndGiveawayCards.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ import androidx.compose.ui.unit.dp
3535
import cafe.adriel.lyricist.LocalStrings
3636
import com.mr3y.ludi.shared.core.model.Deal
3737
import com.mr3y.ludi.shared.core.model.GiveawayEntry
38-
import com.mr3y.ludi.shared.ui.components.AsyncImage
38+
import com.mr3y.ludi.shared.ui.components.LudiAsyncImage
3939
import com.mr3y.ludi.shared.ui.components.placeholder.defaultPlaceholder
4040
import kotlinx.coroutines.delay
4141
import kotlinx.coroutines.isActive
@@ -212,7 +212,7 @@ private fun OfferScaffold(
212212
.defaultPlaceholder(isVisible = title == null),
213213
verticalArrangement = Arrangement.spacedBy(8.dp)
214214
) {
215-
AsyncImage(
215+
LudiAsyncImage(
216216
url = thumbnailUrl,
217217
placeholder = painterResource(Res.drawable.placeholder),
218218
error = painterResource(Res.drawable.placeholder),

0 commit comments

Comments
 (0)