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が使用されているので、ダブって使わないように、ということらしい。

 

本記事はライトな日記思考で書いているので、詳細説明はしておらず、基本、テキストのみで画像とかはあまり載せておりません。。m(_ _)m

解説記事ではないため、解決していない内容や、その時々の間違った解釈を述べてしまっている可能性が大いにありますので、何卒、ご了承ください。

 

前の画面で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の基礎文法を素早くインプットできる/


Dart入門 - Dartの要点をつかむためのクイックツアー

タイトルとURLをコピーしました