Skip to content

Commit 3716d7a

Browse files
authored
Merge pull request #105 from flutter-news-app-full-source-code/enhance-ad-feature
Enhance ad feature
2 parents 521d862 + 4ac3936 commit 3716d7a

File tree

1 file changed

+89
-4
lines changed

1 file changed

+89
-4
lines changed

lib/ads/widgets/ad_loader_widget.dart

Lines changed: 89 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import 'dart:async';
2+
13
import 'package:core/core.dart';
24
import 'package:flutter/material.dart';
35
import 'package:flutter_news_app_mobile_client_full_source_code/ads/ad_cache_service.dart';
@@ -55,34 +57,97 @@ class _AdLoaderWidgetState extends State<AdLoaderWidget> {
5557
final Logger _logger = Logger('AdLoaderWidget');
5658
final AdCacheService _adCacheService = AdCacheService();
5759

60+
// Completer to manage the lifecycle of the ad loading future.
61+
// This helps in cancelling pending operations if the widget is disposed
62+
// or updated, preventing `setState` calls on an unmounted widget
63+
// and avoiding `StateError` from completing a completer multiple times.
64+
Completer<void>? _loadAdCompleter;
65+
5866
@override
5967
void initState() {
6068
super.initState();
6169
_loadAd();
6270
}
6371

72+
@override
73+
void didUpdateWidget(covariant AdLoaderWidget oldWidget) {
74+
super.didUpdateWidget(oldWidget);
75+
// If the adPlaceholder ID changes, it means this widget is being reused
76+
// for a different ad slot. We need to cancel any ongoing load for the old
77+
// ad and initiate a new load for the new ad.
78+
if (widget.adPlaceholder.id != oldWidget.adPlaceholder.id) {
79+
_logger.info(
80+
'AdLoaderWidget updated for new placeholder ID: '
81+
'${widget.adPlaceholder.id}. Re-loading ad.',
82+
);
83+
// Cancel the previous loading operation if it's still active and not yet
84+
// completed. This prevents a race condition if a new load is triggered
85+
// while an old one is still in progress.
86+
if (_loadAdCompleter != null && !_loadAdCompleter!.isCompleted) {
87+
_loadAdCompleter?.completeError(
88+
StateError('Ad loading cancelled: Widget updated with new ID.'),
89+
);
90+
}
91+
_loadAdCompleter = null; // Clear the old completer for the new load
92+
93+
// Immediately set the widget to a loading state to prevent UI flicker.
94+
// This ensures a smooth transition from the old ad (or no ad) to the
95+
// loading indicator for the new ad.
96+
setState(() {
97+
_loadedAd = null;
98+
_isLoading = true;
99+
_hasError = false;
100+
});
101+
_loadAd(); // Start loading the new ad
102+
}
103+
}
104+
105+
@override
106+
void dispose() {
107+
// Cancel any pending ad loading operation when the widget is disposed.
108+
// This prevents `setState()` calls on a disposed widget.
109+
// Ensure the completer is not already completed before attempting to complete it.
110+
if (_loadAdCompleter != null && !_loadAdCompleter!.isCompleted) {
111+
_loadAdCompleter?.completeError(
112+
StateError('Ad loading cancelled: Widget disposed.'),
113+
);
114+
}
115+
_loadAdCompleter = null;
116+
super.dispose();
117+
}
118+
64119
/// Loads the native ad for this slot.
65120
///
66121
/// This method first checks the [AdCacheService] for a pre-loaded ad.
67122
/// If found, it uses the cached ad. Otherwise, it requests a new ad
68123
/// from the [AdService] and stores it in the cache upon success.
69124
Future<void> _loadAd() async {
70-
setState(() {
71-
_isLoading = true;
72-
_hasError = false;
73-
});
125+
// Initialize a new completer for this loading operation.
126+
_loadAdCompleter = Completer<void>();
74127

128+
// Ensure the widget is still mounted before calling setState.
129+
// This prevents the "setState() called after dispose()" error
130+
// if the widget is removed from the tree while the async operation
131+
// is still in progress.
132+
if (!mounted) return;
75133
// Attempt to retrieve the ad from the cache first.
76134
final cachedAd = _adCacheService.getAd(widget.adPlaceholder.id);
77135

78136
if (cachedAd != null) {
79137
_logger.info(
80138
'Using cached ad for placeholder ID: ${widget.adPlaceholder.id}',
81139
);
140+
// Ensure the widget is still mounted before calling setState.
141+
if (!mounted) return;
82142
setState(() {
83143
_loadedAd = cachedAd;
84144
_isLoading = false;
85145
});
146+
// Complete the completer only if it hasn't been completed already
147+
// (e.g., by dispose() or didUpdateWidget() cancelling an old load).
148+
if (_loadAdCompleter?.isCompleted == false) {
149+
_loadAdCompleter!.complete(); // Complete the completer on success
150+
}
86151
return;
87152
}
88153

@@ -105,29 +170,49 @@ class _AdLoaderWidgetState extends State<AdLoaderWidget> {
105170
);
106171
// Store the newly loaded ad in the cache.
107172
_adCacheService.setAd(widget.adPlaceholder.id, adFeedItem.nativeAd);
173+
// Ensure the widget is still mounted before calling setState.
174+
if (!mounted) return;
108175
setState(() {
109176
_loadedAd = adFeedItem.nativeAd;
110177
_isLoading = false;
111178
});
179+
// Complete the completer only if it hasn't been completed already.
180+
if (_loadAdCompleter?.isCompleted == false) {
181+
_loadAdCompleter!.complete(); // Complete the completer on success
182+
}
112183
} else {
113184
_logger.warning(
114185
'Failed to load ad for placeholder ID: ${widget.adPlaceholder.id}. No ad returned.',
115186
);
187+
// Ensure the widget is still mounted before calling setState.
188+
if (!mounted) return;
116189
setState(() {
117190
_hasError = true;
118191
_isLoading = false;
119192
});
193+
// Complete the completer with an error only if it hasn't been completed already.
194+
if (_loadAdCompleter?.isCompleted == false) {
195+
_loadAdCompleter?.completeError(
196+
StateError('Failed to load ad: No ad returned.'),
197+
); // Complete with error
198+
}
120199
}
121200
} catch (e, s) {
122201
_logger.severe(
123202
'Error loading ad for placeholder ID: ${widget.adPlaceholder.id}: $e',
124203
e,
125204
s,
126205
);
206+
// Ensure the widget is still mounted before calling setState.
207+
if (!mounted) return;
127208
setState(() {
128209
_hasError = true;
129210
_isLoading = false;
130211
});
212+
// Complete the completer with an error only if it hasn't been completed already.
213+
if (_loadAdCompleter?.isCompleted == false) {
214+
_loadAdCompleter?.completeError(e); // Complete with error
215+
}
131216
}
132217
}
133218

0 commit comments

Comments
 (0)