くらげになりたい。

くらげのようにふわふわ生きたい日曜プログラマなブログ。趣味の備忘録です。

FlutterでAdMobを導入する

FlutterにAdMob広告をいれたいなと思い、
いろいろ調べたときの備忘録(*´ω`*)

google_mobile_ads | Flutter packageを使えばいいっぽい

ドキュメントやチュートリアルも用意されている

広告ユニットは作成済みの想定

前提条件はこんな感じ

インストール

とりあえずインストール

$ fvm flutter pub add google_mobile_ads

プラットフォーム固有の設定

Andorid

AndroidManifest.xmlmeta-detaを追加
テスト用のサンプルIDはca-app-pub-3940256099942544~3347511713

<manifest>
  <application>
    <meta-data
      android:name="com.google.android.gms.ads.APPLICATION_ID"
      android:value="ca-app-pub-3940256099942544~3347511713"/>
  <application>
<manifest>

iOS

Info.plistGADApplicationIdentifierを追加
テスト用のサンプルIDはca-app-pub-3940256099942544~1458002511

<key>GADApplicationIdentifier</key>
<string>ca-app-pub-3940256099942544~1458002511</string>

AdMobの初期化

初期化はこんな感じ

import 'package:google_mobile_ads/google_mobile_ads.dart';
import 'package:flutter/material.dart';

void main() {
  WidgetsFlutterBinding.ensureInitialized();
  MobileAds.instance.initialize();

  runApp(MyApp());
}

バナー広告の実装

テスト用の広告ユニットIDはこれ

Android: ca-app-pub-3940256099942544/6300978111
iOS:     ca-app-pub-3940256099942544/2934735716

登場人物としては、こんな感じ

  • BannerAd ... バナー広告の情報
  • BannerAdListener ... BannerAdのイベントリスナー
  • AdWidget ... BannerAdを表示するWidget

広告の読み込み

広告を表示する前に読み込み(load)が必要。こんな感じ。

  • BannerAdを作成
  • BannerAd().load()で読み込み開始
  • 読み込みが完了したかは、BannerAdListeneronAdLoadedで扱う
class BannerExampleState extends State<BannerExample> {
  BannerAd? _bannerAd;
  bool _isLoaded = false;

  // TODO: replace this test ad unit with your own ad unit.
  final adUnitId = Platform.isAndroid
    ? 'ca-app-pub-3940256099942544/6300978111'
    : 'ca-app-pub-3940256099942544/2934735716';

  /// Loads a banner ad.
  void loadAd() {
    _bannerAd = BannerAd(
      adUnitId: adUnitId,
      request: const AdRequest(),
      size: AdSize.banner,
      listener: BannerAdListener(
        // Called when an ad is successfully received.
        onAdLoaded: (ad) {
          debugPrint('$ad loaded.');
          setState(() {
            _isLoaded = true;
          });
        },
        // Called when an ad request failed.
        onAdFailedToLoad: (ad, err) {
          debugPrint('BannerAd failed to load: $error');
          // Dispose the ad here to free resources.
          ad.dispose();
        },
      ),
    )..load();
  }
}

正常に読み込まれると、AdMobで設定したタイミングで自動更新されるが、
onAdFailedToLoadなどでエラーになると、再読み込みなどはされないので、
再度、BannerAdを作り直して、load()しないといけない。

常に表示する広告だと、ネットワークが一時中断されると表示されなくなるけど、
ネットワークが復活しても広告が表示されない。。

また、多くの場合は、Wigetのmount時に読み込みを開始すると、
表示までに時間がかかるので、ChangeNotifierやriverpodを使って、
Widget外で事前読み込みをしておくとよいっぽい。

広告の表示

広告の表示はWidgetが用意されているので、
読み込みが完了したBannerAdAdWidgetに渡せばOK

...
if (_bannerAd != null) {
  Align(
    alignment: Alignment.bottomCenter,
    child: SafeArea(
      child: SizedBox(
        width: _bannerAd!.size.width.toDouble(),
        height: _bannerAd!.size.height.toDouble(),
        child: AdWidget(ad: _bannerAd!),
      ),
    ),
  )
}
...

読み込んだ広告に対して表示できる幅/高さが足りないと表示されないので、
SizedBoxなどで表示領域を確保しておくとよいっぽい。

広告の破棄

表示の必要がなくなったら、dispose()を呼び出して破棄する。
Widgetdispose時や、BannerAdListenerのonAdFailedToLoadなど。

@override
void dispose() {
  // TODO: Dispose a BannerAd object
  _bannerAd?.dispose();
  ...
  super.dispose();
}
BannerAdListener(
  // Called when an ad request failed.
  onAdFailedToLoad: (ad, err) {
    debugPrint('BannerAd failed to load: $error');
    // Dispose the ad here to free resources.
    ad.dispose();
  },
)

広告のサイズ

サイズはあらかじめいくつか用意されているので、
標準的なバナーサイズであれば、これらを選択すればOK

AdSize.banner          // 320x 50 標準バナー
AdSize.largeBanner     // 320x100 バナー(大)
AdSize.mediumRectangle // 320x250 レクタングル(中)
AdSize.fullBanner      // 468x 60 フルサイズ バナー
AdSize.leaderboard     // 728x 90 リーダーボード

ほかにもいろいろ用語がある

アダプティブバナーは、サイズが動的なので、
MediaQuery.of(context)で画面サイズを取得したり、
広告のサイズを取得し直したりとちょっと手間がかかる

アンカーアダプティブバナーの実装

画面の上か下に固定するアンカータイプのアダプティブバナー

注意点はこんな感じで書かれている

  • 広告を配置するView幅の把握が必要
  • バイスの幅とセーフエリアやカットアウト考慮が必要
  • アダプティブバナーは横幅いっぱいにしておくと最大限の成果が得られるように設計
  • 特定のデバイスで特定の幅に対して返されるサイズは常に同じ
  • アンカー型バナーの高さは、デバイスの高さの15%以下、
    または密度非依存ピクセルが90ピクセル以上、密度非依存ピクセルが50以上

横幅をデバイス幅にする場合はこんな感じ。

final adUnitId = Platform.isAndroid
  ? 'ca-app-pub-3940256099942544/6300978111'
  : 'ca-app-pub-3940256099942544/2934735716';
Future<void> _loadAd() async {
  // デバイスのサイズの取得
  final deviseSize = MediaQuery.of(context).size;
  // 基準となる広告の横幅を取得
  final adWidth = deviseSize.width.truncate();
  // 広告サイズの計算
  final AnchoredAdaptiveBannerAdSize? size =
      await AdSize.getCurrentOrientationAnchoredAdaptiveBannerAdSize(adWidth);

  if (size == null) {
    print('Unable to get height of anchored banner.');
    return;
  }

  _anchoredAdaptiveAd = BannerAd(
    adUnitId: adUnitId,
    size: size,
    request: AdRequest(),
    listener: BannerAdListener(
      onAdLoaded: (Ad ad) {
        print('$ad loaded: ${ad.responseInfo}');
        setState(() {
          // 読み込みが完了すると、広告サイズが設定された
          // BannerAdを得られるので、保存しておく
          _anchoredAdaptiveAd = ad as BannerAd;
          _isLoaded = true;
        });
      },
      onAdFailedToLoad: (Ad ad, LoadAdError error) {
        print('Anchored adaptive banner failedToLoad: $error');
        ad.dispose();
      },
    ),
  );
  return _anchoredAdaptiveAd!.load();
}

AdSizeを継承したAnchoredAdaptiveBannerAdSizeを利用する感じ。

サイズ計算はいくつか用意されている感じ。
基本は横幅のみ指定して、事前読み込みするときに向きも指定するっぽい

/// 横幅のみ指定。向きは現在の向きを取得 
Future<AnchoredAdaptiveBannerAdSize?> getCurrentOrientationAnchoredAdaptiveBannerAdSize(
  int width,
)

/// 横幅と向きを指定
Future<AnchoredAdaptiveBannerAdSize?> getAnchoredAdaptiveBannerAdSize(
  Orientation orientation,
  int width,
)

インラインアダプティブバナー

スクロールコンテンツ内に配置するタイプのアダプティブバナー
アンカータイプよりも大きめで背が高い

注意点はこんな感じで書かれている

  • インラインアダプティブバナーは、利用可能な全幅を使用した場合に最も効果を発揮するように設計。
    ほとんどの場合、デバイスの画面の全幅になり、該当するセーフエリアを必ず考慮が必要
  • インラインアダプティブバナーは、スクロール可能なコンテンツに配置されるように設計。
    バナーの高さは、APIに応じて、デバイスの画面と同じか、最大の高さに制限
  • 広告の実際の高さは、読み込み完了時にわかる

実装はこんな感じ

final adUnitId = Platform.isAndroid
  ? 'ca-app-pub-3940256099942544/6300978111'
  : 'ca-app-pub-3940256099942544/2934735716';
Future<void> _loadAd() async {
  // デバイスのサイズの取得
  final deviseSize = MediaQuery.of(context).size;
  // 基準となる広告の横幅を取得
  final adWidth = deviseSize.width.truncate();
  // 広告サイズの計算
  final AdSize size =
      await AdSize.getCurrentOrientationInlineAdaptiveBannerAdSize(adWidth);

  _inlineAdaptiveAd = BannerAd(
    adUnitId: adUnitId,
    size: size,
    request: AdRequest(),
    listener: BannerAdListener(
      onAdLoaded: (Ad ad) {
        print('$ad loaded: ${ad.responseInfo}');
        setState(() {
          BannerAd bannerAd = (ad as BannerAd);
          // 読み込みが完了した広告を使って、
          // 現在のプラットフォームにあうサイズを取得
          final AdSize? size = await bannerAd.getPlatformAdSize();
          if (size == null) {
            print('Error: getPlatformAdSize() returned null for $bannerAd');
            return;
          }
          // 広告と合わせて、広告サイズも保持しておく
          _inlineAdaptiveAd = bannerAd;
          _adSize = size;
          _isLoaded = true;
        });
      },
      onAdFailedToLoad: (Ad ad, LoadAdError error) {
        print('Anchored adaptive banner failedToLoad: $error');
        ad.dispose();
      },
    ),
  );
  return _anchoredAdaptiveAd!.load();
}

サンプルではAdSizeが使われているが、実際には、
AdSizeを継承したInlineAdaptiveSizeを扱っている感じ。

こちらもサイズ計算の関数がいくつか用意されている。

/// 横幅のみ指定。向きは現在の向きを取得。高さは0で固定
InlineAdaptiveSize getCurrentOrientationInlineAdaptiveBannerAdSize(int width)

/// 横向きの広告サイズを取得。横幅のみ指定。高さは0で固定
InlineAdaptiveSize getLandscapeInlineAdaptiveBannerAdSize(int width)

/// 縦向きの広告サイズを取得。横幅のみ指定。高さは0で固定
InlineAdaptiveSize getPortraitInlineAdaptiveBannerAdSize(int width)

/// 横幅と最大サイズを指定
InlineAdaptiveSize getInlineAdaptiveBannerAdSize(int width, int maxHeight);

getPlatformAdSizeの定義はこんな感じ。

class BannerAd extends AdWithView {
  // ...略
  /// Returns the AdSize of the associated platform ad object.
  ///
  /// The dimensions of the [AdSize] returned here may differ from [size],
  /// depending on what type of [AdSize] was used.
  /// The future will resolve to null if [load] has not been called yet.
  Future<AdSize?> getPlatformAdSize() async {
    return await instanceManager.getAdSize(this);
  }
}

instanceManagerはなにかというと、MethodChannelを管理するクラスのよう。

/// Maintains access to loaded [Ad] instances and handles sending/receiving
/// messages to platform code.
class AdInstanceManager {
  AdInstanceManager(
      String channelName,
      {this.webViewControllerUtil = const WebViewControllerUtil()})
      : channel = MethodChannel(channelName, StandardMethodCodec(AdMessageCodec()),) {...});

バナー広告の選び方

ざっくりこんな感じがいい気がする

  • 決まったサイズでいい -> AdSize.bannerなど
  • 固定で配置する -> アンカーアダプティブ
  • コンテンツ内に配置 or 高さを制限したい -> インラインアダプティブ

エラーメッセージ

BannerAdListenerのonAdFailedToLoadで確認したメッセージ
とりあえず、確認済みのものを記載しておく

BannerAdListener(
  onAdFailedToLoad: (ad, error) {
    debugPrint(error.message);
  },
)

Ad size will not fit on screen

広告のサイズが画面サイズを超えている?

# width=500 / maxHeight=500でリクエスト
final adSize = AdSize.getInlineAdaptiveBannerAdSize(500, 500);

# => Error building request URL: Ad size will not fit on screen. a_w=500,  a_h=500,  s_w=412,  s_h=815
# 画面サイズが、width:height=412/815なのでNG

maxHeightを調整すると、画面サイズを超えていても行ける時がある。。

# width=500 / maxHeight=400だと何故かうまくいく
final adSize = AdSize.getInlineAdaptiveBannerAdSize(500, 400);
# 取得できた広告サイズは、width:height=500/345

Error while connecting to ad server

ネットにつながってないときのメッセージ

Error while connecting to ad server: Unable to resolve host "googleads.g.doubleclick.net": No address associated with hostname

ATT/GDPR/app-ads.txt対応

長くなったので、とりあえずの参考リンクのみ。
このあたりを見ながら対応していこう。。


以上!! 広告をつけるだけでも大変だ。。(*´ω`*)

参考にしたサイトさま