🐈

Flutterで汎用性の高い、Admobのアダプティブバナー広告表示ウィジェットを作る

2022/01/19に公開

FlutterでAdMobのアダプティブバナー広告を実装するとき、

  • 横幅に対するバナーのサイズを取得するメソッドを非同期で実行しないといけない
  • 広告のロードの完了を待ってからAdBannerをWidget Treeに登録しないといけない

といった条件が出てくるため、実装コードがやや複雑になります。
また、公式のサンプルコードや、他のウェブサイトなどで紹介されている実装だと、MediaQuery.of(context).size.widthを用いた横画面いっぱいに表示するバナーの例が多いですが、

  • Floating Action Buttonを避けて配置したい
  • 特定のWidgetの内部で表示したい(Columnの中の要素の間に挿入やListViewやなど)
  • Dialogの中に表示したい

といったケースのときに、余白や表示する場所のサイズを考慮し横幅を個別に計算したりする必要がでてきて、コードがより複雑化します。できればそれぞれの表示場所毎にサイズの計算は避けたいです。

// こういうのを避けたい
final adSize = await AdSize.getAnchoredAdaptiveBannerAdSize(
  MediaQuery.of(context).orientation,
  MediaQuery.of(context).size.width,
) as AdSize;

// or

final padding = EdgeInsets.symmetric(horizontal: 16.0);
final screenWidth = MediaQuery.of(context).size.width; 
final adWidth = screenWidth - padding.horizontal;
final adSize = await AdSize.getAnchoredAdaptiveBannerAdSize(
  MediaQuery.of(context).orientation,
  adWidth,
) as AdSize;

そして、できればバナー広告の表示ロジックを集約したWidgetクラスを作成して、様々な場所でストレスなく表示させたいですよね。

ということで、今回は汎用性の高い、アダプティブバナーを表示するWidgetを作成してみます。

ちなみにアダプティブバナーに拘らなくても320x50のような固定サイズで表示することも可能ですが、より見栄えを良くするためにもアダプティブバナーを使うことを検討して良いと思います。

必要なもの

  • Admobのアカウント及びバナー広告ユニットID
  • google_mobile_ads
    • pubspec.yamlに google_mobile_ads: ^1.0.1 を追記してインストールできます。
  • flutter_hooks
    • pubspec.yamlに flutter_hooks: ^0.18.2 を追記してインストールできます。

バナーサイズを非同期で取得する部分や、バナーのロードが完了したかのステートの管理をする部分を簡潔にするために(+自分の学習のために)flutter_hooksを用いています。
用いない場合はStatefulWidget + FutureBuilderを使う必要がでてくるかと思います。

AdaptiveAdBannerクラスを作成

全体で70行未満のコードになります。
このままほぼコピペで使えるようにしていて、adUnitIdをご自身の広告ユニットIDに差し替えるだけです。(YOUR AD UNIT IDの部分)

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

class AdaptiveAdBanner extends StatelessWidget {
  const AdaptiveAdBanner({this.onLoaded});

  final VoidCallback? onLoaded;

  
  Widget build(BuildContext context) {
    final adUnitId = kReleaseMode ? 'YOUR AD UNIT ID' : BannerAd.testAdUnitId;
    return LayoutBuilder(builder: (context, constraint) {
      return HookBuilder(builder: (context) {
        final bannerLoaded = useState(false);
        final bannerAd = useFuture(
          useMemoized(
            () async {
              final adWidth = constraint.maxWidth.truncate();
              final adSize = await AdSize.getAnchoredAdaptiveBannerAdSize(
                MediaQuery.of(context).orientation,
                adWidth,
              ) as AdSize;

              return BannerAd(
                size: adSize,
                adUnitId: adUnitId,
                listener: BannerAdListener(
                  onAdFailedToLoad: (ad, error) {
                    ad.dispose();
                    bannerLoaded.value = false;
                  },
                  onAdLoaded: (ad) {
                    bannerLoaded.value = true;
                    onLoaded?.call();
                  },
                ),
                request: const AdRequest(),
              );
            },
          ),
        ).data;

        if (bannerAd == null) {
          return const SizedBox.shrink();
        }

        useEffect(() {
          bannerAd.load();
          return () async => await bannerAd.dispose();
        }, [bannerAd]);

        return bannerLoaded.value
            ? SizedBox(
                width: bannerAd.size.width.toDouble(),
                height: bannerAd.size.height.toDouble(),
                child: AdWidget(ad: bannerAd),
              )
            : const SizedBox.shrink();
      });
    });
  }
}

(実際のコードを少し改変して載せているのでおかしなところがあったら教えて下さい。)

詳解

基本的な方針として、このWidget内で横幅を計算したり、外から横幅の値をwidthパラメータなどで渡すことはしないようにしています。つまり、このクラス上では、MediaQuery.of(context).sizeで得られる画面の横幅には依存しない表示が可能になっています。

その代わり、LayoutBuilderを使って、このAdaptiveAdBannerが表示される親WidgetのConstraintに従って、その横幅にフィットする形でサイズを計算できるようにしています。
なので、親Widgetでwidth=400と指定されれば、バナー広告を読み込む時の横幅が400になります。

そしてflutter_hooksのuseFuture, useMemoizedを用いて、バナー広告のサイズの取得から、BannerAdインスタンスの生成までの一連の処理を行います。
useFutureによって、FutureBuilderと同様のことをネストを深くせずに扱うことができるようになり、useMemoizedでFutureの実行がwidgetのライフサイクル上1回に留まるように(パフォーマンスを考慮)しています。

バナー広告のサイズが決まり、BannerAdインスタンスが初期化されたら、useEffectを使って、バナーのロードを開始します。

バナーのロードが終わり、正常にロードが終わると、useStateを使って管理しているbannerLoadedの変数がtrueに変わり、再度リビルドが実行されます。
そのタイミングで、ロードされたバナー情報をAdWidgetに渡してWidget Treeに載せ、バナー広告が表示されます。

onLoaded プロパティに関しては、AdaptiveBanner Widgetの外側で、広告がロードされた場合に何かハンドリングしたい場合に使います。不要ならこのプロパティを消してしまっても問題ありません。 また、BannerAdListenerの中の各種処理の中で必要に応じてデバッグログを出力したり、イベントログを収集したりしても良いと思います。

以降は具体的な使い方を説明していきます。

Widgetの横幅いっぱいに表示する

横幅が明確に定まる場合

例えば、Scaffoldの中で、予めConstraintの定めのないWidgetが横幅いっぱいに広がることができ 、横幅がInfinityにならず定まる場合には、そのまま配置することで横幅一杯に表示することができます。

class TestPage extends StatelessWidget {
  const TestPage();

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Test'),
      ),
      body: Column(
        children: const [
          AdaptiveAdBanner(),
          SizedBox(height: 32),
          Placeholder(
            fallbackHeight: 200,
          ),
        ],
      ),
    );
  }
}

このコードの表示結果は次のとおりになります。

また、Dialog Widget上で表示する際も、そのウィジェットの横幅に応じて適切なバナー広告を表示できます。
MediaQuery.of(context).size.widthを使ったり、Dialogの外側の余白を引き算するといった処理も不要です。

showDialog(
  context: context,
  builder: (context) {
    return Dialog(
      child: Padding(
        padding: const EdgeInsets.all(8.0),
        child: Column(
          mainAxisSize: MainAxisSize.min,
          children: const [
            Text('Dialog'),
            SizedBox(height: 32),
            AdaptiveAdBanner(),
            SizedBox(height: 32),
            Placeholder(
              fallbackHeight: 200,
            ),
          ],
        ),
      ),
    );
  },
);

Stack Widget + Positioned等の場合

例えば画面の下部にStack Widget + Positioned Widgetを使って横幅いっぱいに表示場合、横幅が定まらないため、自分でSizedBox Widgetに横幅を指定して表示してあげます。

class TestPage extends StatelessWidget {
  const TestPage();

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Test'),
      ),
      body: SafeArea(
        child: Stack(
          children: [
            Column(
              children: const [
                Placeholder(
                  fallbackHeight: 200,
                ),
                SizedBox(height: 32),
                Placeholder(
                  fallbackHeight: 200,
                ),
              ],
            ),
            Positioned(
              bottom: 0,
              child: SizedBox(
                width: MediaQuery.of(context).size.width,
                child: const AdaptiveAdBanner(),
              ),
            ),
          ],
        ),
      ),
    );
  }
}

このコードの表示結果は次のとおりになります。

余白を指定して表示する場合

例えばFloating Action Buttonを避けて表示したい場合も、余白を加味して横幅を計算する必要はなく、Padding Widgetなどと組み合わせるだけで済みます。
例えば、次のようにコードを書くことで、Stack Widgetを使っていても、Floating Action Buttonと被らずに表示することができます。

余白を指定しない場合 余白を指定した場合

応用

ここまでの紹介の通り、親Widgetの表示領域に依存した作りにしてあるため、例えば Card Widgetに包んで、画面下にStack + Positioned Widgetを使って表示する、といったことも容易になります。
また、List Viewの要素の一つとしてランダムに表示する際も、ListViewのpaddingなどを気にせず、横幅の計算をせずに表示することが可能になります。
以下は自分がリリースしているアプリの例ですが、いずれも横幅の計算処理などはせずシンプルにAdaptiveBanner WidgetをCard Widgetで包んだり、ListViewの要素として挿入しているだけです。

さいごに

この実装で、バナー広告を表示したい場所毎に横幅の計算をしたりする必要がなくなり、表示基となるWidgetを整えてあげるだけで適切なサイズでバナー広告を読み込んで表示することができるようになりました。
実装の参考になりましたら幸いです。

また、よければ私が作った、日々の生活の固定費やサブスクリプションサービスの月額料金を管理できるKotekanをインストールして使ってみてください。応援して頂けると嬉しいです 🙏

https://apps.apple.com/jp/app/kotekan-コテカン/id1598027056

Discussion