LazyColumn に AdMob を表示する

# はじめに

アプリの収益化手段として、[AdMob](https://developers.google.com/admob) などのネットワーク広告を表示する方法があります。また、2024年現在 Android アプリの UI 構築には多くのアプリで [Jetpack Compose](https://developer.android.com/compose) が採用されています。しかし AdMob をはじめとする多くのネットワーク広告 SDK は View でのみ提供されています。そのため、Jetpack Compose で構築された UI に AdMob の広告を表示するには、[相互運用 API](https://developer.android.com/develop/ui/compose/migrate/interoperability-apis) を活用する必要があります。

# Jetpack Compose に AdMob を表示する

Jetpack Compose に View を表示する方法は公式ドキュメントの [Compose でビューを使用する](https://developer.android.com/develop/ui/compose/migrate/interoperability-apis/views-in-compose)に記載されています。この手順に従い [AndroidView](https://developer.android.com/reference/kotlin/androidx/compose/ui/viewinterop/package-summary#AndroidView(kotlin.Function1,androidx.compose.ui.Modifier,kotlin.Function1)) Composesable 関数を使用して AdMob を表示します。

```kotlin
AndroidView(
    modifier = Modifier
        .fillMaxWidth()
        .height(50.dp),
    factory = { context ->
        val adRequest = AdRequest.Builder().build()
        val adView = AdView(context)
        adView.adUnitId = "ca-app-pub-3940256099942544/9214589741"
        adView.setAdSize(AdSize.BANNER)
        adView.loadAd(adRequest)
        adView
    },
)
```

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/334798/21384a9b-8de1-5ec7-717f-b484647f1936.png" width="320">

※ adUnitId は[テスト用](https://developers.google.com/admob/android/banner?hl=ja#always_test_with_test_ads)

# LazyColumn に AdMob を表示する

LazyColumn の要素として AdMob を表示する場合、注意点がひとつあります。上記の Composable 関数を LazyColumn の要素として使用すると、広告は一応表示されますが、スクロールアウトして再度スクロールインするたびに AdView インスタンスが生成され、コンテンツの再読み込みが行われてしまいます。このため、広告が真っ白な状態で表示される時間が長くなり、クリックされにくくなる可能性があります。

![output.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/334798/6ae3575c-9461-2991-45df-64d85b2c14e0.gif)

そこで公式ドキュメントの [Compose でビューを使用する](https://developer.android.com/develop/ui/compose/migrate/interoperability-apis/views-in-compose)に戻ると、[遅延リストの AndroidView](https://developer.android.com/develop/ui/compose/migrate/interoperability-apis/views-in-compose?_gl=1*b5kpoi*_up*MQ..*_ga*MTMwNjUwMzc4Ny4xNzI4OTA2MDY2*_ga_6HH9YJMN9M*MTcyODkwNjA2Ni4xLjAuMTcyODkwNjA2Ni4wLjAuMTI5NTE4MzM4Mw..#androidview_in_lazy_lists) という節がありました。こちらにはこのように書かれていました。和訳が機械翻訳で分かりにくかったため、原文を自分で意訳しました。

> This overload allows Compose to reuse the underlying View instance when the containing composition is reused as is the case for Lazy lists.

Lazy List のようにコンポジションが再利用されるとき、View インスタンスは再利用されます。

> onReset - A callback invoked to signal that the View is about to be reused. This must be non-null to enable View reuse.

onReset - View が再利用される時に呼ばれます。View の再利用を有効にするときは、non-null にする必要があります。

よって、onReset メソッドに対して non-null を設定します。

```kotlin
AndroidView(
    modifier = Modifier
        .fillMaxWidth()
        .height(50.dp),
    factory = { context ->
        val adRequest = AdRequest.Builder().build()
        val adView = AdView(context)
        adView.adUnitId = "ca-app-pub-3940256099942544/9214589741"
        adView.setAdSize(AdSize.BANNER)
        adView.loadAd(adRequest)
        adView
    },
    // 追加
    onReset = { },
)
```

その結果、AdView も再利用されるため、スクロールアウトして再度スクロールインしても、最初から広告が表示されるようになりました。

![output2.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/334798/8293b7db-f869-949e-2e11-f03eabb24b53.gif)

# バージョン 1.4.0-rc01 より前の Compose では、View の再利用を自作する必要があった

さて Lazy List による View インスタンスの再利用は [遅延リストの AndroidView](https://developer.android.com/develop/ui/compose/migrate/interoperability-apis/views-in-compose?_gl=1*b5kpoi*_up*MQ..*_ga*MTMwNjUwMzc4Ny4xNzI4OTA2MDY2*_ga_6HH9YJMN9M*MTcyODkwNjA2Ni4xLjAuMTcyODkwNjA2Ni4wLjAuMTI5NTE4MzM4Mw..#androidview_in_lazy_lists) に記載されているように、バージョン 1.4.0-rc01 ([2023年3月8日](https://developer.android.com/jetpack/androidx/releases/compose-runtime?hl=ja&_gl=1*12u16r6*_up*MQ..*_ga*MTg2Mjg5MTQ2Ni4xNzI4OTAzNzQx*_ga_6HH9YJMN9M*MTcyODkwMzc0MC4xLjAuMTcyODkwMzc0MC4wLjAuMTMwMzg4OTM4Nw..#1.4.0-rc01))で導入されたものです。この節では、それより前はどのように LazyColumn に AdMob を表示していたのかを紹介します。

独自に View インスタンス再利用の仕組みを構築していました。

まずは View インスタンスを [Queue](https://ja.wikipedia.org/wiki/%E3%82%AD%E3%83%A5%E3%83%BC_(%E3%82%B3%E3%83%B3%E3%83%94%E3%83%A5%E3%83%BC%E3%82%BF)) で管理するクラスを作成します。

```kotlin
abstract class ViewRecycler<V : View, P> {
    private val viewQueue = mutableListOf<V>()

    /**
     * View インスタンスを作るか、キューから取り出す
     */
    fun createOrDequeue(context: Context, param: P): V {
        return if (viewQueue.isEmpty()) {
            // キューが空の時は View を作って返却する
            createView(context, param)
        } else {
            // キューから取り出して返却する
            viewQueue.removeAt(0)
        }
    }

    /**
     * View インスタンスを作成する
     */
    abstract fun createView(context: Context, param: P): V

    /**
     * 使用が終わった View をキューに追加する
     */
    fun enqueue(view: V) {
        viewQueue.add(view)
    }
}
```

こちらは、様々な View に対応した abstract なクラスなので、継承して AdView クラスに対応したクラスを作成します。

```kotlin
class AdViewRecycler : ViewRecycler<AdView, Unit>() {
    override fun createView(context: Context, param: Unit): AdView {
        val adRequest = AdRequest.Builder().build()
        val adView = AdView(context)
        adView.adUnitId = "ca-app-pub-3940256099942544/9214589741"
        adView.setAdSize(AdSize.BANNER)
        adView.loadAd(adRequest)
        adView
    }
}
```


AdViewRecycler クラスのインスタンスは画面 の Composable 関数など、LazyColumn が表示されている間は生存するように設置します。

```kotlin
val adViewRecycler = remember {
    AdViewRecycler()
}
````

それを使い LazyColumn の要素にする AndroidView Composable 関数は、このように使います。

```kotlin
@Composable
fun RecyclableAdMobBannerAndroidView(adViewRecycler: AdViewRecycler) {
    AndroidView(
        modifier = Modifier
            .fillMaxWidth()
            .height(50.dp),
        factory = { context ->
            val frameLayout = FrameLayout(context)
            frameLayout.addOnAttachStateChangeListener(object : View.OnAttachStateChangeListener {
                override fun onViewAttachedToWindow(view: View) {
                    // FrameLayout が Attach されたら、AdView を FrameLayout に追加する
                    if (frameLayout.childCount == 0) {
                        // AdView は作成するか Queue から取り出す
                        val adView = adViewRecycler.createOrDequeue(context, Unit)
                        frameLayout.addView(adView)
                    }
                }

                override fun onViewDetachedFromWindow(view: View) {
                    // FrameLayout が Detach されたら、AdView を FrameLayout から取り除いて Queue に戻す
                    if (frameLayout.childCount >= 1) {
                        val adView = frameLayout.getChildAt(0) as AdView
                        // 親 GroupView から取り除かないと、次の追加でエラーになる
                        frameLayout.removeView(adView)
                        adViewRecycler.enqueue(adView)
                    }
                }
            })
            frameLayout
        }
    )
}
```

# まとめ

- LazyColumn に AdMob を表示するときは、再利用の仕組みが有効になっているかに注意する必要があります。
- 再利用の仕組みは Compose 1.4.0-rc1 から標準搭載されています。
- リリース日である2023年3月8日から1年半が経過しており、遅れた感のある記事ですが、Compose の進化を感じていただけると幸いです。