Skip to content

Commit a9d2e3a

Browse files
committed
refactor(ads): make AdmobInlineAdWidget stateless to fix lifecycle crash
Converted `AdmobInlineAdWidget` from a `StatefulWidget` to a `StatelessWidget`. This is the core fix for the "AdWidget requires Ad.load" crash. The stateful widget's `dispose` method was prematurely destroying the native AdMob ad object when it scrolled out of view. The `InlineAdCacheService` would then serve this "zombie" ad when the user scrolled back, causing the crash. By making the widget stateless and removing all lifecycle management (including the `dispose` method), the responsibility for the ad's lifecycle is now correctly and solely managed by the `InlineAdCacheService`. The widget's only job is to render a valid, pre-loaded ad.
1 parent bd40a47 commit a9d2e3a

File tree

1 file changed

+39
-81
lines changed

1 file changed

+39
-81
lines changed

lib/ads/widgets/admob_inline_ad_widget.dart

Lines changed: 39 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -7,19 +7,22 @@ import 'package:google_mobile_ads/google_mobile_ads.dart' as admob;
77
import 'package:logging/logging.dart';
88

99
/// {@template admob_inline_ad_widget}
10-
/// A widget that specifically renders an inline Google AdMob ad,
11-
/// supporting both native and banner ad formats.
10+
/// A stateless widget that renders a pre-loaded inline Google AdMob ad.
1211
///
1312
/// This widget is responsible for taking a generic [InlineAd]
1413
/// and rendering it using the `AdWidget` from the `google_mobile_ads` package.
1514
/// It dynamically determines if the underlying `adObject` is an `admob.NativeAd`
1615
/// or an `admob.BannerAd` and adjusts its rendering and size accordingly.
1716
///
18-
/// This is a [StatefulWidget] to properly manage the lifecycle of the AdMob
19-
/// ad object, ensuring it is disposed when the widget is removed from the tree
20-
/// or when the underlying ad object changes.
17+
/// **IMPORTANT:** This widget is intentionally stateless. It does **not** manage
18+
/// the lifecycle of the ad object (e.g., loading or disposing). Its sole
19+
/// responsibility is to render a valid, pre-loaded ad. The lifecycle, including
20+
/// caching and disposal, is managed by the [InlineAdCacheService] and the
21+
/// ad loader widgets ([FeedAdLoaderWidget], [InArticleAdLoaderWidget]). This
22+
/// prevents the ad from being destroyed when it scrolls out of view, which is
23+
/// the root cause of the "Ad.load to be called" error.
2124
/// {@endtemplate}
22-
class AdmobInlineAdWidget extends StatefulWidget {
25+
class AdmobInlineAdWidget extends StatelessWidget {
2326
/// {@macro admob_inline_ad_widget}
2427
const AdmobInlineAdWidget({
2528
required this.inlineAd,
@@ -28,131 +31,86 @@ class AdmobInlineAdWidget extends StatefulWidget {
2831
super.key,
2932
});
3033

31-
/// The generic inline ad model which contains the provider-specific AdMob ad object.
34+
/// The generic inline ad model which contains the provider-specific AdMob ad
35+
/// object. This ad is expected to be fully loaded and valid.
3236
final InlineAd inlineAd;
3337

34-
/// The user's preference for feed layout, used to determine the ad's visual size.
35-
/// This is only relevant for native ads.
38+
/// The user's preference for feed layout, used to determine the ad's visual
39+
/// size. This is only relevant for native ads.
3640
final HeadlineImageStyle? headlineImageStyle;
3741

3842
/// The preferred shape for banner ads, used for in-article banners.
3943
final BannerAdShape? bannerAdShape;
4044

4145
@override
42-
State<AdmobInlineAdWidget> createState() => _AdmobInlineAdWidgetState();
43-
}
44-
45-
class _AdmobInlineAdWidgetState extends State<AdmobInlineAdWidget> {
46-
admob.Ad? _ad;
47-
final Logger _logger = Logger('AdmobInlineAdWidget');
48-
49-
@override
50-
void initState() {
51-
super.initState();
52-
_setAd();
53-
}
54-
55-
@override
56-
void didUpdateWidget(covariant AdmobInlineAdWidget oldWidget) {
57-
super.didUpdateWidget(oldWidget);
58-
// If the inlineAd object reference itself has changed, dispose the old ad
59-
// and set the new one. This is a more robust check than just comparing IDs,
60-
// as a new InlineAd instance might be created for the same logical ad slot.
61-
if (widget.inlineAd != oldWidget.inlineAd) {
62-
_disposeCurrentAd();
63-
_setAd();
64-
}
65-
}
66-
67-
@override
68-
void dispose() {
69-
// Dispose the AdMob ad object when the widget is removed from the tree.
70-
// This is crucial to prevent "AdWidget is already in the Widget tree" errors
71-
// and memory leaks, as each AdWidget instance should manage its own ad object.
72-
_disposeCurrentAd();
73-
_logger.info(
74-
'AdmobInlineAdWidget disposed. Ad object explicitly disposed.',
75-
);
76-
super.dispose();
77-
}
46+
Widget build(BuildContext context) {
47+
final logger = Logger('AdmobInlineAdWidget');
48+
admob.Ad? ad;
7849

79-
/// Sets the internal [_ad] object from the widget's [inlineAd].
80-
///
81-
/// This method ensures that the adObject is of the correct AdMob type
82-
/// (NativeAd or BannerAd) and logs an error if it's not.
83-
void _setAd() {
84-
if (widget.inlineAd.adObject is admob.NativeAd) {
85-
_ad = widget.inlineAd.adObject as admob.NativeAd;
86-
} else if (widget.inlineAd.adObject is admob.BannerAd) {
87-
_ad = widget.inlineAd.adObject as admob.BannerAd;
50+
// Safely cast the generic adObject to a specific AdMob ad type.
51+
if (inlineAd.adObject is admob.NativeAd) {
52+
ad = inlineAd.adObject as admob.NativeAd;
53+
} else if (inlineAd.adObject is admob.BannerAd) {
54+
ad = inlineAd.adObject as admob.BannerAd;
8855
} else {
89-
_ad = null;
90-
_logger.severe(
56+
ad = null;
57+
logger.severe(
9158
'The provided ad object for AdMob inline ad is not of type '
9259
'admob.NativeAd or admob.BannerAd. Received: '
93-
'${widget.inlineAd.adObject.runtimeType}. Ad will not be displayed.',
60+
'${inlineAd.adObject.runtimeType}. Ad will not be displayed.',
9461
);
9562
}
96-
}
97-
98-
/// Disposes the currently held [_ad] object if it's an [admob.Ad].
99-
void _disposeCurrentAd() {
100-
if (_ad is admob.Ad) {
101-
_logger.info('Disposing AdMob ad object: ${_ad!.adUnitId}');
102-
_ad!.dispose();
103-
_ad = null;
104-
}
105-
}
10663

107-
@override
108-
Widget build(BuildContext context) {
109-
if (_ad == null) {
64+
if (ad == null) {
11065
// Return an empty widget if the ad object is not of the correct type
11166
// or if it was explicitly set to null due to an error.
11267
return const SizedBox.shrink();
11368
}
11469

11570
// Determine the height based on the actual ad type and headlineImageStyle.
11671
double adHeight;
117-
if (widget.inlineAd is NativeAd) {
118-
final nativeAd = widget.inlineAd as NativeAd;
72+
if (inlineAd is NativeAd) {
73+
final nativeAd = inlineAd as NativeAd;
11974
adHeight = switch (nativeAd.templateType) {
12075
NativeAdTemplateType.small => 120,
12176
NativeAdTemplateType.medium => 250,
12277
};
123-
} else if (widget.inlineAd is BannerAd) {
78+
} else if (inlineAd is BannerAd) {
12479
// For banner ads, prioritize bannerAdShape if provided (for in-article ads).
12580
// Otherwise, fall back to headlineImageStyle (for feed ads).
126-
if (widget.bannerAdShape != null) {
127-
adHeight = switch (widget.bannerAdShape) {
81+
if (bannerAdShape != null) {
82+
adHeight = switch (bannerAdShape) {
12883
BannerAdShape.square => 250,
12984
BannerAdShape.rectangle => 50,
13085
_ => 50,
13186
};
13287
} else {
133-
adHeight =
134-
widget.headlineImageStyle == HeadlineImageStyle.largeThumbnail
88+
adHeight = headlineImageStyle == HeadlineImageStyle.largeThumbnail
13589
? 250 // Assumes large thumbnail feed style wants a medium rectangle banner
13690
: 50;
13791
}
13892
} else {
13993
// Fallback height for unknown inline ad types.
94+
logger.warning(
95+
'Unknown InlineAd type: ${inlineAd.runtimeType}. '
96+
'Defaulting to height 100.',
97+
);
14098
adHeight = 100;
14199
}
142100

143101
// The AdWidget from the google_mobile_ads package handles the rendering
144102
// of the pre-loaded AdMob ad.
145103
// We wrap it in a SizedBox to provide explicit height constraints,
146-
// which is crucial for platform views (like native ads) within scrollable
147-
// lists to prevent "unbounded height" errors.
104+
// which is crucial for platform views (like native ads) within
105+
// scrollable lists to prevent "unbounded height" errors.
148106
return SizedBox(
149107
height: adHeight,
150108
// Use a ValueKey derived from the adObject's hashCode to force Flutter
151109
// to create a new AdWidget instance if the underlying ad object changes.
152110
// This prevents the "AdWidget is already in the Widget tree" error.
153111
child: admob.AdWidget(
154-
key: ValueKey(_ad!.hashCode),
155-
ad: _ad! as admob.AdWithView,
112+
key: ValueKey(ad.hashCode),
113+
ad: ad as admob.AdWithView,
156114
),
157115
);
158116
}

0 commit comments

Comments
 (0)