Skip to content

Commit 43ad1db

Browse files
committed
fix(ads): prevent ad loader crashes and improve state management
- Add defensive checks for 'setState' calls on disposed widgets - Ensure '_loadAdCompleter' is always completed to prevent 'StateError's - Refactor 'mounted' checks to avoid repetition and improve readability - Remove unnecessary comments and simplify code structure
1 parent 3c7d984 commit 43ad1db

File tree

1 file changed

+49
-34
lines changed

1 file changed

+49
-34
lines changed

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) {

0 commit comments

Comments
 (0)