I've put in a github issue but just wanted to check with the community that this sounds right to everyone else.
Logically when creating and "ad" and an "adwidget" in flutter they may rebuild with the parent widgets, however, they may "logically" be the same ad shown in the same spot within the app (identical pixels even).
However it appears that the tracking for if an ad is already shown is rudimentary and doesn't account for duplication during rebuilds and then throws errors.
My example below will work where you pass in a provider or state handled ad into any widget that gets rebuilt by it's parent (future builder, navigator pops, provider watches, etc).
From here there is no real problem. But remove the Future.delayed and now as the rebuild has a temporary non-utilized object in the tree potentially while native platform views are being unmounted etc, we get the error thrown. Throw in the Future.delayed again and now with this rudimentary number being applied we wait for the previous widget to be fully cleaned up and then place this widget exactly where it was and no issue.
I just want to confirm that this sounds about right to be a github issue for them? To me it would make sense that their handling accounts for re-use in the same location rather than this type of handling? Or at least provide a callback for dispose of previous so we can knowingly say, yes this isn't a duplicate is a rebuild and we're willing to push it back in after disposal of parents.
So you can go through yourself just create a static ad with the test admob id, a stateful widget with a setstate on button press, then place the following widget into it, test both with and without the Future.delayed:
flutter doctor >
[✓] Flutter (Channel master, 3.38.0-1.0.pre-400, on macOS 26.0.1 25A362 darwin-arm64, locale en-AU)
[✓] Android toolchain - develop for Android devices (Android SDK version 35.0.0)
[✓] Xcode - develop for iOS and macOS (Xcode 26.1)
[✓] Chrome - develop for the web
[✓] Connected device (3 available)
[✓] Network resources
Widget:
import "package:flutter/material.dart"; import "package:flutter/scheduler.dart"; import "package:google_mobile_ads/google_mobile_ads.dart"; import "package:hookd/data/providers/explore_provider.dart"; import "package:provider/provider.dart";
enum AdState { idle, loading, loaded, error }
class AdViewMain extends StatefulWidget {
final bool dismissable;
final Ad? advertisement;
const AdViewMain({
required this.advertisement,
this.dismissable = false,
super.key,
});
@OverRide
State createState() => _AdViewMainState();
}
class _AdViewMainState extends State {
bool isLoading = true;
Ad? ad;
@OverRide
void initState() {
WidgetsBinding.instance.addPostFrameCallback((_) async {
setState(() => ad = widget.advertisement);
});
super.initState();
}
@OverRide
Widget build(BuildContext context) {
if (widget.advertisement == null) return _buildErrorPlaceholder(context);
if (ad == null) return _buildLoadingSkeleton(context);
return Stack(
alignment: Alignment.topRight,
children: [
_buildAdWidget(context),
if (widget.dismissable)
GestureDetector(
onTap: () async {
// Mark dismissed and coordinate safe disposal
context.read<ExploreProvider>().removeAd();
},
child: Padding(
padding: const EdgeInsets.only(top: 8, right: 8),
child: Container(
padding: const EdgeInsets.all(6),
margin: const EdgeInsets.all(6),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainer,
shape: BoxShape.circle,
),
child: const Icon(
Icons.close,
color: Colors.white,
size: 20,
),
),
),
),
],
);
}
Widget _buildAdWidget(BuildContext context) {
return ClipRRect(
borderRadius: BorderRadius.circular(12),
child: SizedBox(
child: AdWidget(ad: ad as BannerAd),
),
);
}
Widget _buildLoadingSkeleton(BuildContext context) {
return Container(
width: double.infinity,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainer,
borderRadius: BorderRadius.circular(24),
),
child: Center(
child: SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation(
Theme.of(context).colorScheme.primary.withOpacity(0.6),
),
),
),
),
);
}
Widget _buildErrorPlaceholder(BuildContext context) {
return Container(
width: double.infinity,
height: 60,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.errorContainer.withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: Theme.of(context).colorScheme.outline.withOpacity(0.2),
width: 1,
),
),
child: Center(
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.error_outline,
size: 16,
color: Theme.of(context).colorScheme.onSurfaceVariant.withOpacity(0.5),
),
const SizedBox(width: 8),
Text(
"Unable to load advertisement",
style: TextStyle(
fontSize: 12,
color: Theme.of(context).colorScheme.onSurfaceVariant.withOpacity(0.7),
fontWeight: FontWeight.w500,
),
),
],
),
),
);
}
}