Flutter 2 で登場した Google Mobile Ads を使ってインライン広告を実装する。

#はじめに
日本時間、3月4日に[Flutter 2](https://developers.googleblog.com/2021/03/announcing-flutter-2.html) が発表されました。
それに伴い、AdMob 及び AdManager の新しい導入手法が公開されました。
この記事の内容は、その [google_mobile_ads](https://pub.dev/packages/google_mobile_ads) を使ったインライン広告の実装サンプルです。

ちなみに、利用には Flutter 1.22.0 以上が必要です。
Flutter 2 ではなくてもいいようです。

これまで[公式のプラグイン](https://pub.dev/packages/firebase_admob)ではバナー広告は画面の最前面に表示される形式で、Flutter の Widget Tree とは全く別のシステムで動いていました。それが、これからは Widget としてUIを組むことができるようになります。
これまでも非公式でそういうことができる[プラグイン](https://pub.dev/packages/admob_flutter)は提供されていましたが、満を持して公式でできるようになりました。
つまり、オーバーレイ広告ではなく、インライン広告が使えるようになりました！

**なお、以下の内容はベータテスト段階であることに注意してください。**
今後、破壊的な変更が行われたりする可能性もあります。


#実装してみる
今回の内容は一応 GitHub に上げてあります。

https://github.com/yukihiroK/flutter_ads

##準備
最初に、AdMob を使う場合は定番ですが、Android のマニフェストファイルと iOS の Info.plist を編集する必要があります。

```your_project/android/app/src/main/AndroidManifest.xml
<manifest>
    <application>
        <!-- Sample AdMob App ID: ca-app-pub-3940256099942544~3347511713 -->
        <meta-data
            android:name="com.google.android.gms.ads.APPLICATION_ID"
            android:value="ca-app-pub-xxxxxxxxxxxxxxxx~yyyyyyyyyy"/>
    </application>
</manifest>
```

Info.plist についても同様です。
ついでに SKAdNetwork の対応もしておきます。必要なければ無視してください。

```your_project/ios/Runner/Info.plist
<key>GADApplicationIdentifier</key>
<string>ca-app-pub-################~##########</string>

<key>SKAdNetworkItems</key>
      <array>
        <dict>
          <key>SKAdNetworkIdentifier</key>
          <string>cstr6suwn9.skadnetwork</string>
        </dict>
      </array>
```

##初期化
まず最初に MobileAds を初期化する必要があります。
今回は簡単のために初期化を待ってから runApp() をするようにしていますが、UX的には最初にUIが表示される時間を短くしたほうが良いです。[Example](https://pub.dev/packages/google_mobile_ads/example) では、初期化処理の結果を Future のまま受け取り、広告に関係ない部分はすぐに描画できるようにしています。

```dart
void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await MobileAds.instance.initialize();
  runApp(MyApp());
}
```

##バナー広告の実装
前提として、Google Mobile Ads で広告を実装する手順はざっくりいって、
① 対応する Ad インスタンスを生成する。
② Ad インスタンスを load() する。
③ ロードした後、AdWidget に Ad インスタンスを渡す。
④ 広告が描画される。
のような感じになっています。
この流れはリワード広告やインタースティシャル広告でも同様です。

そして、① の Ad インスタンスを生成する際には、AdRequest インスタンス、AdListener インスタンス、広告のユニットID 等が必要になります。

ここでは Ad は BannerAd で、ユニットIDにはテスト用のものを用いています。


```dart

class BannerAdWidget extends StatefulWidget {
  BannerAdWidget({@required this.size});

  final AdSize size;

  @override
  _BannerAdWidgetState createState() => _BannerAdWidgetState();
}

class _BannerAdWidgetState extends State<BannerAdWidget> {
  BannerAd _bannerAd;
  bool _isReady = false;

  @override
  void initState() {
    super.initState();
    Future.delayed(Duration(seconds: 1), createAd);
  }

  createAd() {
    _bannerAd = BannerAd(
      size: widget.size,
      adUnitId: BannerAd.testAdUnitId,
      request: AdRequest(),
      listener: AdListener(
        onAdLoaded: (ad) {
          print('${ad.runtimeType} loaded!');
          setState(() {
            _isReady = true;
          });
        },
        onAdFailedToLoad: (ad, error) {
          print('${ad.runtimeType} failed to load.\n$error');
          ad.dispose();
          _bannerAd = null;
        },
        onApplicationExit: (Ad ad) =>
            print('${ad.runtimeType} onApplicationExit.'),
      ),
    )..load();
  }

  @override
  void dispose() {
    _bannerAd?.dispose();
    _bannerAd = null;
    super.dispose();
  }

  @override
  void didUpdateWidget(covariant BannerAdWidget oldWidget) {
    super.didUpdateWidget(oldWidget);
    _bannerAd?.dispose();
    _bannerAd = null;
    createAd();
    setState(() {
      _isReady = false;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Container(
      color: Colors.orange,
      width: widget.size.width.toDouble(),
      height: widget.size.height.toDouble(),
      child: _isReady
          ? AdWidget(ad: _bannerAd)
          : Center(
              child: CircularProgressIndicator(),
            ),
    );
  }
}
```

load() した Ad はそれが必要なくなったときに dispose() しなければなりません。
AdListener を使って、ロードするのに失敗したときには dispose() します。
逆にロードに成功したときには _isReady を true にすることで、インジケータから広告に切り替えています。

初期化時にバナーを生成するのを一秒間遅延させています。これはそうしないと
```Ad with id `0` is not available for onAdLoaded.```
といわれてしまうことがあるからです。実行に影響はないのでそのままで良いかとも思いましたが、Example に倣って一秒遅延させています。
(AdInstanceManager が広告を読み込むタイミングでいわれるっぽいですが、その辺りイマイチよくわかっていません。詳しい方がいらっしゃったら教えて下さい。)


あとは適当にサイズをいじれるようにしたのが以下です。いい感じですね！

![banner.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/594767/f5c318ff-dfb4-31f8-d15a-f6e17f09e664.gif)


##インタースティシャル広告
バナー広告のついでに、インタースティシャル広告も実装します。
といってもやることはほぼバナー広告と同じです。
というより、こちらのほうが簡単かもしれません。

今回はインタースティシャル広告を表示するボタンを作成し、その State で InterstitialAd を管理します。

```dart

class InterstitialAdButton extends StatefulWidget {
  @override
  _InterstitialAdButtonState createState() => _InterstitialAdButtonState();
}

class _InterstitialAdButtonState extends State<InterstitialAdButton> {
  InterstitialAd _interstitialAd;
  bool _isReady = false;

  @override
  void initState() {
    super.initState();
    createAd();
  }

  void createAd() {
    _interstitialAd ??= InterstitialAd(
      adUnitId: InterstitialAd.testAdUnitId,
      request: AdRequest(),
      listener: AdListener(
        onAdLoaded: (ad) {
          print('${ad.runtimeType} loaded!');
          _isReady = true;
        },
        onAdFailedToLoad: (ad, error) {
          print('${ad.runtimeType} failed to load.\n$error');
          ad.dispose();
          _interstitialAd = null;
          createAd();
        },
        onAdOpened: (Ad ad) => print('${ad.runtimeType} opened!'),
        onAdClosed: (Ad ad) {
          print('${ad.runtimeType} closed.');
          ad.dispose();
          createAd();
        },
        onApplicationExit: (Ad ad) =>
            print('${ad.runtimeType} onApplicationExit.'),
      ),
    )..load();
  }

  @override
  void dispose() {
    _interstitialAd.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return ElevatedButton(
      child: Text('show Interstitial Ad'),
      onPressed: () {
        if (!_isReady) return;
        _interstitialAd.show();
        _isReady = false;
        _interstitialAd = null;
      },
    );
  }
}
```
バナー広告とほとんど同じ感じです。
見た目はこんな感じ。

![interstitial.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/594767/356c4b72-e5a9-5ae7-ea17-b687789f96ff.gif)

##リワード広告
リワード広告についても変わりません。
インタースティシャル広告のコードをまるまるコピー＆ペーストして、クラス名を少し変えてやれば動きます。
リワード広告とインタースティシャル広告の実装面での違いは、その名の通りリワード広告には報酬があることです。報酬受取りの処理については AdListener に onRewardedAdUserEarnedReward コールバックを渡すことで実装できます。
ここでは、状態として得た報酬の値をもち、ボタンの下にそれを表示するようにしています。

```dart
class RewardedAdButton extends StatefulWidget {
  @override
  _RewardedAdButtonState createState() => _RewardedAdButtonState();
}

class _RewardedAdButtonState extends State<RewardedAdButton> {
  RewardedAd _rewardedAd;
  bool _isReady = false;

  num reward = 0;

  @override
  void initState() {
    super.initState();
    createAd();
  }

  void createAd() {
    _rewardedAd ??= RewardedAd(
      adUnitId: RewardedAd.testAdUnitId,
      request: AdRequest(),
      listener: AdListener(
        onAdLoaded: (ad) {
          print('${ad.runtimeType} loaded!');
          _isReady = true;
        },
        onAdFailedToLoad: (ad, error) {
          print('${ad.runtimeType} failed to load.\n$error');
          ad.dispose();
          _rewardedAd = null;
          createAd();
        },
        onAdOpened: (Ad ad) => print('${ad.runtimeType} opened!'),
        onAdClosed: (Ad ad) {
          print('${ad.runtimeType} closed.');
          ad.dispose();
          createAd();
        },
        onApplicationExit: (Ad ad) =>
            print('${ad.runtimeType} onApplicationExit.'),
        onRewardedAdUserEarnedReward: (ad, item) {
          print(
              '$RewardedAd with reward $RewardItem(${item.amount}, ${item.type})');
          setState(() {
            reward += item.amount;
          });
        },
      ),
    )..load();
  }

  @override
  void dispose() {
    _rewardedAd.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        ElevatedButton(
          child: Text('show Rewarded Ad'),
          onPressed: () {
            if (!_isReady) return;
            _rewardedAd.show();
            _isReady = false;
            _rewardedAd = null;
          },
        ),
        Text('Current Rewards: $reward'),
      ],
    );
  }
}

```


![reward.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/594767/bfc4443a-cd70-131c-ac30-ec84a674d0bb.gif)

ここまでで、こんな風になります。
テスト広告だと報酬アイテムのタイプは coin で、数量は 10 になっています。
これはどうやらリワード広告の広告枠で設定できるようです。

#おわりに
想像していたよりも簡単だった印象です。
ベータテスト段階であることに注意が必要ですが、これならすぐに組み込むことができそうです。

なお、ネイティブ広告については今回は試しませんでした。ただ、今後のアップデートでUI設計が Flutter 側でできるようになれば良いと考えています。
(いまのところ、NativeAds のUI部分についてはネイティブ側で実装する必要があり、Flutter 側でそれをそのまま Widget として表示するという仕組みのようです。そちらについては時間があれば別の記事としてまとめようと思います……)

また、ここまでの内容で誤りや良くない実装法、改善案などありましたらコメントでご指摘ください。

#参考
https://developers.googleblog.com/2021/03/announcing-flutter-2.html

https://pub.dev/packages/google_mobile_ads

https://codelabs.developers.google.com/codelabs/admob-inline-ads-in-flutter