@@ -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}
3341class 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