1
+ import 'dart:async' ;
2
+
1
3
import 'package:core/core.dart' ;
2
4
import 'package:flutter/material.dart' ;
3
5
import 'package:flutter_news_app_mobile_client_full_source_code/ads/ad_cache_service.dart' ;
@@ -55,34 +57,97 @@ class _AdLoaderWidgetState extends State<AdLoaderWidget> {
55
57
final Logger _logger = Logger ('AdLoaderWidget' );
56
58
final AdCacheService _adCacheService = AdCacheService ();
57
59
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
+
58
66
@override
59
67
void initState () {
60
68
super .initState ();
61
69
_loadAd ();
62
70
}
63
71
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
+
64
119
/// Loads the native ad for this slot.
65
120
///
66
121
/// This method first checks the [AdCacheService] for a pre-loaded ad.
67
122
/// If found, it uses the cached ad. Otherwise, it requests a new ad
68
123
/// from the [AdService] and stores it in the cache upon success.
69
124
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 >();
74
127
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 ;
75
133
// Attempt to retrieve the ad from the cache first.
76
134
final cachedAd = _adCacheService.getAd (widget.adPlaceholder.id);
77
135
78
136
if (cachedAd != null ) {
79
137
_logger.info (
80
138
'Using cached ad for placeholder ID: ${widget .adPlaceholder .id }' ,
81
139
);
140
+ // Ensure the widget is still mounted before calling setState.
141
+ if (! mounted) return ;
82
142
setState (() {
83
143
_loadedAd = cachedAd;
84
144
_isLoading = false ;
85
145
});
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
+ }
86
151
return ;
87
152
}
88
153
@@ -105,29 +170,49 @@ class _AdLoaderWidgetState extends State<AdLoaderWidget> {
105
170
);
106
171
// Store the newly loaded ad in the cache.
107
172
_adCacheService.setAd (widget.adPlaceholder.id, adFeedItem.nativeAd);
173
+ // Ensure the widget is still mounted before calling setState.
174
+ if (! mounted) return ;
108
175
setState (() {
109
176
_loadedAd = adFeedItem.nativeAd;
110
177
_isLoading = false ;
111
178
});
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
+ }
112
183
} else {
113
184
_logger.warning (
114
185
'Failed to load ad for placeholder ID: ${widget .adPlaceholder .id }. No ad returned.' ,
115
186
);
187
+ // Ensure the widget is still mounted before calling setState.
188
+ if (! mounted) return ;
116
189
setState (() {
117
190
_hasError = true ;
118
191
_isLoading = false ;
119
192
});
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
+ }
120
199
}
121
200
} catch (e, s) {
122
201
_logger.severe (
123
202
'Error loading ad for placeholder ID: ${widget .adPlaceholder .id }: $e ' ,
124
203
e,
125
204
s,
126
205
);
206
+ // Ensure the widget is still mounted before calling setState.
207
+ if (! mounted) return ;
127
208
setState (() {
128
209
_hasError = true ;
129
210
_isLoading = false ;
130
211
});
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
+ }
131
216
}
132
217
}
133
218
0 commit comments