From everything-claude-code-mobile
Provides image loading patterns for Android Compose using Coil: AsyncImage, SubcomposeAsyncImage, caching strategies, transformations, placeholders, error handling. Useful for mobile apps.
How this skill is triggered — by the user, by Claude, or both
Slash command
/everything-claude-code-mobile:image-loadingThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
```kotlin
dependencies {
implementation("io.coil-kt.coil3:coil-compose:3.0.4")
implementation("io.coil-kt.coil3:coil-network-okhttp:3.0.4")
}
AsyncImage(
model = article.imageUrl,
contentDescription = "Article cover image",
contentScale = ContentScale.Crop,
placeholder = painterResource(R.drawable.placeholder),
error = painterResource(R.drawable.error_image),
modifier = Modifier
.fillMaxWidth()
.height(200.dp)
.clip(RoundedCornerShape(12.dp))
)
SubcomposeAsyncImage(
model = user.avatarUrl,
contentDescription = "User avatar",
modifier = Modifier
.size(64.dp)
.clip(CircleShape)
) {
when (painter.state) {
is AsyncImagePainter.State.Loading -> {
ShimmerBox(modifier = Modifier.fillMaxSize())
}
is AsyncImagePainter.State.Error -> {
Icon(
imageVector = Icons.Default.Person,
contentDescription = null,
modifier = Modifier
.fillMaxSize()
.background(MaterialTheme.colorScheme.surfaceVariant)
.padding(16.dp)
)
}
else -> {
SubcomposeAsyncImageContent(contentScale = ContentScale.Crop)
}
}
}
AsyncImage(
model = ImageRequest.Builder(LocalContext.current)
.data(imageUrl)
.crossfade(300)
.size(Size.ORIGINAL)
.memoryCachePolicy(CachePolicy.ENABLED)
.diskCachePolicy(CachePolicy.ENABLED)
.build(),
contentDescription = "Photo",
contentScale = ContentScale.Fit,
modifier = Modifier.fillMaxWidth()
)
AsyncImage(
model = ImageRequest.Builder(LocalContext.current)
.data(user.avatarUrl)
.crossfade(true)
.transformations(
CircleCropTransformation(),
// or RoundedCornersTransformation(16f)
// or BlurTransformation(LocalContext.current, radius = 25f)
)
.build(),
contentDescription = "Avatar",
modifier = Modifier.size(48.dp)
)
// In Application class or Koin module
val imageLoader = ImageLoader.Builder(context)
.memoryCachePolicy(CachePolicy.ENABLED)
.memoryCache {
MemoryCache.Builder()
.maxSizePercent(context, 0.25) // 25% of app memory
.build()
}
.diskCachePolicy(CachePolicy.ENABLED)
.diskCache {
DiskCache.Builder()
.directory(context.cacheDir.resolve("image_cache"))
.maxSizeBytes(100L * 1024 * 1024) // 100 MB
.build()
}
.respectCacheHeaders(true)
.build()
val imageModule = module {
single {
ImageLoader.Builder(androidContext())
.memoryCache {
MemoryCache.Builder()
.maxSizePercent(androidContext(), 0.25)
.build()
}
.diskCache {
DiskCache.Builder()
.directory(androidContext().cacheDir.resolve("image_cache"))
.maxSizeBytes(100L * 1024 * 1024)
.build()
}
.crossfade(true)
.build()
}
}
// In Application.onCreate or Compose root
setSingletonImageLoaderFactory { context ->
get<ImageLoader>() // from Koin
}
// Preload images for better UX (e.g., in list adapter bind)
fun preloadImage(context: Context, url: String) {
val request = ImageRequest.Builder(context)
.data(url)
.size(200, 200)
.memoryCachePolicy(CachePolicy.ENABLED)
.build()
context.imageLoader.enqueue(request)
}
AsyncImage(url: URL(string: article.imageUrl)) { phase in
switch phase {
case .empty:
ProgressView()
.frame(maxWidth: .infinity, minHeight: 200)
case .success(let image):
image
.resizable()
.aspectRatio(contentMode: .fill)
.frame(height: 200)
.clipShape(RoundedRectangle(cornerRadius: 12))
case .failure:
Image(systemName: "photo")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(height: 200)
.foregroundStyle(.secondary)
@unknown default:
EmptyView()
}
}
@Observable
class ImageCache {
static let shared = ImageCache()
private let cache = NSCache<NSString, UIImage>()
private let session = URLSession.shared
init() {
cache.countLimit = 100
cache.totalCostLimit = 50 * 1024 * 1024 // 50 MB
}
func image(for url: URL) async throws -> UIImage {
let key = url.absoluteString as NSString
if let cached = cache.object(forKey: key) {
return cached
}
let (data, _) = try await session.data(from: url)
guard let image = UIImage(data: data) else {
throw ImageError.decodingFailed
}
cache.setObject(image, forKey: key, cost: data.count)
return image
}
func clearCache() {
cache.removeAllObjects()
}
}
struct CachedAsyncImage: View {
let url: URL?
@State private var image: UIImage?
@State private var isLoading = true
var body: some View {
Group {
if let image {
Image(uiImage: image)
.resizable()
.aspectRatio(contentMode: .fill)
} else if isLoading {
ShimmerView()
} else {
Image(systemName: "photo")
.foregroundStyle(.secondary)
}
}
.task {
guard let url else {
isLoading = false
return
}
do {
image = try await ImageCache.shared.image(for: url)
} catch {
// log error
}
isLoading = false
}
}
}
@Composable
fun ShimmerBox(modifier: Modifier = Modifier) {
val transition = rememberInfiniteTransition(label = "shimmer")
val alpha by transition.animateFloat(
initialValue = 0.3f,
targetValue = 0.9f,
animationSpec = infiniteRepeatable(
animation = tween(durationMillis = 1000),
repeatMode = RepeatMode.Reverse
),
label = "shimmer_alpha"
)
Box(
modifier = modifier
.background(
color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = alpha),
shape = RoundedCornerShape(8.dp)
)
)
}
// Usage
ShimmerBox(
modifier = Modifier
.fillMaxWidth()
.height(200.dp)
.clip(RoundedCornerShape(12.dp))
)
@Composable
fun ImageErrorState(
onRetry: (() -> Unit)? = null,
modifier: Modifier = Modifier
) {
Box(
modifier = modifier
.background(MaterialTheme.colorScheme.surfaceVariant),
contentAlignment = Alignment.Center
) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Icon(
imageVector = Icons.Default.BrokenImage,
contentDescription = "Failed to load image",
tint = MaterialTheme.colorScheme.onSurfaceVariant
)
if (onRetry != null) {
TextButton(onClick = onRetry) {
Text("Retry")
}
}
}
}
}
// Coil automatically downsamples, but for manual control:
AsyncImage(
model = ImageRequest.Builder(LocalContext.current)
.data(highResUrl)
.size(400, 300) // downsample to target size
.precision(Precision.INEXACT) // allow slight size differences
.build(),
contentDescription = "Thumbnail",
modifier = Modifier.size(200.dp, 150.dp)
)
// iOS: Downsample large images
func downsample(imageAt url: URL, to pointSize: CGSize, scale: CGFloat) -> UIImage? {
let imageSourceOptions = [kCGImageSourceShouldCache: false] as CFDictionary
guard let imageSource = CGImageSourceCreateWithURL(url as CFURL, imageSourceOptions) else {
return nil
}
let maxDimension = max(pointSize.width, pointSize.height) * scale
let options: [CFString: Any] = [
kCGImageSourceCreateThumbnailFromImageAlways: true,
kCGImageSourceShouldCacheImmediately: true,
kCGImageSourceCreateThumbnailWithTransform: true,
kCGImageSourceThumbnailMaxPixelSize: maxDimension
]
guard let cgImage = CGImageSourceCreateThumbnailAtIndex(imageSource, 0, options as CFDictionary) else {
return nil
}
return UIImage(cgImage: cgImage)
}
// Circle crop avatar
@Composable
fun Avatar(url: String?, size: Dp = 48.dp) {
AsyncImage(
model = ImageRequest.Builder(LocalContext.current)
.data(url)
.crossfade(true)
.transformations(CircleCropTransformation())
.build(),
contentDescription = "User avatar",
placeholder = painterResource(R.drawable.avatar_placeholder),
error = painterResource(R.drawable.avatar_default),
modifier = Modifier
.size(size)
.clip(CircleShape)
.border(2.dp, MaterialTheme.colorScheme.outline, CircleShape)
)
}
// Rounded corners card image
@Composable
fun CardImage(url: String?, modifier: Modifier = Modifier) {
AsyncImage(
model = url,
contentDescription = null,
contentScale = ContentScale.Crop,
placeholder = painterResource(R.drawable.placeholder),
modifier = modifier.clip(RoundedCornerShape(12.dp))
)
}
crossfade(true) for smoother transitions from placeholder to loaded image.ContentScale.Crop for fixed-size containers, ContentScale.Fit for flexible ones.onTrimMemory / didReceiveMemoryWarning).npx claudepluginhub ahmed3elshaer/everything-claude-code-mobile --plugin everything-claude-code-mobileGuides image loading in Compose Compose Multiplatform with Coil 3, covering AsyncImage variants, KMP setup with LocalPlatformContext, and performance guardrails for lists.
Provides modern SwiftUI patterns for PhotosPicker, camera capture, image loading, and privacy permissions for iOS apps.
Provides Jetpack Compose patterns for state hoisting, remember variants, slot APIs, modifiers, side effects, theming, animations, and performance in Android UI development.