jpskill.com
🛠️ 開発・MCP コミュニティ

image-loading

AndroidやiOSのモバイルアプリで、画像を効率的に表示するための技術(Coilやasync image loadingなど)を活用し、キャッシュや変形、エラー処理などを適切に行うことで、ユーザー体験を向上させるSkill。

📜 元の英語説明(参考)

Image loading patterns for mobile - Coil for Android/Compose, async image loading for iOS, caching strategies, transformations, placeholders, and error handling.

🇯🇵 日本人クリエイター向け解説

一言でいうと

AndroidやiOSのモバイルアプリで、画像を効率的に表示するための技術(Coilやasync image loadingなど)を活用し、キャッシュや変形、エラー処理などを適切に行うことで、ユーザー体験を向上させるSkill。

※ jpskill.com 編集部が日本のビジネス現場向けに補足した解説です。Skill本体の挙動とは独立した参考情報です。

⚡ おすすめ: コマンド1行でインストール(60秒)

下記のコマンドをコピーしてターミナル(Mac/Linux)または PowerShell(Windows)に貼り付けてください。 ダウンロード → 解凍 → 配置まで全自動。

🍎 Mac / 🐧 Linux
mkdir -p ~/.claude/skills && cd ~/.claude/skills && curl -L -o image-loading.zip https://jpskill.com/download/16414.zip && unzip -o image-loading.zip && rm image-loading.zip
🪟 Windows (PowerShell)
$d = "$env:USERPROFILE\.claude\skills"; ni -Force -ItemType Directory $d | Out-Null; iwr https://jpskill.com/download/16414.zip -OutFile "$d\image-loading.zip"; Expand-Archive "$d\image-loading.zip" -DestinationPath $d -Force; ri "$d\image-loading.zip"

完了後、Claude Code を再起動 → 普通に「動画プロンプト作って」のように話しかけるだけで自動発動します。

💾 手動でダウンロードしたい(コマンドが難しい人向け)
  1. 1. 下の青いボタンを押して image-loading.zip をダウンロード
  2. 2. ZIPファイルをダブルクリックで解凍 → image-loading フォルダができる
  3. 3. そのフォルダを C:\Users\あなたの名前\.claude\skills\(Win)または ~/.claude/skills/(Mac)へ移動
  4. 4. Claude Code を再起動

⚠️ ダウンロード・利用は自己責任でお願いします。当サイトは内容・動作・安全性について責任を負いません。

🎯 このSkillでできること

下記の説明文を読むと、このSkillがあなたに何をしてくれるかが分かります。Claudeにこの分野の依頼をすると、自動で発動します。

📦 インストール方法 (3ステップ)

  1. 1. 上の「ダウンロード」ボタンを押して .skill ファイルを取得
  2. 2. ファイル名の拡張子を .skill から .zip に変えて展開(macは自動展開可)
  3. 3. 展開してできたフォルダを、ホームフォルダの .claude/skills/ に置く
    • · macOS / Linux: ~/.claude/skills/
    • · Windows: %USERPROFILE%\.claude\skills\

Claude Code を再起動すれば完了。「このSkillを使って…」と話しかけなくても、関連する依頼で自動的に呼び出されます。

詳しい使い方ガイドを見る →
最終更新
2026-05-18
取得日時
2026-05-18
同梱ファイル
1

📖 Skill本文(日本語訳)

※ 原文(英語/中国語)を Gemini で日本語化したものです。Claude 自身は原文を読みます。誤訳がある場合は原文をご確認ください。

モバイル向けの画像読み込みパターン

依存関係 (Android - Coil)

dependencies {
    implementation("io.coil-kt.coil3:coil-compose:3.0.4")
    implementation("io.coil-kt.coil3:coil-network-okhttp:3.0.4")
}

Android / Compose with Coil

AsyncImage Composable

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

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)
        }
    }
}

ImageRequest Builder

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()
)

Transformations

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)
)

カスタム ImageLoader 設定

// 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()

Coil + Koin 連携

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
}

画像のプリロード

// より良い UX のために画像をプリロードします (例: リストアダプターのバインド時)
fun preloadImage(context: Context, url: String) {
    val request = ImageRequest.Builder(context)
        .data(url)
        .size(200, 200)
        .memoryCachePolicy(CachePolicy.ENABLED)
        .build()
    context.imageLoader.enqueue(request)
}

iOS / SwiftUI

AsyncImage

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()
    }
}

再利用可能な CachedAsyncImage View


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 {


(原文がここで切り詰められています)
📜 原文 SKILL.md(Claudeが読む英語/中国語)を展開

Image Loading Patterns for Mobile

Dependencies (Android - Coil)

dependencies {
    implementation("io.coil-kt.coil3:coil-compose:3.0.4")
    implementation("io.coil-kt.coil3:coil-network-okhttp:3.0.4")
}

Android / Compose with Coil

AsyncImage Composable

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 for Custom States

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)
        }
    }
}

ImageRequest Builder

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()
)

Transformations

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)
)

Custom ImageLoader Configuration

// 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()

Coil + Koin Integration

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
}

Preloading Images

// 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)
}

iOS / SwiftUI

AsyncImage

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()
    }
}

Custom Async Image Loader with Caching

@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()
    }
}

Reusable CachedAsyncImage View

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
        }
    }
}

Cross-Platform Patterns

Shimmer / Placeholder Effect (Compose)

@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))
)

Error State Component

@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")
                }
            }
        }
    }
}

Memory Management - Downsampling

// 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)
}

Image Transformation Patterns

// 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))
    )
}

Best Practices

  • Always provide placeholder and error drawables for every image load.
  • Use crossfade(true) for smoother transitions from placeholder to loaded image.
  • Configure disk cache size based on app needs (50-200 MB typical).
  • Set memory cache to 20-25% of available app memory.
  • Downsample images to the display size; never load a 4000px image into a 200dp view.
  • Use ContentScale.Crop for fixed-size containers, ContentScale.Fit for flexible ones.
  • Preload images for items about to scroll into view in lists.
  • Clear caches on low-memory warnings (onTrimMemory / didReceiveMemoryWarning).
  • For lists, set explicit sizes on image composables to prevent layout jumps during load.
  • Use shimmer effects instead of spinner placeholders for a more polished loading experience.