Compose Multiplatform : ネットワーク経由で画像を読み込む ② ImageLoader編

Compose Multiplatformでネットワーク経由で画像を読み込みたいときはImageLoaderを利用すると良いらしい。本記事ではImageLoaderの機能を調べ、どのような画像読み込み表示を実装できるか調べてまとめようと思います。

目次

前提

名称 バージョン 備考
Fleet(IDE) Fleet version: build 1.26.104 OS: Mac OS X (14.1.1, aarch64)
Kotlin 1.9.20
Compose Multiplatform 1.5.4

※もし不明点がある場合はこちらのGitHubのソースコードにて詳細を確認ください。

https://github.com/kaleidot725/Compose-Multiplatform-Playground/tree/main/projects/ImageLoader

セットアップ

TOML
[versions]
imageloader = "1.7.1"

[libraries]
imageloader = { module = "io.github.qdsfdhvh:image-loader", version.ref = "imageloader" }

Kotlin
   sourceSets {
        commonMain.dependencies {
            implementation(libs.imageloader)
     }
   }

画像読み込み機能

ImageLoaderに備わる画像読み込み機能として、以下のような機能がサポートされている。

機能名称 説明
画像読み込み ・Bitmap(JPG・PNG)やVector(XML)、SVGの画像をダウンロードして表示できる
キャッシュ ・ダウンロードした画像をキャッシュに保存できる
・キャッシュにはメモリキャッシュとディスクキャッシュがある
エラーハンドリング  ・読み込み中・成功・エラーという状態ごとに画像を出し分けることができる
・読み込み中・成功・エラーという状態を取得してComposable関数を出し分けることができる
ロギング ・独自のインタフェースで読み込んだ画像に関するログを記録できるようになっている

画像読み込み表示

ImageLoaderに備わる画像読み込み機能を利用すると、以下のような表示の制御ができる。

画像を表示する

動作サンプル

コードサンプル

Kotlin
@Composable
fun ImageLoaderImageDemo() {
    Box(
        modifier = Modifier
            .fillMaxSize()
            .background(Color.Black)
    ) {
        AutoSizeImage(
            url = ImageResource.sampleImage,
            contentDescription = "sample",
            contentScale = ContentScale.Crop,
            modifier = Modifier.size(200.dp)
        )
    }
}

読み込み中を表示する

動作サンプル

コードサンプル

Kotlin
@Composable
fun ImageLoaderLoadingDemo() {
    Box(
        modifier = Modifier
            .fillMaxSize()
            .background(Color.Black)
    ) {
        AutoSizeBox(
            url = ImageResource.sampleImage
        ) { action ->
            when (action) {
                is ImageAction.Success -> {
                    Image(
                        rememberImageSuccessPainter(action),
                        contentDescription = "image",
                        contentScale = ContentScale.Crop,
                        modifier = Modifier.size(200.dp)
                    )
                }

                is ImageAction.Loading -> {
                    CircularProgressIndicator(
                        modifier = Modifier.size(128.dp)
                    )
                }

                else -> {}
            }
        }
    }
}

エラー画像を表示する

動作サンプル

コードサンプル

Kotlin
@OptIn(ExperimentalResourceApi::class)
@Composable
fun ImageLoaderErrorDemo() {
    Box(
        modifier = Modifier
            .fillMaxSize()
            .background(Color.Black)
    ) {
        AutoSizeImage(
            url = ImageResource.sampleErrorImage,
            contentDescription = "sample",
            contentScale = ContentScale.Crop,
            errorPainter = { painterResource(ImageResource.error) },
            modifier = Modifier.size(200.dp)
        )
    }
}

プレースホルダー画像を表示する

動作サンプル

コードサンプル

Kotlin
@OptIn(ExperimentalResourceApi::class)
@Composable
fun ImageLoaderPlaceHolderDemo() {
    Box(
        modifier = Modifier
            .fillMaxSize()
            .background(Color.Black)
    ) {
        AutoSizeImage(
            url = ImageResource.sampleImage,
            contentDescription = "sample",
            placeholderPainter = { painterResource(ImageResource.placeholder) },
            contentScale = ContentScale.Crop,
            modifier = Modifier.size(200.dp)
        )
    }
}

クロスフェードする

動作サンプル

コードサンプル

Kotlin
@Composable
fun ImageLoaderCrossfadeDemo() {
    Column(
        modifier = Modifier
            .fillMaxSize()
            .background(Color.Black)
    ) {
        AutoSizeCrossfadeImage(url = ImageResource.sampleImage)
        AutoSizeCrossfadeImage(url = ImageResource.sampleErrorImage)
    }
}

@OptIn(ExperimentalAnimationApi::class, ExperimentalResourceApi::class)
@Composable
private fun AutoSizeCrossfadeImage(
    url: String,
    modifier: Modifier = Modifier,
) {
    AutoSizeBox(
        url = url,
        modifier = modifier,
    ) { action ->
        AnimatedContent(
            targetState = action,
            transitionSpec = { fadeIn(animationSpec = tween(0)) with fadeOut(animationSpec = tween(0)) }
        ) { action ->
            when (action) {
                is ImageAction.Success -> {
                    Image(
                        painter = rememberImageSuccessPainter(action),
                        contentDescription = "image",
                        contentScale = ContentScale.Crop,
                        modifier = Modifier
                            .size(200.dp)
                            .animateEnterExit(
                                enter = fadeIn(),
                                exit = fadeOut(),
                            )
                    )
                }

                is ImageAction.Loading -> {
                    Image(
                        painter = painterResource(ImageResource.placeholder),
                        contentDescription = "image",
                        contentScale = ContentScale.Crop,
                        modifier = Modifier
                            .size(200.dp)
                            .animateEnterExit(
                                enter = fadeIn(animationSpec = tween(0)),
                                exit = fadeOut(),
                            )
                    )
                }

                is ImageAction.Failure -> {
                    Image(
                        painter = painterResource(ImageResource.error),
                        contentDescription = "image",
                        contentScale = ContentScale.Crop,
                        modifier = Modifier
                            .size(200.dp)
                            .animateEnterExit(
                                enter = fadeIn(),
                                exit = fadeOut(),
                            )
                    )
                }

                else -> {}
            }
        }
    }
}

エラーハンドリングする

動作サンプル

コードサンプル

Kotlin
@OptIn(ExperimentalResourceApi::class)
@Composable
fun ImageLoaderErrorHandlingDemo() {
    Box(
        modifier = Modifier
            .fillMaxSize()
            .background(Color.Black)
    ) {
        AutoSizeBox(url = ImageResource.sampleImage) { action ->
            when (action) {
                is ImageAction.Success -> {
                    Image(
                        painter = rememberImageSuccessPainter(action),
                        contentDescription = "image",
                        contentScale = ContentScale.Crop,
                        modifier = Modifier.size(200.dp)
                    )
                }

                is ImageAction.Loading -> {
                    Image(
                        painter = painterResource(ImageResource.placeholder),
                        contentDescription = "image",
                        contentScale = ContentScale.Crop,
                        modifier = Modifier.size(200.dp)
                    )
                }

                is ImageAction.Failure -> {
                    Image(
                        painter = painterResource(ImageResource.error),
                        contentDescription = "image",
                        contentScale = ContentScale.Crop,
                        modifier = Modifier.size(200.dp)
                    )
                }

                else -> {}
            }
        }
    }
}

使ってみて

ImageLoaderは画像良い込みの基本的な制御ができますが、独自の読み込み画面をカスタマイズしたり・エラーハンドリングを伴う処理実装しようとすると、コードが少々長くなるかなと感じました。

ImageLoaderのリポジトリを見る感じ、最近になりドキュメントを揃えたり、AutoSizeBoxやAutoSizeImageなどのコンポーネントが追加されたり、まだまだ試行錯誤して品質を上げている最中なのかなと思っています。

少々カスタマイズに手間が必要な状態ではあると思うのですが、Compose Multiplatformの亜瓜を作る際にImageLoaderを導入すれば問題なく画像読み込みができると思います。

参考文献