Android Coil 非同步圖片載入實戰

在 App 開發中,處理遠端圖片的加載、比例縮放、圓角裁切以及快取管理是一項至關重要且複雜的任務。處理不當會導致記憶體溢出 (OOM) 或列表滑動卡頓。

Coil (Coroutine Image Loader) 是 Android 官方推薦的新一代圖片加載庫,它由 Kotlin 編寫,並與 Coroutines 平滑整合。

為什麼選擇 Coil?

目前的 Android 生態中還有 GlidePicasso 等老牌圖片庫,但 Coil 具備以下優勢:

特性CoilGlidePicasso
首選語言Kotlin (100%)JavaJava
非同步模型CoroutinesExecutor ServiceExecutor Service
庫體積非常輕量 (~2000 個方法)較重 (~8000 個方法)輕量
Jetpack 整合完美對齊 Compose透過過渡層支援透過過渡層支援
KMP 支援支援 Kotlin Multiplatform不支援不支援

基礎使用與配置

首先,在 build.gradle.kts 中引入相依項:

dependencies {
    // Coil Compose 核心庫
    implementation("io.coil-kt:coil-compose:2.6.0")
    // 如果需要支援 SVG
    implementation("io.coil-kt:coil-svg:2.6.0")
}

基礎 AsyncImage (Compose)

在 Jetpack Compose 中,使用 AsyncImage 是加載遠端圖片的最簡單方式。

@Composable
fun UserAvatar(imageUrl: String) {
    AsyncImage(
        model = imageUrl,
        contentDescription = "User Avatar",
        modifier = Modifier
            .size(64.dp)
            .clip(CircleShape),
        placeholder = painterResource(R.drawable.placeholder),
        error = painterResource(R.drawable.error_img),
        contentScale = ContentScale.Crop
    )
}

全域單例配置 (Singleton ImageLoader)

為了共用快取資源、連線池並統一處理圖片解碼(如 GIF, SVG),我們應實作 ImageLoaderFactory

Application 類別中:

class MyApplication : Application(), ImageLoaderFactory {
    override fun newImageLoader(): ImageLoader {
        return ImageLoader.Builder(this)
            .crossfade(true) // 淡入效果
            .diskCache {
                DiskCache.Builder()
                    .directory(cacheDir.resolve("image_cache"))
                    .maxSizeBytes(100 * 1024 * 1024) // 限制磁碟快取 100MB
                    .build()
            }
            .memoryCache {
                MemoryCache.Builder(this)
                    .maxSizePercent(0.25) // 使用 25% 的可用記憶體作為快取
                    .build()
            }
            .components {
                add(SvgDecoder.Factory()) // 支援 SVG 解析
            }
            .build()
    }
}

預先載入 (Preloading)

如果你預知使用者即將看到某些圖片(如列表滑動的前幾項),可以使用預先載入來消除白屏現象。

val request = ImageRequest.Builder(context)
    .data("https://example.com/huge-banner.jpg")
    // 設定優先權為最高
    .precision(Precision.EXACT)
    .build()

// 預先載入至快取,不需等待 UI 渲染
context.imageLoader.enqueue(request)

監聽與效能追蹤

透過 EventListener,我們可以監控全 App 的圖片加載狀況,並找出載入失敗的原因。

val imageLoader = ImageLoader.Builder(context)
    .eventListener(object : EventListener {
        override fun onStart(request: ImageRequest) {
            Log.d("Coil", "載入開始: ${request.data}")
        }
        override fun onSuccess(request: ImageRequest, result: SuccessResult) {
            Log.d("Coil", "載入成功: 來源為 ${result.dataSource}")
        }
        override fun onError(request: ImageRequest, result: ErrorResult) {
            Log.e("Coil", "載入失敗: ${result.throwable.message}")
        }
    })
    .build()

在測試中 Mock 圖片載入

在 UI 測試時,為了避免真的發送網路請求並節省時間,你可以提供一個本地的 ImageLoader

@Before
fun setup() {
    val engine = FakeImageLoaderEngine.Builder()
        .intercept("https://example.com/test.jpg", ColorDrawable(Color.RED))
        .default(ColorDrawable(Color.BLUE))
        .build()

    val imageLoader = ImageLoader.Builder(context)
        .components { add(engine) }
        .build()
    
    // 設定為全域單例,讓 AsyncImage 在測試時使用它
    Coil.setImageLoader(imageLoader)
}

透過合理的快取配置與預載入策略,Coil 能協助你構建出極致流暢、體積極小的 Android 應用程式。