Flutter: AdMobのバナー広告を別画面で再利用したら「This AdWidget is already in the Widget tree」のエラー
結論:画面遷移メソッド(Navigator.…)の直前でdisposeする
2022/12/23 Flutter エラー・バグ日記
AdMob(google_mobile_ads)のバナー広告を表示する際、複数の画面内で「BannerAd」クラスのインスタンスを使い回そうとしたら、2つ目の画面に遷移したときに、以下のエラーが発生した。
This AdWidget is already in the Widget tree
If you placed this AdWidget in a list, make sure you create a new instance in the builder function with a unique ad object.
Make sure you are not using the same ad object in more than one AdWidget
既にAdWidgetが使用されているので、ダブって使わないように、ということらしい。
前の画面でdisposeしていてもエラーになる
これは以前、下記日記に掲載した内容と同じエラー。
このときは、同じ画面内に複数のバナー広告を出そうとして発生したので、「BannerAd」のインスタンスを別々に(別の変数名で)作成することで解消した(「adUnitId」は同じIDを使用)。
しかし、今回はあくまで1画面内に1つのバナー広告しか出さないため、画面が違えば、同じインスタンスでも使い回せるはず。。
最初の画面(Stateful Widgetで作成)から出るときに、きちんと下記のとおりインスタンスを破棄(dispose)し、次の画面で再ロードする処理をしていたのだが。。。
// 次の画面での再ロード部分は省略 // クラス名、メソッド名、プロパティ名(変数名)について、筆者が作成したもの(名前変更可のもの) // の名前の末尾には、大文字のオー「O」をつけています // ※ライブラリ(パッケージ)で予め決められているもの(名前の変更不可のもの)と、 // 自分で作成したもの(名前の変更可のもの)の区別をしやすくするため @override void dispose() { // bannerAdO は、BannerAd クラスのインスタンス bannerAdO?.dispose(); super.dispose(); }
Stateful Widgetの「dispose」メソッドは、次の画面の描画終了後に実行される模様
Flutter公式の「dispose」メソッドの説明
を見ると、「dispose」メソッドは、「このオブジェクトがツリーから完全に削除されたときに呼び出されます。」とのこと。
以前から「dispose」の完了タイミングがよく分からないと思っていたので、下記のような簡単なコードを作ってテストしてみた(広告関連は実装していない)。
import 'package:flutter/material.dart'; void main() => runApp(MyApp()); class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( title: "Test", theme: ThemeData.light(), home: SampleScreenO(), ); } } /// 最初の画面 class SampleScreenO extends StatefulWidget { @override _SampleScreenOState createState() => _SampleScreenOState(); } class _SampleScreenOState extends State<SampleScreenO> { @override void dispose() { debugPrint("disposeした"); super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: Text("FirstPage")), body: Center( child: TextButton( child: Text("次の画面へ"), onPressed: () { Navigator.pushReplacement( context, MaterialPageRoute( builder: (_) => NextPageO(), ), ); }, ), ), ); } } /// 次の画面の画面 class NextPageO extends StatefulWidget { const NextPageO({Key? key}) : super(key: key); @override State<NextPageO> createState() => _NextPageOState(); } class _NextPageOState extends State<NextPageO> { @override Widget build(BuildContext context) { debugPrint("次のページを描画"); return Scaffold( appBar: AppBar(title: Text("NextPage")), body: Center(child: TextWidgetO()), ); } } /// 次の画面のText Widget部分 class TextWidgetO extends StatelessWidget { const TextWidgetO({Key? key}) : super(key: key); @override Widget build(BuildContext context) { debugPrint("次のページのTextWidgetを描画"); return Text("次のページ"); } }
コンソールに出力してみた実行結果は以下のとおり。
実は「dispose」メソッドは、次の画面描画が終わった後に実行されていた。
ということは、恐らく、最初の画面のバナー広告のインスタンスが「dispose」される前に、次の画面のバナー広告の表示が始まってしまうため、エラーになったのだろう。
公式サイトには具体的な「dispose」の利用法の解説は無い
とは言え、どのタイミングで「dispose」すれば良いのだろうか。。
画面毎に異なるインスタンスを用意し、アプリ起動中はこれらのバナー広告をdisposeしない、という方法も考えられるが、メモリーリークの恐れもあり、望ましくはなさそう。。
下記にあるGoogleのFlutter用AdMobの公式説明
には、
dispose() を呼び出すおすすめのタイミングは、AdWidget がウィジェットツリーから削除された後、または AdListener.onAdFailedToLoad コールバック時です。
という説明しか無い。。
後者の「onAdFailedToLoad コールバック時」にdisposeする方法については、下記サンプルコード
で使用例を確認できるが、この方法で「dispose」を実装しても、今回のエラーは解消できなかった。
他に、同じ悩みが報告されていないか、ググってみると、下記Q&A記事が見つかった。
こちらの回答では、バナー広告を表示する画面(Stateful WIdget)の「initState」メソッド内で、「dispose」してからロードする方法が提案されている。
確かにこの方法なら解決しそうではあるが、自分のコード構造では適用が難しかった。。(広告のロードは、広告表示用に作ったクラス内で実行しており、画面毎にロードする仕様にしていなかったため)
次の画面描画前に待ち時間を入れるか、画面遷移時にdisposeするか
試しに、次の画面を描画する際に「Future.delayed」で待ち時間を入れたところ、「dispose」が先に実行されることを確認できた。
しかし、どの程度の待ち時間なら確実にdisposeされるのか確証が持てず、また、無駄な待ち時間も入れたくはない。。
試行錯誤の結果、Stateful Widgetの「dispose」メソッドは使用せず、下記のとおり、次の画面に遷移する「Navigator.pushReplacement」の実行直前に、バナー広告の「dispose」メソッドを入れてみた。
// TextButtonで次の画面に遷移するケース TextButton( child: Text("次のバナー広告表示画面へ"), onPressed: () async{ // 画面遷移直前でバナー広告を dispose する // 「dispose」は非同期処理のため、完了を待ってから画面遷移されるよう async await を付ける await bannerAdO?.dispose(); Navigator.pushReplacement( context, MaterialPageRoute( builder: (_) => NextScreenO(), ), ); }, ),
この結果、エラーは発生せず、次の画面でも同じインスタンスを使ったバナー広告を表示できるようになった。
ただ、この方法だと画面遷移の箇所に、都度dispose処理を入れる必要があり、やや面倒。。
もっと良い方法があるかもしれないので、引き続き検証してみる。
恐らく初歩的な話だったと思われるが、、「dispose」のタイミングを理解する良い機会になった。
\一般的なエラー対処法をまとめた記事はこちら/
リリースしたアプリ(全てFlutterで開発)
個人アプリ開発で役立ったもの
おすすめの学習教材
\超初心者向けでオススメな元Udemyの講座/
\キャンペーン時を狙えば安価で網羅的な内容が学べる(日本語訳あり)/
\Gitの基礎について無料で学べる/
おすすめの学習書籍
\実用的。image_pickerに関してかなり助けられた/
\Dartの基礎文法を素早くインプットできる/