Skip to content

Commit 01439e3

Browse files
committed
fix(ads): prevent AdWidget reuse for AdMob in-article ads
- Implement logic to bypass cache for AdMob in-article ads - Add comments explaining the handling of AdMob ads vs. cached ads - Update dispose methods to only remove non-AdMob ads from cache - Modify _loadAd method to handle AdMob ads separately
1 parent 2244411 commit 01439e3

File tree

1 file changed

+52
-15
lines changed

1 file changed

+52
-15
lines changed

lib/ads/widgets/in_article_ad_loader_widget.dart

Lines changed: 52 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,14 @@ import 'package:ui_kit/ui_kit.dart';
2929
/// This approach decouples ad loading from the BLoC and ensures that native
3030
/// ad resources are managed efficiently, preventing crashes and improving
3131
/// scrolling performance in lists.
32+
///
33+
/// **AdMob In-Article Ad Handling:**
34+
/// For AdMob in-article ads, this widget intentionally bypasses the
35+
/// [InlineAdCacheService]. This is a critical design decision to prevent
36+
/// the "AdWidget is already in the Widget tree" error that occurs when
37+
/// navigating between multiple article detail pages. Each AdMob in-article
38+
/// ad will be loaded as a new instance, ensuring unique `admob.Ad` objects
39+
/// for each `AdmobInlineAdWidget` in the widget tree.
3240
/// {@endtemplate}
3341
class InArticleAdLoaderWidget extends StatefulWidget {
3442
/// {@macro in_article_ad_loader_widget}
@@ -87,6 +95,8 @@ class _InArticleAdLoaderWidgetState extends State<InArticleAdLoaderWidget> {
8795
// Dispose of the old ad's resources before loading a new one.
8896
final oldCacheKey =
8997
'in_article_ad_${oldWidget.slotConfiguration.slotType.name}';
98+
// Only dispose if it was actually cached (i.e., not an AdMob in-article ad).
99+
// The removeAndDisposeAd method handles the check internally.
90100
_adCacheService.removeAndDisposeAd(oldCacheKey);
91101

92102
if (_loadAdCompleter != null && !_loadAdCompleter!.isCompleted) {
@@ -107,6 +117,8 @@ class _InArticleAdLoaderWidgetState extends State<InArticleAdLoaderWidget> {
107117
void dispose() {
108118
// Dispose of the ad's resources when the widget is permanently removed.
109119
final cacheKey = 'in_article_ad_${widget.slotConfiguration.slotType.name}';
120+
// Only dispose if it was actually cached (i.e., not an AdMob in-article ad).
121+
// The removeAndDisposeAd method handles the check internally.
110122
_adCacheService.removeAndDisposeAd(cacheKey);
111123

112124
if (_loadAdCompleter != null && !_loadAdCompleter!.isCompleted) {
@@ -122,6 +134,12 @@ class _InArticleAdLoaderWidgetState extends State<InArticleAdLoaderWidget> {
122134
/// If found, it uses the cached ad. Otherwise, it requests a new in-article ad
123135
/// from the [AdService] using `getInArticleAd` and stores it in the cache
124136
/// upon success.
137+
///
138+
/// **AdMob Specific Behavior:**
139+
/// For AdMob in-article ads, this method intentionally bypasses the cache.
140+
/// This ensures that each `AdmobInlineAdWidget` receives a unique `admob.Ad`
141+
/// object, preventing the "AdWidget is already in the Widget tree" error
142+
/// when multiple article detail pages are in the navigation stack.
125143
Future<void> _loadAd() async {
126144
_loadAdCompleter = Completer<void>();
127145

@@ -130,30 +148,41 @@ class _InArticleAdLoaderWidgetState extends State<InArticleAdLoaderWidget> {
130148
// In-article ads are typically unique to their slot, so we use the slotType
131149
// as part of the cache key to differentiate them.
132150
final cacheKey = 'in_article_ad_${widget.slotConfiguration.slotType.name}';
133-
final cachedAd = _adCacheService.getAd(cacheKey);
151+
InlineAd? loadedAd;
152+
153+
// Determine if the primary ad platform is AdMob.
154+
final isAdMob = widget.adConfig.primaryAdPlatform == AdPlatformType.admob;
134155

135-
if (cachedAd != null) {
156+
if (!isAdMob) {
157+
// For non-AdMob platforms (e.g., Local, Demo), try to get from cache.
158+
final cachedAd = _adCacheService.getAd(cacheKey);
159+
if (cachedAd != null) {
160+
_logger.info(
161+
'Using cached in-article ad for slot: ${widget.slotConfiguration.slotType.name}',
162+
);
163+
if (!mounted) return;
164+
setState(() {
165+
_loadedAd = cachedAd;
166+
_isLoading = false;
167+
});
168+
if (_loadAdCompleter?.isCompleted == false) {
169+
_loadAdCompleter!.complete();
170+
}
171+
return;
172+
}
173+
} else {
136174
_logger.info(
137-
'Using cached in-article ad for slot: ${widget.slotConfiguration.slotType.name}',
175+
'AdMob is primary ad platform. Bypassing cache for in-article ad '
176+
'for slot: ${widget.slotConfiguration.slotType.name}.',
138177
);
139-
if (!mounted) return;
140-
setState(() {
141-
_loadedAd = cachedAd;
142-
_isLoading = false;
143-
});
144-
// Complete the completer only if it hasn't been completed already.
145-
if (_loadAdCompleter?.isCompleted == false) {
146-
_loadAdCompleter!.complete();
147-
}
148-
return;
149178
}
150179

151180
_logger.info(
152181
'Loading new in-article ad for slot: ${widget.slotConfiguration.slotType.name}',
153182
);
154183
try {
155184
// Call AdService.getInArticleAd with the full AdConfig.
156-
final loadedAd = await _adService.getInArticleAd(
185+
loadedAd = await _adService.getInArticleAd(
157186
adConfig: widget.adConfig,
158187
adThemeStyle: widget.adThemeStyle,
159188
);
@@ -162,7 +191,15 @@ class _InArticleAdLoaderWidgetState extends State<InArticleAdLoaderWidget> {
162191
_logger.info(
163192
'New in-article ad loaded for slot: ${widget.slotConfiguration.slotType.name}',
164193
);
165-
_adCacheService.setAd(cacheKey, loadedAd);
194+
// Only cache non-AdMob ads. AdMob ads are not cached to prevent reuse issues.
195+
if (!isAdMob) {
196+
_adCacheService.setAd(cacheKey, loadedAd);
197+
} else {
198+
_logger.info(
199+
'AdMob in-article ad not cached to prevent reuse issues.',
200+
);
201+
}
202+
166203
if (!mounted) return;
167204
setState(() {
168205
_loadedAd = loadedAd;

0 commit comments

Comments
 (0)