Skip to content

Commit 6390016

Browse files
authored
Merge pull request #119 from flutter-news-app-full-source-code/Robust-Ad-Lifecycle-Management-and-Intelligent-Placement
Robust ad lifecycle management and intelligent placement
2 parents 53a6e10 + 456f91a commit 6390016

File tree

4 files changed

+68
-48
lines changed

4 files changed

+68
-48
lines changed

lib/ads/widgets/admob_inline_ad_widget.dart

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -55,9 +55,10 @@ class _AdmobInlineAdWidgetState extends State<AdmobInlineAdWidget> {
5555
@override
5656
void didUpdateWidget(covariant AdmobInlineAdWidget oldWidget) {
5757
super.didUpdateWidget(oldWidget);
58-
// If the inlineAd object itself has changed (e.g., a new ad was loaded
59-
// for the same placeholder ID), dispose the old ad and set the new one.
60-
if (widget.inlineAd.id != oldWidget.inlineAd.id) {
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) {
6162
_disposeCurrentAd(); // Dispose the old ad object
6263
_setAd();
6364
}
@@ -146,7 +147,13 @@ class _AdmobInlineAdWidgetState extends State<AdmobInlineAdWidget> {
146147
// lists to prevent "unbounded height" errors.
147148
return SizedBox(
148149
height: adHeight,
149-
child: admob.AdWidget(ad: _ad! as admob.AdWithView), // Cast to AdWithView
150+
// Use a ValueKey derived from the adObject's hashCode to force Flutter
151+
// to create a new AdWidget instance if the underlying ad object changes.
152+
// This prevents the "AdWidget is already in the Widget tree" error.
153+
child: admob.AdWidget(
154+
key: ValueKey(_ad!.hashCode),
155+
ad: _ad! as admob.AdWithView,
156+
),
150157
);
151158
}
152159
}

lib/ads/widgets/feed_ad_loader_widget.dart

Lines changed: 49 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -62,8 +62,7 @@ class _FeedAdLoaderWidgetState extends State<FeedAdLoaderWidget> {
6262
bool _hasError = false;
6363
final Logger _logger = Logger('FeedAdLoaderWidget');
6464
late final InlineAdCacheService _adCacheService;
65-
late final AdService
66-
_adService; // AdService will be accessed via _adCacheService
65+
late final AdService _adService;
6766

6867
/// Completer to manage the lifecycle of the ad loading future.
6968
/// This helps in cancelling pending operations if the widget is disposed
@@ -106,11 +105,13 @@ class _FeedAdLoaderWidgetState extends State<FeedAdLoaderWidget> {
106105
// Immediately set the widget to a loading state to prevent UI flicker.
107106
// This ensures a smooth transition from the old ad (or no ad) to the
108107
// loading indicator for the new ad.
109-
setState(() {
110-
_loadedAd = null;
111-
_isLoading = true;
112-
_hasError = false;
113-
});
108+
if (mounted) {
109+
setState(() {
110+
_loadedAd = null;
111+
_isLoading = true;
112+
_hasError = false;
113+
});
114+
}
114115
_loadAd();
115116
}
116117
}
@@ -136,15 +137,24 @@ class _FeedAdLoaderWidgetState extends State<FeedAdLoaderWidget> {
136137
/// If found, it uses the cached ad. Otherwise, it requests a new inline ad
137138
/// from the [AdService] using `getFeedAd` and stores it in the cache
138139
/// upon success.
140+
///
141+
/// It also includes defensive checks (`mounted`) to prevent `setState` calls
142+
/// on disposed widgets and ensures the `_loadAdCompleter` is always completed
143+
/// to prevent `StateError`s.
139144
Future<void> _loadAd() async {
140145
// Initialize a new completer for this loading operation.
141146
_loadAdCompleter = Completer<void>();
142147

143-
// Ensure the widget is still mounted before calling setState.
148+
// Ensure the widget is still mounted before proceeding.
144149
// This prevents the "setState() called after dispose()" error
145150
// if the widget is removed from the tree while the async operation
146151
// is still in progress.
147-
if (!mounted) return;
152+
if (!mounted) {
153+
if (_loadAdCompleter?.isCompleted == false) {
154+
_loadAdCompleter!.complete();
155+
}
156+
return;
157+
}
148158

149159
// Attempt to retrieve the ad from the cache first.
150160
final cachedAd = _adCacheService.getAd(widget.adPlaceholder.id);
@@ -154,11 +164,12 @@ class _FeedAdLoaderWidgetState extends State<FeedAdLoaderWidget> {
154164
'Using cached ad for feed placeholder ID: ${widget.adPlaceholder.id}',
155165
);
156166
// Ensure the widget is still mounted before calling setState.
157-
if (!mounted) return;
158-
setState(() {
159-
_loadedAd = cachedAd;
160-
_isLoading = false;
161-
});
167+
if (mounted) {
168+
setState(() {
169+
_loadedAd = cachedAd;
170+
_isLoading = false;
171+
});
172+
}
162173
// Complete the completer only if it hasn't been completed already.
163174
if (_loadAdCompleter?.isCompleted == false) {
164175
_loadAdCompleter!.complete();
@@ -178,11 +189,12 @@ class _FeedAdLoaderWidgetState extends State<FeedAdLoaderWidget> {
178189
'Ad placeholder ID ${widget.adPlaceholder.id} has no adIdentifier. '
179190
'Cannot load ad.',
180191
);
181-
if (!mounted) return;
182-
setState(() {
183-
_hasError = true;
184-
_isLoading = false;
185-
});
192+
if (mounted) {
193+
setState(() {
194+
_hasError = true;
195+
_isLoading = false;
196+
});
197+
}
186198
// Complete the completer normally, indicating that loading finished
187199
// but no ad was available. This prevents crashes.
188200
if (_loadAdCompleter?.isCompleted == false) {
@@ -214,11 +226,12 @@ class _FeedAdLoaderWidgetState extends State<FeedAdLoaderWidget> {
214226
// Store the newly loaded ad in the cache.
215227
_adCacheService.setAd(widget.adPlaceholder.id, loadedAd);
216228
// Ensure the widget is still mounted before calling setState.
217-
if (!mounted) return;
218-
setState(() {
219-
_loadedAd = loadedAd;
220-
_isLoading = false;
221-
});
229+
if (mounted) {
230+
setState(() {
231+
_loadedAd = loadedAd;
232+
_isLoading = false;
233+
});
234+
}
222235
// Complete the completer only if it hasn't been completed already.
223236
if (_loadAdCompleter?.isCompleted == false) {
224237
_loadAdCompleter!.complete();
@@ -229,11 +242,12 @@ class _FeedAdLoaderWidgetState extends State<FeedAdLoaderWidget> {
229242
'No ad returned.',
230243
);
231244
// Ensure the widget is still mounted before calling setState.
232-
if (!mounted) return;
233-
setState(() {
234-
_hasError = true;
235-
_isLoading = false;
236-
});
245+
if (mounted) {
246+
setState(() {
247+
_hasError = true;
248+
_isLoading = false;
249+
});
250+
}
237251
// Complete the completer normally, indicating that loading finished
238252
// but no ad was available. This prevents crashes.
239253
if (_loadAdCompleter?.isCompleted == false) {
@@ -247,11 +261,12 @@ class _FeedAdLoaderWidgetState extends State<FeedAdLoaderWidget> {
247261
s,
248262
);
249263
// Ensure the widget is still mounted before calling setState.
250-
if (!mounted) return;
251-
setState(() {
252-
_hasError = true;
253-
_isLoading = false;
254-
});
264+
if (mounted) {
265+
setState(() {
266+
_hasError = true;
267+
_isLoading = false;
268+
});
269+
}
255270
// Complete the completer normally, indicating that loading finished
256271
// but an error occurred. This prevents crashes.
257272
if (_loadAdCompleter?.isCompleted == false) {

lib/headlines-feed/view/headlines_feed_page.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -342,6 +342,7 @@ class _HeadlinesFeedPageState extends State<HeadlinesFeedPage> {
342342
}
343343

344344
return FeedAdLoaderWidget(
345+
key: ValueKey(item.id), // Add a unique key for AdPlaceholder
345346
adPlaceholder: item,
346347
adThemeStyle: AdThemeStyle.fromTheme(Theme.of(context)),
347348
adConfig: adConfig,

lib/shared/services/feed_decorator_service.dart

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -392,8 +392,8 @@ class FeedDecoratorService {
392392
/// This method ensures that ad placeholders for *inline* ads (native and banner)
393393
/// are placed according to the `adPlacementInterval` (initial buffer before
394394
/// the first ad) and `adFrequency` (subsequent ad spacing). It correctly
395-
/// accounts for content items only, ignoring previously injected decorators
396-
/// when calculating placement. Interstitial ads are not handled here.
395+
/// accounts for content items and decorators, ignoring previously injected
396+
/// ad placeholders when calculating placement.
397397
///
398398
/// [feedItems]: The list of feed items (headlines, other decorators)
399399
/// to inject ad placeholders into.
@@ -452,8 +452,8 @@ class FeedDecoratorService {
452452

453453
final result = <FeedItem>[];
454454
// This counter tracks the absolute number of *content items* (headlines,
455-
// topics, sources, countries) processed so far, including those from
456-
// previous pages. This is key for accurate ad placement.
455+
// topics, sources, countries, and decorators) processed so far, including
456+
// those from previous pages. This is key for accurate ad placement.
457457
var currentContentItemCount = processedContentItemCount;
458458

459459
// Get the primary ad platform and its identifiers
@@ -475,12 +475,9 @@ class FeedDecoratorService {
475475
result.add(item);
476476

477477
// Only increment the content item counter if the current item is
478-
// a primary content type (not an ad or a decorator).
479-
// This ensures ad placement is based purely on content density.
480-
if (item is Headline ||
481-
item is Topic ||
482-
item is Source ||
483-
item is Country) {
478+
// a primary content type or a decorator (not an ad placeholder).
479+
// This ensures ad placement is based purely on content/decorator density.
480+
if (item is! AdPlaceholder) {
484481
currentContentItemCount++;
485482
}
486483

0 commit comments

Comments
 (0)