Flutterで広告を貼る備忘録（admob）

flutterで開発したアプリに広告を貼る方法をメモしておく。
勉強中の備忘録なのでかなり雑な文章だと思うし、間違えもあると思ういます。なんなら指摘して欲しいです。
## 環境とか   
|  | バージョン |
|:-:|:-:|
|Flutter   |3.3.0   |
|Dart   |2.18.0   |
|google_mobile_ads   |2.0.1   |
|provider|6.0.3|

## 勉強方法　
とりあえずYouTubeでFlutter公式の動画を見る。 
<iframe width="560" height="315" src="https://www.youtube.com/embed/m0d_pbgeeG8" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
おじさんの顔が終始愛らしくていい感じ。 

## 実際の手順　
こっから↑の動画の内容をただただ書き綴っていく(?) 
### 1 Google AdMobの設定
####  アカウント登録　
↑これはやってたので省略。。。 
https://admob.google.com/home/?utm_source=sem&utm_medium=text&utm_campaign=2022-admob-apac-jp&utm_content=rsa&gclid=EAIaIQobChMIuZvWrZ72-QIVFnmLCh3TmgY9EAAYASAAEgKAtfD_BwE&gclsrc=aw.ds
#### アプリの登録 
左の**apps**タブから**add app**を選択 
↓ 
androidかiosを選択（後でもう片方も作るので適当でいい）
↓ 
「アプリストアに登録してる？」的な項目はNO（開発中なので）
↓ 
作ったものを選択して**ADD AD UNIT**
↓ 
適当なフォーマットを選ぶ
今回は動画と同じようにするてめbanneを選ぶ 
↓ 
適当な**Ad unit name**を決めてcreate
動画内では「A fun unit」なのでまじでなんでもいい

### 2 Flutterでadmobの設定 
#### google_mobile_adsを追加 
バージョンは多分https://pub.dev/packages/google_mobile_ads から適当にコピペしてきたもので大丈夫。
`google_mobile_ads: ^2.0.1`をルートディレクトリのpubspeck.yamlに追加する。
```yaml:pubspeck.yaml
dependencies:
  flutter:
    sdk: flutter

  cupertino_icons: ^1.0.2

  google_mobile_ads: ^2.0.1
```
#### app idを設定 
android用とios用で別々の場所に記載する必要があるから注意！！ 
##### android
android > app > src > main > AndroidManifest.xmlの`application`内の最後に`<meta-data android:name="com.google.android.gms.ads.APPLICATION_ID" android:value="{app id}" />`を追記
```xml:AndroidManifest.xml
        </activity>
        <!-- Don't delete the meta-data below.
             This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
        <meta-data
            android:name="flutterEmbedding"
            android:value="2" />
        <meta-data
            android:name="com.google.android.gms.ads.APPLICATION_ID"
            android:value="ca-app-pub-0000000000~0000000000"  />
</application>

```

##### ios
iosフォルダを右クリックしてxcodeで開く(vscodeなら)
↓ 
Runner > infoを開いて右クリックで**add row**
↓ 
key => GADApplicationIdentifier
type => String
value => ios用のapp id
### 3 広告を貼る(テスト用)
#### initializeする 
とりあえずmain.dartにgoogle_mobile_adsをインポート 
```dart:main.dart
import 'package:google_mobile_ads/google_mobile_ads.dart';
```
↓ 
main関数内でイニシャライズ
```dart:main.dart
void main() {
  WidgetsFlutterBinding.ensureInitialized();
  final initFuture = MobileAds.instance.initialize();
  runApp(const MyApp());
}
```
`WidgetsFlutterBinding.ensureInitialized();`を入れると、runApp()の前に呼び出すとランタイムエラーを回避できるらしい。。。
よくわかんないけどいい感じっぽいのでとりあえずやっとく。　
↓ 
lib直下にad_state.dartを作ってAdStateクラスを定義する。
```dart:ad_state.dart
import 'dart:io';
import 'package:google_mobile_ads/google_mobile_ads.dart';

class AdState {
  Future<InitializationStatus> initialization;

  AdState(this.initialization);

  String get bannerAdUnitId => Platform.isAndroid
      ? "ca-app-pub-3940256099942544/6300978111"
      : "ca-app-pub-3940256099942544/2934735716";
}
```
とりあえずテスト広告ユニットを使う
android→https://developers.google.com/admob/android/test-ads?hl=ja 
ios→https://developers.google.com/admob/ios/test-ads?hl=ja 
ここでも愛らしいおじさんが動画で解説してくれる。うれしい。
↓
main関数に戻っていろいろ追記
(ここでproviderをpubspeckに追加しなきゃいけない。なんか多分言及されていない？)
```dart:main.dart
import 'package:flutter/material.dart';
import 'package:google_mobile_ads/google_mobile_ads.dart';
import 'ad_state.dart'; // add
import 'package:provider/provider.dart'; // add

void main() {
  WidgetsFlutterBinding.ensureInitialized();
  final initFuture = MobileAds.instance.initialize();
  final adState = AdState(initFuture); // add
  runApp(
    Provider.value(
      // add
      value: adState, // add
      builder: (context, child) => const MyApp(), // add
    ),
  );
}
```
（ここでcocoapodsのなんちゃらが古すぎるからなんちゃらって言われて`flutter run`できなかったので、言われるがままに`pod repo update`した。なんとかなった。）
#### 広告を貼るスペースを確保する 
動画に則って進めるためにいい感じにリストビューを表示するスクリーン用のファイルを作った(list_screen.dart)
広告用のスペースはColumn > childern > Expandedの並列のとこに置いとくといい感じ。 
SiedBoxの部分。
```dart:list_screen.dart
import 'package:flutter/material.dart';

class ListScreen extends StatefulWidget {
  const ListScreen({super.key});

  @override
  State<ListScreen> createState() => _ListScreenState();
}

class _ListScreenState extends State<ListScreen> {
  Widget _tile(int index) {
    return Container(
      decoration: const BoxDecoration(
        border: Border(
          bottom: BorderSide(
            width: 1.0,
            color: Colors.grey,
          ),
        ),
      ),
      child: ListTile(
        title: Text(
          "item $index",
          style: const TextStyle(
            fontSize: 20.0,
            color: Colors.black,
          ),
        ),
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text("admob sample"),
      ),
      body: Column(
        children: <Widget>[
          Expanded(
            child: ListView.builder(
              itemBuilder: (context, index) => _tile(index + 1),
              itemCount: 100,
            ),
          ),
          const SizedBox(height: 50),
        ],
      ),
    );
  }
}

```
横320*縦50が一般的でちょうどいいらしい（ま？）
#### adListenerを追加 
```dart:ad_state.dart
import 'dart:io';
import 'package:google_mobile_ads/google_mobile_ads.dart';

class AdState {
  Future<InitializationStatus> initialization;

  AdState(this.initialization);

  String get bannerAdUnitId => Platform.isAndroid
      ? "ca-app-pub-3940256099942544/6300978111"
      : "ca-app-pub-3940256099942544/2934735716";

  BannerAdListener get adListener => _adListener;

  BannerAdListener _adListener = BannerAdListener(
    onAdLoaded: (ad) => print('Ad loaded: ${ad.adUnitId}'),
    onAdClosed: (ad) => print('Ad closed: ${ad.adUnitId}'),
    onAdFailedToLoad: (ad, error) =>
        print('Ad failed to load ${ad.adUnitId}, $error'),
    onAdOpened: (ad) => print('Ad opened ${ad.adUnitId}'),
  );
}
```
動画では`AdListener`だけどないって言われる。代わりに`BannerAdListener`にするといいっぽい。
[この記事](https://zenn.dev/kentaroh/articles/729a52a3e90bca)を参考にした。
ここら辺は普通に意味わかってない。
##### AdStateクラスの中でBannerAdListener初期化するのやめた（追記 9/13） 
[この記事（上のと同じやつ）](https://zenn.dev/kentaroh/articles/729a52a3e90bca)に則って、`didChangeDependencies()`の中の`banner = BannerAd()`のlistenerに`BannerAdListener()`を直接渡す？（表現が合ってるのかわからん）
```dart:list_screen.dart
  @override
  void didChangeDependencies() {
    super.didChangeDependencies();
    final adState = Provider.of<AdState>(context);
    adState.initialization.then((status) {
      setState(() {
        banner = BannerAd(
          adUnitId: adState.bannerAdUnitId,
          size: AdSize.banner,
          request: const AdRequest(),
          listener: BannerAdListener(
            onAdLoaded: (ad) {
              print('${ad.adUnitId} loaded');
              setState(() {
                _hasBanner = true;
              });
            },
            onAdFailedToLoad: (ad, error) {
              print('${ad.adUnitId} failed; $error');
              setState(() {
                _hasBanner = false;
              });
            },
            onAdOpened: (ad) => print('${ad.adUnitId} opend'),
            onAdClosed: (ad) => print('${ad.adUnitId} closed'),
          ),
        )..load();
      });
      // _hasBanner = true;
    });
  }
```
**↓理由**
`setState()`の中で_hasBannerの値を変更したかった。 
ProviderとChangeNotifierを使ったり、then()とかwhenComplete()でチェーンしようとしたけど何かダメだった。ダメだった理由はいまいち理解できてないからいつか理解したい。

↓とりあえずそれっぽい考察
###### then() or whenComplete()
`load()`とか`setState(() {//...いろいろ})`にチェーンでできないかと思ったがダメだった。 
AdStateクラスの中のonAdLoad()とonAdFailedToLoad()が実行されるのが遅いため、then()とかwhenComplete()の中身の方が先に実行されてしまうっぽい？
いや、こいつらって処理が全部終わらないと実行されないんじゃないの？ってめっちゃ思った。 
おそらく、`banner = BannerAd()`自体は終わるからんなんでしょう。わからん。。。 
Futureで解決できんじゃね？とか思ったけど理解が浅いので保留したら解決してた。
###### Provider & ChangeNotifier
AdStateクラス内の`onAdLoad()`と`onAdFailedToLoad()`の中で状態の変化を検知できればいいんじゃないかと考えたけど、contextを渡せなくて詰んだ。そもそもWidgetじゃないから無理？ 
工夫すればできたのかも知れないけど、これも理解が浅いのでがんばれなかった。次は頑張れるようになっていたい。
#### 広告をロードする
_ListScreenStateの中に追加
```dart:list_screen.dart
class _ListScreenState extends State<ListScreen> {
  late BannerAd banner; // add

  @override
  void didChangeDependencies() { // add
    super.didChangeDependencies();
    final adState = Provider.of<AdState>(context);
    adState.initialization.then((status) {
      setState(() {
        banner = BannerAd(
          adUnitId: adState.bannerAdUnitId,
          size: AdSize.banner,
          request: const AdRequest(),
          listener: adState.adListener,
        )..load();
      });
    });
  }
```
#### 広告を表示する 
動画のままだと一瞬エラーが出てだるいので少し変更を加える
`bool _hasBanner`を用意して切り替えを行う 
```dart:list_screen.dart
import 'package:flutter/material.dart';
import 'package:google_mobile_ads/google_mobile_ads.dart';
import 'package:provider/provider.dart';
import 'ad_state.dart';

class ListScreen extends StatefulWidget {
  const ListScreen({super.key});

  @override
  State<ListScreen> createState() => _ListScreenState();
}

class _ListScreenState extends State<ListScreen> {
  late BannerAd banner;
  bool _hasBanner = false; // add

  @override
  void didChangeDependencies() {
    super.didChangeDependencies();
    final adState = Provider.of<AdState>(context);
    adState.initialization.then((status) {
      setState(() {
        banner = BannerAd(
          adUnitId: adState.bannerAdUnitId,
          size: AdSize.banner,
          request: const AdRequest(),
          listener: adState.adListener,
        )..load();
      });
      _hasBanner = true; // add
    });
  }

  Widget _tile(int index) {
    return Container(
      decoration: const BoxDecoration(
        border: Border(
          bottom: BorderSide(
            width: 1.0,
            color: Colors.grey,
          ),
        ),
      ),
      child: ListTile(
        title: Text(
          "item $index",
          style: const TextStyle(
            fontSize: 20.0,
            color: Colors.black,
          ),
        ),
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text("admob sample"),
      ),
      body: Column(
        children: <Widget>[
          Expanded(
            child: ListView.builder(
              itemBuilder: (context, index) => _tile(index + 1),
              itemCount: 100,
            ),
          ),
          if (_hasBanner == false) // change
            const SizedBox(height: 50)
          else
            Container(
              height: 50,
              child: AdWidget(ad: banner),
            ),
        ],
      ),
    );
  }
}
``` 
動画の愛くるしいおじさんは`if (banner == null)`で切り替えてるけど、そもそもnull-safety(?)でwarning出るし、画面が一瞬エラーになる。 
### 4 本番用の広告 
本番用の広告を貼ろうとしたら、 
> Publisher data not found 

というエラーを吐かれてロードできなかった。　
[admobのヘルプ](https://support.google.com/admob/answer/9905175#9&zippy=%2C%E3%83%91%E3%83%96%E3%83%AA%E3%83%83%E3%82%B7%E3%83%A3%E3%83%BC-%E3%83%87%E3%83%BC%E3%82%BF%E3%81%8C%E8%A6%8B%E3%81%A4%E3%81%8B%E3%82%8A%E3%81%BE%E3%81%9B%E3%82%93)を読むと
> お支払い情報に未入力の箇所がないか、その他に設定上の問題がないかなど、アカウントのエラーを確認する 

とあるので、支払い情報を設定するといけるかも？
他にもadmobの設定してから1時間ほどお待ちくださいっていうのがあったけど、余裕で超えてるので多分これは違う。
#### 支払い情報設定後（追記 9/13）
情報を入力してもダメだった 
> Request Error: No ad to show. 

というエラーが出る。 
軽く調べた感じ、本番用の広告はストアに登録してるアプリじゃないとロードできないっぽい？ 
シミュレーターとか、テスト用に登録したデバイスでは本番用のユニットを使ってもテスト広告を流してくれる的なことがどっかに書いてあった気がする、仕様に変更があった？ 
理由はよくわからなかった（というかそこまで調べる気力が湧かなかった）けど、とりあえずデバッグでは現状確認できなかった。
## 余談
リストの途中にちょいちょい広告を挟む方法が最後に紹介されているけど、面倒だったのでスキップした。
[この記事](https://zenn.dev/kentaroh/articles/729a52a3e90bca)の人は公式のドキュメントを読んでやったらしい。記事内でも言われてるけど、バージョンアップの関係(?)で時々そのまま書いてもダメなところがある。公式大事。動画も公式だけど。
プログラムを書きながらこの記事を書いてるので、修正し忘れてるところとかあるかも。がんばれ未来の自分。