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()
}
}
参考
- yoonseopshin/kicpa-wordquiz (opens in a new tab): ネイティブ広告を Jetpack Compose で表示する方法については、このリポジトリを参考にしました。