【Android】シンプルなネットワーク画像ライブラリを自作する

## はじめに

インターネット上にある画像をAndroidのアプリ内で表示したい場合、[Coil](https://github.com/coil-kt/coil)などのネットワーク画像ライブラリを使用することが一般的です。

本記事では、そのような**画像ライブラリのシンプル版を自作**してみたいと思います。

## 処理の流れ

ネットワーク上の画像を表示するまでの処理は、以下の3段階に分かれます。

```mermaid
flowchart LR
    A[1.インターネットから画像のデータを取得する] --> B[2.取得した画像データをBitmapに変換する]
    B --> C[3.BitmapからPainterを作成し画面に表示する]
```

### 1. インターネットから画像のデータを取得する

HTTPクライアントとして[OkHttp](https://square.github.io/okhttp/)を使用します。レスポンスのボディから[BufferedSource](https://square.github.io/okio/3.x/okio/okio/okio/-buffered-source/index.html)を取得し、結果として返します。

```kotlin
object ImageRequester {
    private val client = OkHttpClient()

    suspend fun request(url: String): BitmapBufferedSource {
        return withContext(Dispatchers.IO) {
            val request = Request.Builder().url(url).build()
            val response = client.newCall(request).execute()
            BitmapBufferedSource(response.body?.source()!!)
        }
    }
}
```

ちなみに、Androidではネットワークアクセスをメインスレッドで行うと[NetworkOnMainThreadException](https://developer.android.com/reference/android/os/NetworkOnMainThreadException)が発生するため注意が必要です。

### 2. 取得した画像データをBitmapに変換する

取得した画像データを[android.graphics.Bitmap](https://developer.android.com/reference/android/graphics/Bitmap)に変換します。ここでは[BitmapFactory#decodeStream(InputStream)](https://developer.android.com/reference/android/graphics/BitmapFactory#decodeStream(java.io.InputStream))を使用します。

```kotlin
data class BitmapBufferedSource(
    val bufferedSource: BufferedSource
) {
    fun toBitmap(): Bitmap {
        return BitmapFactory.decodeStream(bufferedSource.inputStream())
    }
}
```

### 3. BitmapからPainterを作成し画面に表示する

Bitmapを作成できたら、Composeで画像を表示するために[Painter](https://developer.android.com/reference/kotlin/androidx/compose/ui/graphics/painter/Painter)を作成します。ここでは[BitmapPainter](https://developer.android.com/reference/kotlin/androidx/compose/ui/graphics/painter/BitmapPainter)を使用します。

```kotlin
@Composable
fun rememberNetworkImagePainter(url: String): Painter {
    var currentImageBitmap: ImageBitmap? by remember { mutableStateOf(null) }

    LaunchedEffect(url) {
        val bitmapBufferedSource = ImageRequester.request(url)
        val bitmap = bitmapBufferedSource.toBitmap()
        val imageBitMap = bitmap.asImageBitmap()
        currentImageBitmap = imageBitMap
    }

    return remember(currentImageBitmap) {
        val imageBitmap = currentImageBitmap
        if (imageBitmap == null) {
            EmptyPainter
        } else {
            BitmapPainter(imageBitmap)
        }
    }
}
```

作成したPainterを[Image](https://developer.android.com/reference/kotlin/androidx/compose/foundation/package-summary#Image(androidx.compose.ui.graphics.vector.ImageVector,kotlin.String,androidx.compose.ui.Modifier,androidx.compose.ui.Alignment,androidx.compose.ui.layout.ContentScale,kotlin.Float,androidx.compose.ui.graphics.ColorFilter))で使用します。

```kotlin
@Composable
internal fun NetworkImage(
    url: String,
    modifier: Modifier = Modifier
) {
    Image(
        painter = rememberNetworkImagePainter(url),
        contentDescription = null,
        modifier = modifier,
    )
}
```

## できました！

約50行程度のコードで、ネットワーク上の画像を表示することができました :tada:

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/56635/e10f7048-319a-dcf5-1e06-32022dde6f06.png" width="320" />

## なぜ画像ライブラリを使用するべきか

ここまで読んで、意外と簡単にComposeでネットワーク画像を表示できることが分かったと思います。それでも画像ライブラリを使用するべき理由がいくつかあります。

### キャッシュ

画像ライブラリはメモリキャッシュやディスクキャッシュを自動で管理し、不要なネットワークアクセスを削減します。一度ロードした画像を再取得せず即座に表示できるため、スクロール時のパフォーマンス向上にもつながります。

### 様々なフォーマット

画像ライブラリを使用すると、JPEGやPNGだけでなく、GIFやSVGなどのフォーマットも簡単に扱えます。Coilの場合、[coil-gif](https://coil-kt.github.io/coil/gifs/)、[coil-svg](https://coil-kt.github.io/coil/svgs/)、[coil-video](https://coil-kt.github.io/coil/videos/)を利用できます

### マルチプラットフォーム

[Coil v3](https://coil-kt.github.io/coil/upgrading_to_coil3/)からはKotlin Multiplatform(Compose Multiplatform)に対応し、Android以外のプラットフォームでも動作するようになりました。

### 状態管理

画像の読み込みにはローディング、エラー、プレースホルダーなどの状態管理が必要です。画像ライブラリを使用すると、これらの処理を簡単に実装できます。

## まとめ

本記事では、シンプルなネットワーク画像ライブラリを自作しました。コード量も50行程度で実装することができましたが、キャッシュ管理や様々なフォーマットや状態管理等を考慮すると、既存の画像ライブラリ（Coilなど）を利用する方がメリットが大きいことが分かります。

本記事を通して画像ライブラリがどのような処理を行っているのか理解が深まれば幸いです :thumbsup: 

> 本記事のサンプルコード

https://github.com/maxfie1d/tiny-imaging