‘This AdWidget is already in the Widget tree’ — 仅在某个特定流程后崩溃
Source: Dev.to
错误
我在使用 google_mobile_ads 显示横幅广告的 Flutter 应用中,某天在 MyPage 页面出现了以下错误:
This AdWidget is already in the Widget tree
Make sure you are not using the same ad object in more than one AdWidget.错误信息很明确,但崩溃并不总是出现。
观察到的行为
| 流程 | 结果 |
|---|---|
| 正常切换标签页 | 不会崩溃 |
| 前往其他页面后再返回 | 不会崩溃 |
| 更新地址 → 返回主页面 → 打开 MyPage 标签页 | 崩溃 |
只有地址更新的流程会触发该错误。
产生原因
应用使用 BottomNavigationBar 与 IndexedStack。在地址更新后,代码通过以下方式返回主页面:
Navigator.push(
context,
MaterialPageRoute(builder: (context) => MainPage()),
);Navigator.push 会在栈顶添加一个新页面,而 不会 移除旧页面。因此,旧的 MainPage(以及其中的 MyPage)仍然保留在内存中。
当在新的 MainPage 上打开 MyPage 时,旧的 MyPage 状态仍持有一个已挂载 AdWidget 的 BannerAd。于是两个 AdWidget 同时尝试使用同一广告资源,导致崩溃。
在普通的标签切换中只有一个 MainPage 与一个 MyPage 实例,所以不会出现此问题。
解决方案:清除导航栈
将 push 替换为 pushAndRemoveUntil:
Navigator.pushAndRemoveUntil(
context,
MaterialPageRoute(builder: (_) => MainPage()),
(route) => false,
);这样会移除所有之前的路由,确保旧的 MainPage 及其 MyPage 被销毁。用户再次打开 MyPage 时会创建全新的实例,从而消除重复的 AdWidget。更改后崩溃不再出现。
didUpdateWidget 与广告
原来的代码在每次 widget 更新时都会重新创建横幅广告:
@override
void didUpdateWidget(covariant MyPage oldWidget) {
super.didUpdateWidget(oldWidget);
_bannerAd?.dispose();
_bannerAd = AdHelper.createBannerAd();
}如果旧的 AdWidget 仍在树中,销毁并重新创建广告会导致同样的重复问题。解决办法是 完全移除这段逻辑,仅在 initState 与 dispose 中管理横幅广告:
@override
void initState() {
super.initState();
_bannerAd = AdHelper.createBannerAd();
}
@override
void dispose() {
_bannerAd?.dispose();
super.dispose();
}插件如何强制此规则
google_mobile_ads 插件会跟踪广告是否已经挂载在 widget 树中。在 ad_containers.dart 中:
_AdWidgetState.initState通过instanceManager.isWidgetAdIdMounted()检查广告的 ID。- 若广告已挂载,
build方法会抛出 “already in the Widget tree” 错误。 - 只有当
_AdWidgetState.dispose调用unmountWidgetAdId时,ID 才会被释放。
因此,同一个 BannerAd 只能 被一个 AdWidget 使用;插件在代码层面强制了这一限制。
教训
Navigator.push 会保留之前的页面(以及它拥有的任何原生资源)。当这些资源——例如广告——需要被释放时,必须将旧页面的状态从导航栈中移除。使用 pushAndRemoveUntil(或其他清栈的导航方式)来确保正确销毁,避免因重复的 AdWidget 导致的崩溃。