Help us understand the problem. What is going on with this article?

詳解AdMob

広告に関する実装をしてきて、やっと納得のいく実装ができた気がしたので備忘録的にまとめようと思います。
今後モバイルアプリに広告の導入を検討されている方の手助けになれば幸いです。
ユーザー影響大きいのでご利用は計画的に...

下記のような実装は経験できたので関連でもし質問などあれば回答できることもあるかもしれません。

  • バナー広告
    • メディエーションする
    • ヘッダービディングをラップする
    • RecyclerViewの中で使う
    • RemoteConfigと連携する
  • ネイティブ広告
    • メディエーションする
    • RecyclerViewの中で使う
    • Firebase A/B Testingと連携する
  • インタースティシャル広告
    • メディエーションする
    • RemoteConfigと連携する

AdMobについて(3行)

  • Firebase機能群の1つで、アプリ内に広告を表示することができる機能
  • 複数の広告フォーマットに対応しており、他社の広告をAdMobの中で表示するといったことも可能
  • Firebase(Google開発)提供なのでAndroidアプリではAdMobをまずは検討する(はず)

この記事で触れないもの

  • 公式ドキュメントを見てもらえば簡単にわかってしまうこと
  • AdMob以外の広告SDK仕様
  • アドテク用語
    • 全部知っておかないと開発できないわけではないと思います。
    • 本記事の用語集としてリンク先に一読はおすすめします
  • 収益に関わるパフォーマンスチューニングのTips
    • 業種やアプリの規模、採用しているアドネットワークによって全く異なるので言及しません。
  • AdMob管理画面の使い方
    • ちゃんと触ってません!
  • マルチモジュールを使った広告管理
  • CoroutineFlowを使った広告管理
    • こういうのやってみたい....
  • 実装時の開発以外でのつらみ
  • 広告SDKの内部実装について
    • 広告のSDKは大半が実装部分は難読化されていて読み解くことが難しいです。なので以下でつらつら書いていることも動作確認した時の動きだったりおそらくこういうことだろう、という考察で説明しているところも多いので実際は間違っているということもあるかもしれませんのでご理解ください。

基本編

おすすめの実装、ハマりそうなポイントなど

広告の表示

AdMobは機能自体はそこまで多くないので公式ドキュメントを一周してもらえればある程度の使い方を理解するのは簡単だと思います。テスト用の枠IDは公式で用意されているのでアプリを識別するためのAppIDが発行できればすぐ表示まで確認できると思います。

公式ドキュメント
https://developers.google.com/admob?hl=ja

実装メモ
  • AdView に使う Context は必ず Activity である必要があります
  • TestMode(広告を読み込んでも収益が発生しない状態)にするには端末ごとに異なるキーを AdRequest.Builder.addTestDevice() に設定して AdView をロードする必要があります
  • AppIDはAndroidManifest.xmlに参照がないとアプリがクラッシュします:construction_worker_tone4:
  • バナー広告のサイズ一覧(https://developers.google.com/admob/android/banner?hl=ja#banner_sizes)
Size in dp (WxH) Description Availability AdSize constant
320x50 Banner Phones and Tablets BANNER
320x100 Large Banner Phones and Tablets LARGE_BANNER
300x250 IAB Medium Rectangle Phones and Tablets MEDIUM_RECTANGLE
468x60 IAB Full-Size Banner Tablets FULL_BANNER
728x90 IAB Leaderboard Tablets LEADERBOARD
Provided width x Adaptive height Adaptive banner Phones and Tablets N/A
Screen width x 32 50 90 Smart banner
  • テスト用の枠IDで広告を読み込んで以下のように表示されたら成功です!
    • Bannerの場合

Screenshot_20191128-204011のコピー.png

  • Rectangleの場合

Screenshot_20191128-204011.png

広告はAACのViewModelで管理しない方が良い?

上でも書いたのですが、広告をインスタンス化する時はActivityContextが必要になります。
AndroidViewModelから取れるApplicationクラスのContextを使うことはできません。
また、外からActivityContextを入れてもViewModelはActivity/Fragmentよりも長生きするのでメモリーリークの原因になり得ます。

ViewModelLifecycleObserver を付けてON_DESTROYのイベントを受け取った時に広告インスタンスを破棄したりすればまあいけるのかもしれないですね。
こういう時ってActivityのインスタンスをDIでViewModelにInjectして使っても問題にならないのでしょうか?(詳しい方教えてください:joy:)

ただ、バナー広告は基本的にキャッシュされることを想定されていないので、Fragmentをまたいで広告インスタンスを使い回すのはあまりオススメしません(広告SDKが計測しているimpの定義ともずれてしまいがち)。なのでViewModelを使うメリットがあまり活かせないのかなと思っています。

広告のライフサイクルを管理する

AdViewクラスが継承しているBaseAdViewクラスにはresume() pause() destroy()が用意されていて、広告を表示しているUIのライフサイクルに応じてそれぞれのメソッドを叩く必要があります。
この3つのメソッドはAdMobに関わらず他社の大半の広告SDKでも同じように設計されている印象です。
各メソッドの役割は例えば以下のようなことがあると言えます(これらは各社SDKの仕様に左右されます。ただ、叩いておいた方がいいことには変わりないという認識は持っておいた方が良いでしょう)。

  • resume()
    • 広告のオートリフレッシュ間隔の計測を再開する
    • Impression(収益に関わる)の計測を再開する
    • メディエーション実装クラスにライフサイクルの変更イベントを通知する
  • pause()
    • 広告のオートリフレッシュ間隔の計測を中断する
    • Impression(収益に関わる)の計測を中断する
    • メディエーション実装クラスにライフサイクルの変更イベントを通知する
  • destroy()
    • 広告のインスタンスを破棄する
    • メディエーション実装クラスにライフサイクルの変更イベントを通知する

これらは記述漏れが発生しやすいので注意が必要です。
Androidでは広告の管理クラスにLifecycleObserverをセットしてライフサイクルの処理も管理クラス内部で処理しておくとイマドキな気がします。

実装メモ
  • 広告で収益を発生させるにはざっくりクリックされる多く見られるの2種類がある
    • クリックされたこと画面に見えているというのは広告のView内で計測しているので外からはいじれない
    • AdMobでは広告が見えていない時にオートリフレッシュのタイミングを迎えるとリフレッシュはスキップされるような動きをする
    • destroy()を呼んだ広告インスタンスはリロードできなくなるので作り直すしかありません

メディエーションはどう動くか

メディエーションというのは、AdMob以外の広告SDKから表示される広告コンテンツをAdMobのAdViewの中で表示できる機能です。例えば、TwitterFacebookAmazonなど広告SDKを提供している企業が抱える広告コンテンツもAdMobの中で表示できるようになります。
もちろんAdMob自身が持っている広告コンテンツも様々な広告配信ネットワークから価値があるものが優先して表示されるのですが、ただこれだとAdMobの配信システムに大きく依存しているので、アドネットワーク内に競合を増やして収益獲得の効率化を目指そうよというものです(ヘッダービディングも思想は同じようなものだと認識してますが違うのかな?)。

具体的なやり方を説明する前に、なぜこういうことができるのかというと、簡単に言ってしまえば、

バナー広告はそれぞれViewGroupを継承しているのでaddView(view: View)みたいなことをすれば中身を上書きできるよ

というだけです。
(ちなみにAdMobのネイティブ広告はViewGroupではなくて、UnifiedNativeAdというデータクラスを使っていて、パーツごとに情報をセットするコンバート処理を挟むことでメディエーションを実現していると言えます)

それでは実装を解説していきましょう。

1. メディエーション実装クラスを作る

CustomEventBannerを継承していることで他社の広告SDKのメディエーションをしたいんだなとAdMob側に認識してもらえる準備になります。

package sample.android.ad

class SampleBannerAdCustomEvent : CustomEventBanner {

    /**
     * @param customEventBannerListener 各社SDKのAdViewを読み込んで必要なタイミングで対象のイベントをコールする
     * @param serverParameter AdMob管理画面で設定できる文字列が返る(枠IDを入れておくことが多いです)
     * @param adSize AdMob側で設定した広告のサイズを受け取り、メディエーション時の読み込む広告のサイズと合わせるために使う
     * @param mediationAdRequest AdRequest.Builder()内で設定したKeywordやBirthday情報を取れる(使ったことないです)
     * @param customEventExtras AdRequest.Builder()内で設定したBundleの値を渡せる(枠IDを入れておくことが多いです)
     */
    override fun requestBannerAd(context: Context, customEventBannerListener: CustomEventBannerListener,
                                 serverParameter: String, adSize: AdSize, mediationAdRequest: MediationAdRequest,
                                 customEventExtras: Bundle?) {
        // 取り扱いたい広告SDKのAdViewを読み込む
    }

    override fun onResume() {
        // 各社SDKの`Resume`時の処理を書く
    }

    override fun onPause() {
        // 各社SDKの`Pause`時の処理を書く
    }

    override fun onDestroy() {
        // 各社SDKの`Destroy`時の処理を書く
    }
}

1つ注意なのですが、Kotlinで実装する場合、customEventExtrasはリクエスト時に設定されていない場合はnullで返ってくるのでNullableで定義しておきましょう:raising_hand_tone1:

ちなみにCustomEventBannerListener は以下のようなメソッドを持っています。

public interface CustomEventBannerListener extends CustomEventListener {
    void onAdLoaded(View adView);
}

public interface CustomEventListener {
    void onAdFailedToLoad(int errorCode);

    void onAdOpened();

    void onAdClicked();

    void onAdClosed();

    void onAdLeftApplication();
}

requestBannerAd内で行なった他社SDKの読み込み処理やイベントに応じていずれかのメソッドを叩く必要があります。
例えば、広告を読み込んで成功した時に広告のインスタンスをonAdLoaded(View adView)に突っ込んであげるとAdMobのAdViewaddView(view: View)されるようなイメージです。
逆に読み込み失敗時にonAdFailedToLoad(int errorCode)が呼ばれるとAdMobが違うメディエーション先へ広告を取りに行くというわけです。

実装メモ
  • Logcatを見る感じ、広告の読み込み時に枠IDごとに設定されている管理画面上のClassNameの文字列とメディエーション実装クラスの絶対パスが一致しているものがあるかをチェックしている挙動も見て取れました。
    • Adsでフィルターすると良い感じです

2. AdMob管理画面でClassNameとParameterを設定する

1で作ったメディエーション実装クラスのrequestBannerAdが呼ばれるためには、枠IDごとにどんなメディエーションが設定されているかを管理画面上で定義しておく必要があります。それを紐付けるのがClassNameParameterという概念です。例えば1のようなクラスを作った場合は対象の枠IDに対して以下のように設定することになります。

設定項目 ClassName Parameter(オプショナル)
Android sample.android.ad.SampleBannerAdCustomEvent 文字列を1つだけ設定可能
iOS SampleBannerAdCustomEvent 文字列を1つだけ設定可能

iOSは実装クラスのクラス名だけでいけるみたいですね:ok_woman:

AdMobの管理画面で設定してみてください!
スクリーンショット 2019-12-02 14.44.09.png

上記2点で最低限のメディエーションの実装は完了です!
あとは管理画面で複数のメディエーションに対してどういう順序でメディエーションするとより収益化が見込めるかという視点で運用しながらチューニングをしていくと良いでしょう。

ただ、最初の頃はこの実装だけでなぜ動くのか不思議なはずです。先ほど作ったSampleBannerAdCustomEventFind Usageしてもどこからも使われていません。
なんで参照されていないクラスのrequestBannerAdが呼ばれるねん!!!
と突っ込みたくなりますが、実装自体は合っています。
内部実装が読み取れないので具体的な解説は難しいのですが、リフレクションとかでうまいことやったりしてるんでしょう(適当)

補足ですが、requestBannerAdのそれぞれの引数と実装者が外から設定した場合の値の紐付き方を考察混じりになりますが書いてみます。

  • context : AdViewをイニシャライズした時に入れたContext
  • customEventBannerListener : SDKの内部実装で勝手に紐付く(これだけが謎紐付き)
  • serverParameter : AdMob管理画面で設定したParameter
  • adSize : AdViewをイニシャライズした時にセットしたAdSize
  • mediationAdRequest : AdViewのload時にセットしたAdRequestの一部のパラメーター(LocationやKeywords)
  • customEventExtras : AdViewのload時にセットしたAdRequestの中のaddCustomEventExtrasBundleで対象のクラスにセットしたBundle

実装メモ
  • カスタムイベントとメディエーションはだいたい同じ意味です
  • Parameterにはメディエーション先で使いたい何かしらの文字列を管理画面から送りたい要件があるときに使うと良いです
    • 画面ごとにAdMobの枠IDを発行して収益性を比較したいのでメディエーション先の枠IDも同様に発行して柔軟に変更させたい。その場合はParameterにそれぞれの枠IDを設定して管理画面側で動的に読み込む枠IDを変更できる
    • バナー広告とレクタングル広告で同じ広告SDKに対してメディエーションしたい。その場合は同じ実装クラスを参照しつつ、指定フォーマットは引数のadSizeから取り、Parameterに対象の枠IDをセットすることでメディエーション実装を使い回しできる
  • メディエーション実装クラスをリリース後にリファクタリングの途中でパッケージやクラス名を変えてしまうと動かなくなる可能性があるので注意
    • JavaからKotlinへのコンバートは問題ないです

3. メディエーション実装クラスに外から値を受け渡ししたい

実際に運用していて困るのが、リリース後に管理画面の設定やメディエーション内の挙動を変えたい場合です。
枠IDやメディエーションは一度リリースしてしまうと古いバージョンのアプリの動きを担保したまま運用していくのが難しくなります。これはアプリのソースコード上でも管理画面上でも同様です。
どれだけ意識して開発していても広告SDK側の破壊的なアップデートや先方との契約の都合など変更せざるを得ないケースはいくつか挙げられるでしょう。
ではメディエーションの実装に関してはどのような柔軟性を持たせておけばいいのか。方法が2つあります。

  • 管理画面で設定したParameterを受け取れるserverParameterを使うようにする
  • RemoteConfigを使って必要なパラメーターをアプリ側でcustomEventExtrasに詰めて渡す

customEventExtrasを使った値の受け渡しは以下のように実装します。

val adRequest = AdRequest.Builder()
            .addCustomEventExtrasBundle(SampleBannerAdCustomEvent::class.java, Bundle().apply {
                // Bundle型にできればなんでも可
                putString("key", value)
            }).build()

adView.loadAd(adRequest)

絶対にやらないほうがいいのは決め打ちで固定の枠IDや特定の文字列をメディエーション実装クラス内で使ってしまうということで、上の2つのやり方に関しては実装しやすい方を選択すると良いでしょう。

ただ、個人的には、serverParameterを使う方法は初めて実装を見た人がどうやって動いているのか理解しづらいと思うので、RemoteConfigを使った処理に寄せてしまった方が良いと考えています。
customEventExtrasに詰める箇所はJavaもしくはKotlinで書くことになるので実装が追いやすいし、SampleBannerAdCustomEventのような実行クラスへの参照を作ることができます。

実装メモ
  • 管理画面もしくはRemoteConfigから渡す文字列はJson形式にしておくと何かと安心です
    • 例えば、古いバージョンに影響を与えずに、新しいバージョンで管理画面もしくはRemoteConfigの値を参照したいような要件に対応できます(特に管理画面では1つしか文字列を設定できないので)
  • 一部のメディエーション設定を消したい場合、古いアプリにメディエーション実装クラスが残っていても、ためらわず消してもらっても大丈夫です
    • メディエーション時に内部でエラーが起きた時の動きと同様に他のメディエーション先にリクエストが優先されます
    • 実装クラスのパスと管理画面のクラスネームが間違っていても同じ動きになります
  • 逆にメディエーション設定を増やしたい場合も新しいバージョンでしか認識されないので古いバージョンを使っているユーザーでクラッシュや意図しない挙動が起きるケースは少ないはずです
  • メディエーションしようとしたのにメディエーション先でエラーとなり、他に表示できる広告がない場合は白板になるのを防ぐためデフォルト設定ではAdMobの広告が表示されるようになります(フィラーの設定で変更可能)
  • メディエーション先の広告SDKのオートリフレッシュ機能はOFFにしておいてAdMobのオートリフレッシュに委ねておくのがいいです
    • ちらつきの原因になり得るのと、無駄なimpが意図せず増えてしまう要因になり得ます

応用編

実務でつまづいたこと、運用してみて良かったこと、分かったことなど

メディエーション実装には大きく2種類あった

どういうことかというと、メディエーションするにあたり必要な実装は1つしかないのですが、自前で実装する場合すでに実装されたjar/aarファイルやGradleで取り込めるライブラリが提供されている場合があります。比較してみると以下のような違いがあります。

実装方式 自前実装 提供品利用
使い方 CustomEventBannerを継承したクラスを作る Gradleで参照する
クラスネーム設定 変動(作成クラスの絶対パス) 常に固定
手軽さ 🙅‍♂️ 🙆‍♂️
拡張性 🙆‍♂️ 🙅‍♂️
バグの原因の発見容易性 🙆‍♂️ 🙅‍♂️

提供品は圧倒的に手軽で、ライブラリを入れるだけでアプリ側の作業が完了したりするのですが、内部実装をいじれないので要件によってはうまく適合しないケースもあります。
ですので、すでに提供品がある広告SDKであれば一旦そちらを使ってみて、うまくいかなかった時に自前で実装するというアプローチが良いように思います(最近はもうメディエーションを自前実装する機会は多くないかなと思います)。
最近はGoogleがAdMobに組み込むための各社SDK用のアダプターを公開してくれているので実装がとても楽になってきていると言えます。

AdMobのメディエーションアダプター一覧
https://developers.google.com/admob/android/mediate?hl=ja

これまでに自前実装をしたケース
  • メディエーションしたい広告SDKがアダプターを提供していなかった
  • メディエーション先で特定のキーワードを設定して広告をロードしたかったが、提供されているアダプターが対応していなかった
  • ファーストリクエストの広告がNO_AD(在庫がない)を返してきたらフォーマットの異なる広告を読み込みたい要件があった
    • 広告フォーマットが違うのでメディエーションで対応できないケース

実装メモ
  • アダプターを利用する場合は管理画面に設定するClassNameが他社のパッケージのパスになり違和感がありますが間違ってはないのでご安心ください

広告テストがしづらいので枠IDの管理を考える

これは運用してみると出てくる問題の一部で、例えば以下のような問題がありました。対策と合わせて書いてみます。

1. TestModeを使用せずにDev環境やQAで広告をテストしすぎると垢BAN食らって2週間ほどアカウント停止されがち問題

AdMobでTestModeを使うには、一度ビルドしてログから一意なKeyを取得してきて都度設定する必要があります。
QAのために社内全ての端末のIDを管理するのは大規模なプロジェクトほど骨が折れます。
また、判定の仕組みは全く不明ですが、StoreにリリースされているApplicationIdと異なるビルドやDebugビルドからTestModeを設定せずに広告を読み込みすぎる(おそらくクリックすると更に良くないです)とAdMobのアカウントが停止されて問答無用で広告も読み込めなくなるし管理画面もいじれなくなります。
本番にリリースされている状態で垢BANされると相当クリティカルな問題ですので、広告の挙動テストではせめてReleaseビルドを使う不正に収益を得ることがまずいのでクリックするテストは避けるは心がけておいたほうが良いでしょう。Debugビルドでは広告を表示しない仕組みがあると安心ですね。

実装メモ
  • 他社のSDKではTestModeはTrue/Falseだけで切り替えられるSDKも多い
  • AdMobに理由を添えて問い合わせしても例外なく2週間程度停止されるようです
  • 今はもう起きてないかもですが、TestModeをONにしているとメディエーションのテストはできなかったです

2. 広告機能のリリース後に広告に関する機能追加をしようとした時、本番の枠IDの設定をテストのためにいじりづらい問題

これはもう解決策は明確で、枠IDにもProduction、Staging、Developの概念を取り入れると良いです。
例えば自社では下記のような思想で運用していました。

  • Production枠ID : 基本ProductionReleaseのようなVariantでしか参照しないようにする
  • Staging枠ID : ここで機能開発を行う。eCPMも自由に調整してメディエーション配信の優先順位も操作できるようにする
  • Development枠ID : 公式のテスト用IDを使うか、抽象化した広告インターフェースにテスト用のダミーViewを実装したようなクラスを差し替えれるようにしてAdMobを実行クラスで使わないようにする(AdMobのAdViewの代わりにImageViewを表示しておく的な)

3. メディエーション先の広告が表示されているのかわかりづらい問題

2の案でStaging枠でメディエーション設定は自由に弄れるようになるのですが、実際にメディエーションが成功しているかを目視で判断するのは難しいケースもあります。広告出稿側がAdMob以外にも同じ広告を提供しているケースがあるためです。そんな時は以下のような判断方法を検討してみてください。

  • AdView.getMediationAdapterClassName()を使う
    • メディエーション先にリクエストが渡る時はLogcatでも確認はできます
  • クリックした時にブラウザが開く時のURLをチェックする
    • 広告のスクショと一緒に広告担当者に確認すれば出稿している広告かどうか判断してもらえるケースが多いです
    • Production以外は垢BANにドキドキしながらクリックすることになりますが、、、

実装メモ
  • TwitterやAmazonは見た目に特徴があるので判断しやすい
  • TwitterやAmazonなどは公式アプリが入っていないとメディエーションが反応しないケースもあります

4. 意図したメディエーションを試すのに手間がかかる問題

2で解説したStaging枠を更に拡張させるやり方の提案です。
管理画面で設定を都度変更するのは反映ラグ(1~2時間程度)もあり、複数人からの一斉の要望に答えづらい問題があります。
QAを置いている企業だと連携コストがかかってしまうので、弊社ではStaging枠IDをメディエーション分だけ複数発行するというやり方を取っています。
1つの枠IDに対して例えば3つのメディエーションが紐づいていると目視での確認がしんどくなってくるので、
1つの枠IDに対して1つのメディエーションというセットを複数作って参照する枠IDに応じて表示される広告が切り替わるような仕組みにしています。
アプリのデバッグ設定などで動的に枠IDを切り替えられるようにしておくと即反映できるし便利ですね!

実装メモ
  • 設定してすぐの時はある程度広告リクエストを送らないとメディエーションが反応しないので辛抱強くリクエスト送ってみましょう

白板を防ぐ

広告コンテンツが読み込めない、在庫がない、広告の読み込み中などの状態でユーザーに広告のViewが見えてしまうと真っ白に見えるようになっています。これは白板(しろいた/はくばん)と呼ばれていて、これが見えている状態というのは、ユーザー側にも収益的に見てもベネフィットが限りなく小さいです。
SDK側の仕様に大きく左右されるということもあり、この白板をゼロにすることは難しいので、なるだけ起きない方法を考えていきたいと思います。

1. 広告の読み込み中もしくはエラーになった場合は広告表示領域を隠す

読み込み中かどうかはAdMobでは、AdView.isLoadingというメソッドがあり、状態を取得することが可能です。またエラー状態もAdListenerのonAdFailedToLoadで受け取れるので状態管理の仕組みを作れば簡単に判定できます。広告表示領域はデフォルトGoneにしておいて、読み込みに成功したらVisibleに切り替えるのが良さそうです。

private fun loadAd() {
    binding.adContainer.isGone = true
    ad?.also {
        it.setCallback(object : Ad.Callback {
            override fun onSuccess(view: View) {
                bindAdView(binding.adContainer, view)
                binding.adContainer.isVisible = true
            }

            override fun onFailure(errorCode: Int) {
                binding.adContainer.isGone = true
            }
        })
        if (it.isLoading()) {
            return
        }
        it.load()
    }
}

ちなみにこの辺の実装は各広告SDKの仕様によって書き方が異なるので、状態管理を内部で行うことも含めて下記のようなInterfaceを切っておくと便利です。

interface Ad {

    fun isLoading(): Boolean

    fun isOnError(): Boolean

    fun getView(): LiveData<View>?

    // 必要に応じて
    fun getNativeAd(): LiveData<UnifiedNativeAd>?

    fun resume()

    fun pause()

    fun destroy()

    fun setCallback(callback: Callback?)

    fun load()

    interface Callback {

        fun onSuccess(view: View?)

        fun onFailure(errorCode: Int)
    }
}

実装メモ
  • オートリフレッシュが有効な場合、AdListenerをセットしていた場合はオートリフレッシュ間隔でコールバックが返ってきます。成功->失敗のようなケースに広告が白板になることもあるので考慮できると良いです。
  • Adの実行クラスはDaggerなどでManagerクラスに注入できるようにすればテストが簡単にできそうです。

2. プリフェッチしておく

バナー広告でキャッシュがNGと書いてあるものの、見えることがほぼ確実な広告を先に読み込んでおいてもバチは当たらないと思います。プリフェッチを実装した公式サンプルもあります。
ただこのサンプルおそらく問題があるケースがあって、オートリフレッシュが有効な広告の場合だとプリフェッチがループしてしまいます。

private fun loadAdForInitialize(adLoadCount: Int) {
    if (adLoadCount >= adList.size) {
        return
    }
    adList?.also {
        if (it.isEmpty() || !isInitialized) {
            // 読み込み中に画面が破棄されていたら止めたい
            return
        }
        val ad = it[adLoadCount]
        val callback = object : Ad.Callback {
            override fun onSuccess(view: View) {
                ad.setCallback(null)
                loadAdForInitialize(adLoadCount + 1)
            }

            override fun onFailure(errorCode: Int) {
                ad.setCallback(null)
                loadAdForInitialize(adLoadCount + 1)
            }
        }
        ad.setCallback(callback)
        ad.load()
    }
}

サンプルコードのように広告実装を抽象化している場合はCallbackを、AdMobを使っている場合はAdListenerをnullにしてあげましょう。また、非同期処理がループしているので画面のライフサイクルに応じて処理を止めてあげる仕組みがあるとさらに良いかなと思います。

3. (任意)リスト内で広告を取り扱う場合の白板防止策

リスト内で広告を扱う実装方法はまだ確立したベストプラクティスがないので実装方法によりますが、プリフェッチを採用していて、かつList型で広告を管理している場合、以下のようなUtilメソッドを作っておくと便利です。

fun getIncrementedIndex(targetIndex: Int, adListSize: Int): Int {
    var nextIndex = targetIndex + 1
    if (nextIndex >= adListSize) {
        // 配列のサイズを超えたら0に戻す
        nextIndex = 0
    }
    return nextIndex
}

fun getValidAd(adList: List<Ad>, targetIndex: Int): Ad? {
    val targetAd = adList[targetIndex]
    if (!targetAd.isOnError()) {
        // 白板でなければそのまま利用する
        return targetAd
    }
    var nextAd: Ad? = null
    var incrementedIndex = getIncrementedIndex(targetIndex, adList.size)
    // adListを一周して有効な広告がないか探してくる
    while (incrementedIndex != targetIndex) {
        val incrementedAd = adList[incrementedIndex]
        val isLoadedAd = !incrementedAd.isLoading() && !incrementedAd.isOnError()
        if (isLoadedAd) {
            nextAd = incrementedAd
            break
        }
        incrementedIndex = getIncrementedIndex(incrementedIndex, adList.size)
    }
    return nextAd
}

これで白板が見えてしまう可能性は大きく下がるはずです!
これでもAdnullで返ってきてしまう場合は広告表示領域をGONEにすると良いでしょう。

開発Tips/小ネタなど

  • Firebase A/B Testingを使えば広告のパフォーマンス比較ができる
    • 例えばバナー広告とネイティブ広告
    • AdMobを使っていると収益性やインプレッション数もFirebase上でわかるので比較しやすい
    • TargetingにPredictionsを活用できて非常にイケている
  • 広告の詳細な実装はInterfaceで隠してテスタブル設計にしておく
    • 最近のdex.fmでも話題に上がってたので少しテンション上がりました:relaxed:
  • AdViewはリロード時に再利用できるか、作り直すべきか
    • AdViewはdestroy()を呼ばない限りはリロードが可能みたいです
    • リングバッファ管理できますね!
    • 広告SDKによっては作り直さないとダメなやつもあるっぽいです
  • 広告は比較的プレミアム機能と絡むことが多いので、destroy時とcreate時の処理を外から叩けるようにしておくと良いと思います
    • たまーーーーーに行儀の悪い広告コンテンツだと、画面に見えていなくても、広告の読み込みが成功していると画面を奪ったり、動画広告であれば裏で音声を流し始めたりするものもあるので、プリフェッチの取り扱いには気をつけるのと、例えばプレミアム機能が有効なユーザーはそもそも広告を読み込まない、という設計にするべきです。

まとめ

  • 実装設計するにあたり、LifecyclesとLiveDataはAdMobと相性がいい気がした
  • ViewModelで広告インスタンスを管理するのは玄人でなければやめておく
  • RemoteConfigどんどん使っていきましょう
  • A/B TestingとPredictionsの連携がアツい
  • できるだけ公式の実装から外れないようにする
  • メディエーションは各社SDKがアダプターを提供してくれているかをまず探す
  • バナー広告はキャッシュしすぎないようにする
  • 自前で実装するメディエーション用のクラスはパッケージを安易に移動させないように気をつける
  • メディエーションをたくさん付けるよりもパフォーマンスチューニングをうまくやるほうが有益
  • RequestクラスとViewクラスをそれぞれ使いまわしてもいいかはSDKごとに確認する
  • ユーザーに悪影響がある分、良い実装をして出来る限りストレスは軽減してあげる努力をしていきましょう

最後に

長らく読んでいただきありがとうございました!
最初は取っつきづらい領域なんですが、実装方法を理解してしまえば拡張もしやすく、色々な要件にも柔軟に対応できて収益の最大化に貢献できるはずなのでネット上にもっと情報が増えてくれると良いなぁと思っています。

広告のSDKはAndroidXに対応されてなかったりJavaのままだったりSDKによって挙動が大きく違ったりと最新のトレンドと比べると遅れているなぁと感じますが、技術としてはとても面白いと思うのでどんどん盛り上がってくれるといいなぁと切に願っています!

MoPubはAndroidX対応をすでに諦めているっぽい:ghost:
https://github.com/mopub/mopub-android-sdk/issues/311

そして、リストの中で広告を表示させたい要件ってあると思うんですが、公式のBannerRecyclerViewSampleはリストのデータをObject型で管理してたりと苦しい感じだし、
ViewHolderにAdViewを設定してonBindのタイミングで読み込みを開始するだけだと読み込みのレイテンシで白板の状態が見えてしまって見栄えが良くないです。

ということで良い感じにリスト内でも動くサンプルを作ってみました。

https://github.com/FujiKinaga/AdMobSample

まだこれがベストプラクティスな実装ではないと思っているのでアドバイスなども頂けると大変嬉しく思います!
個人的にはRecyclerViewPoolをうまく扱えるともっと良い感じになるのかなと思ったりします。

Qiita楽しい:santa:

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした