AdMobのネイティブ広告をJetpack Composeで表示し、プリロード対応する

概要

AdMob のネイティブ広告 (opens in a new tab)は、アプリに表示される広告のデザインをカスタマイズできる広告フォーマットです。

ただし、Google Mobile Ads SDK は Jetpack Compose に対応していないので、広告のレイアウトは従来の xml で組む必要があります。 また、広告表示のタイムラグをなくすためには、広告を事前に取得(プリロード)するのが有効です。

この記事では具体的な実装方法を説明します。

完成例

小サイズ広告中サイズ広告

事前準備

以下を参照し、native ad templates をプロジェクトにインストールします。

Installing the native ad templates (opens in a new tab)

ソースコード

ネイティブ広告をプリロードするクラス

NativeAdPreloader.kt
import android.content.Context
import com.google.android.gms.ads.AdLoader
import com.google.android.gms.ads.AdRequest
import com.google.android.gms.ads.nativead.NativeAd
import com.google.android.gms.ads.nativead.NativeAdOptions
import java.time.LocalDateTime
 
data class PreloadedNativeAd(
    val ad: NativeAd,
    val date: LocalDateTime,
)
 
/** ネイティブ広告をプリロードするクラス */
class NativeAdPreloader(context: Context) {
    private var preloadedNativeAds: MutableList<PreloadedNativeAd> = mutableListOf()
    private var adLoader: AdLoader
 
    init {
        adLoader =
            AdLoader.Builder(context, AdMobUtil.getNativeAdUnitId())
                .forNativeAd { nativeAd ->
                    preloadedNativeAds.add(PreloadedNativeAd(nativeAd, LocalDateTime.now()))
                }
                .withNativeAdOptions(NativeAdOptions.Builder().build())
                .build()
    }
 
    fun preloadAd() {
        if (adLoader.isLoading) {
            return
        }
        removeExpiredAds()
        val numberOfAds = NUMBER_OF_PRELOAD_ADS - preloadedNativeAds.count()
        if (numberOfAds > 0) {
            adLoader.loadAds(AdRequest.Builder().build(), numberOfAds)
        }
    }
 
    fun popAd(): NativeAd? {
        removeExpiredAds()
        val first = preloadedNativeAds.firstOrNull()
        return if (first != null) {
            preloadedNativeAds.removeFirst()
            // ここでプリロードしてしまうと、フリークエンシーキャップを設定している場合の効果がなくなる.
            // 従って、広告を表示する少し前にプリロードするのが望ましい
            // preloadAd()
            first.ad
        } else {
            preloadAd()
            null
        }
    }
 
    /** 期限切れのNativeAdを削除する */
    private fun removeExpiredAds() {
        val adLimitDate = LocalDateTime.now().minusHours(1)
        preloadedNativeAds = preloadedNativeAds.filter { it.date > adLimitDate }.toMutableList()
    }
 
    companion object {
        const val NUMBER_OF_PRELOAD_ADS = 3
    }
}
 
AdMobUtil.kt
object AdMobUtil {
    fun getNativeAdUnitId(): String {
        return if (BuildConfig.DEBUG) {
            "ca-app-pub-3940256099942544/2247696110" // テスト用
        } else {
            "YOUR_AD_UNIT_ID" // 本番用
        }
    }
}

ネイティブ広告の JetpackCompose のコンポーネント

NativeAd.kt
 
import android.content.res.ColorStateList
import android.util.Log
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.viewinterop.AndroidViewBinding
import com.google.android.gms.ads.AdLoader
import com.google.android.gms.ads.AdRequest
import com.google.android.gms.ads.nativead.NativeAd
import com.google.android.gms.ads.nativead.NativeAdOptions
import com.kyamada.listentonovels.androidApp.MyApplication
import com.kyamada.listentonovels.androidApp.databinding.LayoutNativeAdMediumBinding
import com.kyamada.listentonovels.androidApp.databinding.LayoutNativeAdSmallBinding
import com.kyamada.listentonovels.androidApp.model.ad.AdMobUtil
 
data class NativeAdColorSet(
    val text: Color,
    val buttonText: Color,
    val buttonBackground: Color,
    val adText: Color,
)
 
@Composable
fun NativeSmallAd(colorSet: NativeAdColorSet? = null) {
    AndroidViewBinding(factory = { inflater, parent, attachToParent ->
        val binding = LayoutNativeAdSmallBinding.inflate(inflater, parent, attachToParent)
 
        val adView = binding.root.also { adView ->
            adView.bodyView = binding.tvBody
            adView.callToActionView = binding.btnCta
            adView.headlineView = binding.tvHeadline
            adView.iconView = binding.ivAppIcon
            adView.storeView = binding.tvStore
        }
 
        fun setupNativeAd(nativeAd: NativeAd) {
            nativeAd.body?.let { binding.tvBody.text = it }
            nativeAd.callToAction?.let { binding.btnCta.text = it }
            nativeAd.headline?.let { binding.tvHeadline.text = it }
            nativeAd.icon?.let { binding.ivAppIcon.setImageDrawable(it.drawable) }
            nativeAd.store?.let { binding.tvStore.text = it }
            adView.setNativeAd(nativeAd)
        }
 
        colorSet?.let {
            binding.tvBody.setTextColor(it.text.toArgb())
            binding.btnCta.setTextColor(it.buttonText.toArgb())
            binding.btnCta.backgroundTintList =
                ColorStateList.valueOf(it.buttonBackground.toArgb())
            binding.tvHeadline.setTextColor(it.text.toArgb())
            binding.tvStore.setTextColor(it.text.toArgb())
        }
 
        val preloadedNativeAd = MyApplication.INSTANCE.nativeAdPreloader.popAd()
        if (preloadedNativeAd != null) {
            setupNativeAd(preloadedNativeAd)
        } else {
            val adLoader = AdLoader.Builder(adView.context, AdMobUtil.getNativeAdUnitId())
                .forNativeAd { nativeAd ->
                    setupNativeAd(nativeAd)
                }
                .withNativeAdOptions(NativeAdOptions.Builder().build())
                .build()
            adLoader.loadAd(AdRequest.Builder().build())
        }
 
        binding
    })
}
 
@Preview(showBackground = true)
@Composable
private fun NativeSmallAdPreview() {
    NativeSmallAd()
}
 
 
@Composable
fun NativeMediumAd(colorSet: NativeAdColorSet? = null) {
    AndroidViewBinding(
        factory = { inflater, parent, attachToParent ->
            val binding = LayoutNativeAdMediumBinding.inflate(inflater, parent, attachToParent)
 
            val adView = binding.root.also { adView ->
                adView.advertiserView = binding.tvAdvertiser
                adView.bodyView = binding.tvBody
                adView.callToActionView = binding.btnCta
                adView.headlineView = binding.tvHeadline
                adView.iconView = binding.ivAppIcon
                adView.priceView = binding.tvPrice
                adView.starRatingView = binding.rtbStars
                adView.storeView = binding.tvStore
                adView.mediaView = binding.mvContent
            }
 
            colorSet?.let {
                binding.tvAdvertiser.setTextColor(it.text.toArgb())
                binding.tvBody.setTextColor(it.text.toArgb())
                binding.btnCta.setTextColor(it.buttonText.toArgb())
                binding.btnCta.backgroundTintList =
                    ColorStateList.valueOf(it.buttonBackground.toArgb())
                binding.tvHeadline.setTextColor(it.text.toArgb())
                binding.tvPrice.setTextColor(it.text.toArgb())
                binding.tvStore.setTextColor(it.text.toArgb())
                binding.tvAd.setTextColor(it.adText.toArgb())
            }
 
            fun setupNativeAd(nativeAd: NativeAd) {
                nativeAd.advertiser?.let { advertiser ->
                    binding.tvAdvertiser.text = advertiser
                }
 
                nativeAd.body?.let { body ->
                    binding.tvBody.text = body
                }
 
                nativeAd.callToAction?.let { cta ->
                    binding.btnCta.text = cta
                }
 
                nativeAd.headline?.let { headline ->
                    binding.tvHeadline.text = headline
                }
 
                nativeAd.icon?.let { icon ->
                    binding.ivAppIcon.setImageDrawable(icon.drawable)
                }
 
                nativeAd.price?.let { price ->
                    binding.tvPrice.text = price
                }
 
                nativeAd.starRating?.let { rating ->
                    binding.rtbStars.rating = rating.toFloat()
                }
 
                nativeAd.store?.let { store ->
                    binding.tvStore.text = store
                }
                adView.setNativeAd(nativeAd)
            }
 
            val preloadedNativeAd = MyApplication.INSTANCE.nativeAdPreloader.popAd()
            if (preloadedNativeAd != null) {
                setupNativeAd(preloadedNativeAd)
            } else {
                val adLoader = AdLoader.Builder(adView.context, AdMobUtil.getNativeAdUnitId())
                    .forNativeAd { nativeAd ->
                        setupNativeAd(nativeAd)
                    }
                    .withNativeAdOptions(NativeAdOptions.Builder().build())
                    .build()
                adLoader.loadAd(AdRequest.Builder().build())
            }
 
            binding
        }
    )
}
 
@Preview(showBackground = true)
@Composable
private fun NativeMediumAdPreview() {
    NativeMediumAd()
}

Application のサブクラス。 NativeAdPreloader を初期化する。

MyApplication.kt
class MyApplication : Application() {
    lateinit var nativeAdPreloader: NativeAdPreloader
 
    override fun onCreate() {
        super.onCreate()
        INSTANCE = this
        nativeAdPreloader = NativeAdPreloader(this)
        nativeAdPreloader.preloadAd()
    }
 
    companion object {
        internal lateinit var INSTANCE: MyApplication
            private set
    }
}

リソースファイル

小サイズの広告レイアウト

res/layout/layout_native_ad_small.xml
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.gms.ads.nativead.NativeAdView xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="wrap_content">
 
    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:paddingBottom="6dp">
 
        <ImageView
            android:id="@+id/iv_app_icon"
            android:layout_width="24dp"
            android:layout_height="24dp"
            android:layout_marginStart="16dp"
            android:layout_marginTop="6dp"
            android:adjustViewBounds="true"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            tools:ignore="ContentDescription" />
 
        <TextView
            android:id="@+id/tv_headline"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_marginStart="8dp"
            android:layout_marginEnd="8dp"
            android:ellipsize="end"
            android:maxLines="2"
            android:minWidth="100dp"
            android:textSize="14sp"
            android:textStyle="bold"
            app:layout_constraintBottom_toBottomOf="@+id/iv_app_icon"
            app:layout_constraintEnd_toStartOf="@id/layout_action"
            app:layout_constraintStart_toEndOf="@id/iv_app_icon"
            app:layout_constraintTop_toTopOf="@id/iv_app_icon"
            tools:text="Test Ad : Google Ads" />
 
        <LinearLayout
            android:id="@+id/layout_action"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="6dp"
            android:layout_marginEnd="16dp"
            android:orientation="vertical"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintTop_toTopOf="parent">
 
            <Button
                android:id="@+id/btn_cta"
                android:layout_width="wrap_content"
                android:layout_height="48dp"
                android:backgroundTint="@color/ad_button_background"
                android:gravity="center"
                android:textColor="@color/ad_button_text"
                android:textSize="12sp"
                tools:text="INSTALL" />
 
            <TextView
                android:id="@+id/tv_store"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:gravity="center"
                android:maxLines="1"
                android:textSize="12sp"
                tools:text="Google Play" />
 
        </LinearLayout>
 
        <TextView
            android:id="@+id/tv_body"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_marginStart="16dp"
            android:layout_marginTop="6dp"
            android:layout_marginEnd="12dp"
            android:ellipsize="end"
            android:maxLines="2"
            android:minHeight="32dp"
            android:paddingHorizontal="4dp"
            android:textSize="12sp"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toStartOf="@+id/layout_action"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@id/iv_app_icon"
            tools:text="Stay up to date with your Ads Check how your ads are performing" />
 
    </androidx.constraintlayout.widget.ConstraintLayout>
 
</com.google.android.gms.ads.nativead.NativeAdView>

中サイズの広告レイアウト

res/layout/layout_native_ad_medium.xml
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.gms.ads.nativead.NativeAdView xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="wrap_content">
 
    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:minHeight="60dp">
 
        <TextView
            android:id="@+id/tv_ad"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:background="@drawable/ad_rounded_corners_shape"
            android:paddingHorizontal="4dp"
            android:paddingVertical="1dp"
            android:text="@string/ad"
            android:textColor="@color/ad_green"
            android:textSize="10sp"
            android:textStyle="bold"
            app:layout_constraintBottom_toTopOf="@id/tv_body"
            app:layout_constraintEnd_toEndOf="@id/tv_body"
            tools:ignore="SmallSp" />
 
        <ImageView
            android:id="@+id/iv_app_icon"
            android:layout_width="48dp"
            android:layout_height="48dp"
            android:layout_marginStart="16dp"
            android:layout_marginTop="24dp"
            android:adjustViewBounds="true"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            tools:ignore="ContentDescription" />
 
        <TextView
            android:id="@+id/tv_headline"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_marginStart="8dp"
            android:layout_marginTop="24dp"
            android:layout_marginEnd="8dp"
            android:ellipsize="end"
            android:maxLines="2"
            android:minWidth="100dp"
            android:textSize="16sp"
            android:textStyle="bold"
            app:layout_constraintBottom_toBottomOf="@+id/iv_app_icon"
            app:layout_constraintEnd_toStartOf="@id/layout_action"
            app:layout_constraintStart_toEndOf="@id/iv_app_icon"
            app:layout_constraintTop_toTopOf="parent"
            tools:text="Test Ad : Google Ads" />
 
        <LinearLayout
            android:id="@+id/layout_action"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="24dp"
            android:layout_marginEnd="16dp"
            android:orientation="vertical"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintTop_toTopOf="parent">
 
            <Button
                android:id="@+id/btn_cta"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:backgroundTint="@color/ad_button_background"
                android:gravity="center"
                android:textColor="@color/ad_button_text"
                android:textSize="12sp"
                tools:text="INSTALL" />
 
            <TextView
                android:id="@+id/tv_store"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:maxLines="1"
                android:paddingHorizontal="4dp"
                android:paddingVertical="2dp"
                android:textSize="12sp"
                tools:text="Google Play" />
 
            <TextView
                android:id="@+id/tv_price"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:maxLines="1"
                android:paddingHorizontal="4dp"
                android:paddingVertical="2dp"
                android:textSize="12sp"
                tools:text="Free" />
 
        </LinearLayout>
 
        <TextView
            android:id="@+id/tv_advertiser"
            android:layout_width="wrap_content"
            android:layout_height="match_parent"
            android:layout_marginStart="16dp"
            android:layout_marginTop="4dp"
            android:textSize="14sp"
            android:textStyle="bold"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@id/iv_app_icon" />
 
        <RatingBar
            android:id="@+id/rtb_stars"
            style="?android:attr/ratingBarStyleSmall"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:isIndicator="true"
            android:numStars="5"
            android:stepSize="0.5"
            app:layout_constraintBottom_toBottomOf="@id/tv_advertiser"
            app:layout_constraintStart_toEndOf="@id/tv_advertiser"
            app:layout_constraintTop_toTopOf="@id/tv_advertiser"
            tools:rating="4.5" />
 
        <TextView
            android:id="@+id/tv_body"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_marginStart="16dp"
            android:layout_marginEnd="12dp"
            android:paddingVertical="2dp"
            android:textSize="12sp"
            app:layout_constraintEnd_toStartOf="@+id/layout_action"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@id/rtb_stars"
            tools:text="Stay up to date with your Ads Check how your ads are performing" />
 
        <com.google.android.gms.ads.nativead.MediaView
            android:id="@+id/mv_content"
            android:layout_width="0dp"
            android:layout_height="180dp"
            android:layout_gravity="center_horizontal"
            android:layout_marginHorizontal="16dp"
            android:layout_marginTop="5dp"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@id/tv_body" />
 
    </androidx.constraintlayout.widget.ConstraintLayout>
 
</com.google.android.gms.ads.nativead.NativeAdView>

広告で使用する色の定義

res/values/colors.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <color name="ad_button_text">#FF33691E</color>
    <color name="ad_button_background">#0F689F38</color>
    <color name="ad_green">#FF2E7D32</color>
</resources>
res/values-night/colors.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <color name="ad_button_text">#FFB8B8B8</color>
    <color name="ad_button_background">#FF091C80</color>
    <color name="ad_green">#FF1B5E20</color>
</resources>

使用例

PlayerScreen.kt
LazyColumn(modifier = Modifier.weight(1f, true)) {
    item {
        NativeSmallAd()
    }
    // ....
    item {
        NativeMediumAd()
    }
}

参考

関連記事

© 品川アプリ.RSS