Android Coil 非同步圖片載入實戰
在 App 開發中,處理遠端圖片的加載、比例縮放、圓角裁切以及快取管理是一項至關重要且複雜的任務。處理不當會導致記憶體溢出 (OOM) 或列表滑動卡頓。
Coil (Coroutine Image Loader) 是 Android 官方推薦的新一代圖片加載庫,它由 Kotlin 編寫,並與 Coroutines 平滑整合。
為什麼選擇 Coil?
目前的 Android 生態中還有 Glide 與 Picasso 等老牌圖片庫,但 Coil 具備以下優勢:
| 特性 | Coil | Glide | Picasso |
|---|---|---|---|
| 首選語言 | Kotlin (100%) | Java | Java |
| 非同步模型 | Coroutines | Executor Service | Executor 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 應用程式。