$npx -y skills add rcosteira79/android-skills --skill coil-composeUse when loading images in Compose or Compose Multiplatform with Coil 3 — AsyncImage vs SubcomposeAsyncImage vs rememberAsyncImagePainter, ImageRequest configuration, placeholder/error states, performance in lists, and KMP setup with LocalPlatformContext.
| 1 | # Coil 3 for Compose & Compose Multiplatform |
| 2 | |
| 3 | This reference covers the KMP specifics and two performance guardrails, not the basics of `AsyncImage` / `SubcomposeAsyncImage` / `ImageRequest` / `placeholder` / `error` / `contentDescription` / a shared `ImageLoader`. |
| 4 | |
| 5 | ## KMP: `LocalPlatformContext`, not `LocalContext` |
| 6 | |
| 7 | Coil 3 is fully multiplatform — `AsyncImage` / `SubcomposeAsyncImage` / `rememberAsyncImagePainter` are identical in `commonMain`, with **no `expect`/`actual` needed**. The one swap: use `LocalPlatformContext.current`, not `LocalContext.current` (Android-only; a compile error in `commonMain`). It resolves to `android.content.Context` on Android and `PlatformContext.INSTANCE` on other targets. |
| 8 | |
| 9 | ```kotlin |
| 10 | // commonMain — works on all platforms |
| 11 | AsyncImage( |
| 12 | model = ImageRequest.Builder(LocalPlatformContext.current).data(url).crossfade(true).build(), |
| 13 | contentDescription = "User avatar", |
| 14 | modifier = Modifier.size(48.dp).clip(CircleShape), |
| 15 | ) |
| 16 | ``` |
| 17 | |
| 18 | **Singleton setup (KMP):** call `SingletonImageLoader.setSafe { ImageLoader.Builder(it).crossfade(true).build() }` once early in the app lifecycle, or pass an `imageLoader` explicitly. **`coil-compose` vs `coil-compose-core`:** with `coil-compose`, `AsyncImage` *without* an `imageLoader` argument uses the singleton automatically; with `coil-compose-core`, the `imageLoader` parameter is **required** (no singleton convenience). Add a platform network engine as needed (e.g. `coil-network-ktor3` for KMP). |
| 19 | |
| 20 | ## Two performance guardrails |
| 21 | |
| 22 | **`SubcomposeAsyncImage` is significantly slower than regular composition — never use it inside `LazyColumn` / `LazyRow`.** It creates a subcomposition per item; during scroll that overhead accumulates into visible jank. For custom loading/error states in a list, use `AsyncImage` with `placeholder` / `error` parameters instead — same visual result, no subcomposition. |
| 23 | |
| 24 | **`rememberAsyncImagePainter` does NOT infer the display size from layout constraints** — without an explicit `.size()` it loads the image at its original dimensions (a 4000×3000 photo shown at 64dp still occupies the full bitmap in memory → OOM in a 50-item list). Reach for it only when a `Painter` is strictly required (`Canvas`, `Icon`, a custom draw); always pass `.size()`. Otherwise use `AsyncImage`, which sizes automatically. |
| 25 | |
| 26 | ```kotlin |
| 27 | // only when a Painter is genuinely required — explicit size is mandatory |
| 28 | val painter = rememberAsyncImagePainter( |
| 29 | ImageRequest.Builder(LocalPlatformContext.current).data(url).size(Size(128, 128)).build(), |
| 30 | ) |
| 31 | Image(painter = painter, contentDescription = null) |
| 32 | ``` |