GIFアニメ再生のために、Coil3(coil-compose/coil-gif/coil-network-okhttp)を入れた際に対応したりつまずいた箇所がありましたのでまとめました。

更新前のアプリ情報はざっくり以下の通りです。

  • レイアウトはJetpack Compose
  • 既存で画像系ライブラリの利用はなし
  • Kotlin 1.4.3
  • Gradle 8.2(AGP:8.2.0)
  • Java17
  • minSdk 28
  • targetSdk 35

Coil3を利用するためのバージョン更新

既存アプリのKotlin1.xではCoil3を利用できないため、KotlinとGradleのバージョンを上げました。

  • Kotlin: 1.4.3 → 2.1.20
  • Gradle: 8.2(AGP: 8.2.0) → 8.7(AGP: 8.6.0)

Coil3(coil-compose/coil-gif/coil-network-okhttp)はKotlin2.0以降で利用できるため、バージョンを上げる必要がありました。
3.0.0-alpha07からKotlin2.0になっており、結構な更新で必要なバージョンも上がっているため、今回のように、あまり更新していなかったり古いプロジェクトへ導入する際は注意が必要です。
coil-kt:CHANGELOG.md#300-alpha07

当時最新のCoilバージョンである3.2.0に上げたため、Kotlinは2.1.20まで上げ、併せてKotlinバージョンに対応するAGPも更新しています。
Kotlin バージョンに必要な AGP、D8、R8 のバージョン

バージョン更新時の問題と対処法

Gradleのバージョン更新に伴い、依存関係のチェックが厳密になったことから、
Twitter認証のために導入していたライブラリ com.twitter:twitter-api-java-sdk:2.0.3 で使用している javax.ws.rs-apijavax.ws.jsr311-api の競合が発生し、ビルドできなくなってしまいました。
これが一番のつまずきポイントでした。

こちらの対応としては、jsr311-api の方が実態ではなく、ドキュメントのみとのことでしたので以下のように
twitter-apiの依存関係を記載しているbuild.gradle(app)javax.ws.rsjsr311-apiを除外することで解決しました。

implementation("com.twitter:twitter-api-java-sdk:2.0.3") {
    // 以下も依存関係を通すために必要な設定ですが、今回の対応とは無関係のため詳細は省きます。
    exclude group: 'org.apache.oltu.oauth2', module: 'org.apache.oltu.oauth2.common'
    exclude module: 'listenablefuture'
    exclude module: 'guava'

    // **今回、以下を追記**
    exclude group: "javax.ws.rs", module: "jsr311-api"
}

GIF読み込みの実装

利用方法はたくさん記事があると思うので細かい説明は省略します。
ここでのポイントは、modelで設定している rememberedImageRequest と、imageLoaderで指定している gifImageLoader になります。
こちらについては詰まった部分もありましたので後述します。

...
val rememberedImageRequest = remember(imageUri) { imageRequest }

AsyncImage(
    model = rememberedImageRequest,
    contentDescription = "GIF",
    modifier = Modifier.matchParentSize(),
    contentScale = ContentScale.Crop,
    imageLoader = gifImageLoader,
    onSuccess = {
        onSuccessLoadImage(imageUri)
    }
)

gifImageLoader

ImageLoaderはViewModel経由でRepositoryから取得しています。
こちらは固定値なので以下のように何度も作成しないようにしています。

private val gifImageLoaderSingle by lazy {
    ImageLoader.Builder(context)
        .memoryCachePolicy(CachePolicy.ENABLED)
        .diskCachePolicy(CachePolicy.ENABLED)
        .components {
            add(OkHttpNetworkFetcherFactory())
            add(AnimatedImageDecoder.Factory())
        }
        .build()
}

override fun getGifImageLoader(): ImageLoader {
    return gifImageLoaderSingle
}

注意する点としては GifDecoder.Factory() というものも存在しており、こちらでもGIF再生ができるのですが、こちらを使用するとGIFの情報として持っているリピート回数が反映されず常にリピート再生されてしまいました。
(requestに repeatCount(ENCODED_LOOP_COUNT) を設定しても反映されない)
検索してすぐに出てくる情報やAIに聞いた結果だと GifDecoder の方が出てくるのですが、APIレベル28以上では AnimatedImageDecoder を利用するよう記載がありました。
coilドキュメント:gifs

rememberedImageRequest

ImageRequestもViewModel経由でRepositoryから取得しています。

override fun getImageRequest(imageUri: Uri): ImageRequest {
    return ImageRequest.Builder(context)
        .repeatCount(ENCODED_LOOP_COUNT)
        .data(imageUri)
        .build()
}

AsyncImageのmodelにGIFのURIを読んだこのImageRequestを使えばGIFアニメは再生されます。
しかし、アニメーション再生が何度も行われるケースがあります。
Jetpack Composeで実装したことがある方はもう気付いたと思いますが、再コンポーズによって 1 から再生がやり直されるために起こります。
自分は当初これに気づかず、そのままImageRequestを呼び出していました……。
remember(imageUri) { imageRequest } を使うことで imageUri に変更がない場合のみGIFの再読み込みをするようにしています。

さいごに

ほとんどが簡単な対応でしたが意外とつまずきポイントがありました。
AnimatedImageDecoderの利用に関しては今のとりあえずAIに任せてみる時代においても公式ドキュメントのような一次情報も併せて確認を怠らないようにしようという気持ちにさせられました。



ギャップロを運営しているアップフロンティア株式会社では、一緒に働いてくれる仲間を随時、募集しています。 興味がある!一緒に働いてみたい!という方は下記よりご応募お待ちしております。
採用情報をみる