@@ -4,7 +4,6 @@ import 'package:core/core.dart';
44import 'package:flutter/material.dart' ;
55import 'package:flutter_bloc/flutter_bloc.dart' ;
66import 'package:flutter_news_app_mobile_client_full_source_code/ads/ad_service.dart' ;
7- import 'package:flutter_news_app_mobile_client_full_source_code/ads/inline_ad_cache_service.dart' ;
87import 'package:flutter_news_app_mobile_client_full_source_code/ads/models/ad_theme_style.dart' ;
98import 'package:flutter_news_app_mobile_client_full_source_code/ads/models/banner_ad.dart' ;
109import 'package:flutter_news_app_mobile_client_full_source_code/ads/models/inline_ad.dart' ;
@@ -19,33 +18,29 @@ import 'package:logging/logging.dart';
1918import 'package:ui_kit/ui_kit.dart' ;
2019
2120/// {@template in_article_ad_loader_widget}
22- /// A self-contained widget responsible for loading and displaying an in-article
23- /// ad .
21+ /// A self-contained, stateful widget that manages the entire lifecycle
22+ /// of a single in-article ad slot .
2423///
25- /// This widget handles the entire lifecycle of a single in-article ad slot.
26- /// It attempts to retrieve a cached [InlineAd] first. If no ad is cached,
27- /// it requests a new one from the [AdService] using `getInArticleAd` and stores
28- /// it in the cache upon success. It manages its own loading and error states .
24+ /// This widget is designed to be robust and efficient. It fetches an
25+ /// [InlineAd] from the [AdService] in its `initState` method and
26+ /// stores it in its own local state. Crucially, it is responsible
27+ /// for disposing of the loaded ad's resources in its `dispose` method .
2928///
30- /// This approach decouples ad loading from the BLoC and ensures that native
31- /// ad resources are managed efficiently, preventing crashes and improving
32- /// scrolling performance in lists.
29+ /// This approach ensures that the ad's lifecycle is tightly coupled
30+ /// with the widget's lifecycle, preventing ad cache collisions when
31+ /// multiple instances of the same article page are in the navigation
32+ /// stack. It also ensures proper resource cleanup when the widget
33+ /// is removed from the widget tree.
3334/// {@endtemplate}
3435class InArticleAdLoaderWidget extends StatefulWidget {
3536 /// {@macro in_article_ad_loader_widget}
3637 const InArticleAdLoaderWidget ({
37- required this .contextKey,
3838 required this .slotType,
3939 required this .adThemeStyle,
4040 required this .adConfig,
4141 super .key,
4242 });
4343
44- /// A unique key representing the ad context this ad belongs to, typically the
45- /// article ID. This is used to scope the ad within the
46- /// [InlineAdCacheService] .
47- final String contextKey;
48-
4944 /// The type of the in-article ad slot.
5045 final InArticleAdSlotType slotType;
5146
@@ -61,68 +56,88 @@ class InArticleAdLoaderWidget extends StatefulWidget {
6156}
6257
6358class _InArticleAdLoaderWidgetState extends State <InArticleAdLoaderWidget > {
59+ /// The currently loaded inline ad object.
60+ /// This is managed entirely by this widget's state.
6461 InlineAd ? _loadedAd;
6562 bool _isLoading = true ;
6663 bool _hasError = false ;
6764 final Logger _logger = Logger ('InArticleAdLoaderWidget' );
68- late final InlineAdCacheService _adCacheService;
6965 late final AdService _adService;
7066
7167 Completer <void >? _loadAdCompleter;
7268
7369 @override
7470 void initState () {
7571 super .initState ();
76- _adCacheService = context. read < InlineAdCacheService >();
72+ // AdService is used to fetch new ads and dispose of them.
7773 _adService = context.read <AdService >();
7874 _loadAd ();
7975 }
8076
8177 @override
8278 void didUpdateWidget (covariant InArticleAdLoaderWidget oldWidget) {
8379 super .didUpdateWidget (oldWidget);
84- // If the slotType or contextKey changes, it means this widget is being
85- // reused for a different ad slot or context. We need to cancel any ongoing
86- // load for the old ad and initiate a new load for the new ad.
80+ // If the slotType or adConfig changes, it means this widget is
81+ // being reused for a different ad slot or its configuration has
82+ // been updated. We need to cancel any ongoing load for the old ad
83+ // and initiate a new load for the new ad.
8784 // Also, if the adConfig changes, we should re-evaluate and potentially
8885 // reload.
89- if (widget.contextKey != oldWidget.contextKey ||
90- widget.slotType != oldWidget.slotType ||
86+ if (widget.slotType != oldWidget.slotType ||
9187 widget.adConfig != oldWidget.adConfig) {
9288 _logger.info (
9389 'InArticleAdLoaderWidget updated for new slot type: '
94- '${widget .slotType .name } or contextKey: ${widget .contextKey }. '
95- 'Re-loading ad.' ,
90+ '${widget .slotType .name } or adConfig changed. Re-loading ad.' ,
9691 );
9792
98- // Dispose of the old ad's resources before loading a new one to prevent
99- // memory leaks if the widget is reused for a different context.
100- _adCacheService.removeAndDisposeAd (
101- contextKey: oldWidget.contextKey,
102- placeholderId: oldWidget.slotType.name,
103- );
93+ // Cancel the previous loading operation if it's still active and not yet
94+ // completed. This prevents a race condition if a new load is triggered.
10495 if (_loadAdCompleter != null && ! _loadAdCompleter! .isCompleted) {
10596 // Complete normally to prevent crashes
10697 _loadAdCompleter! .complete ();
10798 }
10899 _loadAdCompleter = null ;
109100
110- setState (() {
111- _loadedAd = null ;
112- _isLoading = true ;
113- _hasError = false ;
114- });
101+ // If an ad was previously loaded, dispose of its resources
102+ // immediately as this widget is now responsible for its lifecycle.
103+ if (_loadedAd != null ) {
104+ _logger.info (
105+ 'Disposing old ad for slot "${oldWidget .slotType .name }" '
106+ 'before loading new one.' ,
107+ );
108+ _adService.disposeAd (_loadedAd);
109+ }
110+
111+ // Immediately set the widget to a loading state to prevent UI flicker.
112+ // This ensures a smooth transition from the old ad (or no ad) to the
113+ // loading indicator for the new ad.
114+ if (mounted) {
115+ setState (() {
116+ _loadedAd = null ;
117+ _isLoading = true ;
118+ _hasError = false ;
119+ });
120+ }
115121 _loadAd ();
116122 }
117123 }
118124
119125 @override
120126 void dispose () {
121- // The ad object (_loadedAd) is intentionally not disposed of here.
122- // Its lifecycle is managed by the InlineAdCacheService. The ad remains in
123- // the cache even after navigating away from the article, allowing for
124- // instant reuse if the user returns. The cache is managed by time-based
125- // staleness checks and is cleared globally on user logout.
127+ // The ad object (_loadedAd) is managed by this widget.
128+ // Therefore, its resources MUST be explicitly disposed of here
129+ // when the widget is removed from the widget tree to prevent
130+ // memory leaks.
131+ if (_loadedAd != null ) {
132+ _logger.info (
133+ 'Disposing in-article ad for slot "${widget .slotType .name }" '
134+ 'as widget is being disposed.' ,
135+ );
136+ _adService.disposeAd (_loadedAd);
137+ }
138+
139+ // Cancel any pending ad loading operation when the widget is disposed.
140+ // This prevents `setState()` calls on a disposed widget.
126141 if (_loadAdCompleter != null && ! _loadAdCompleter! .isCompleted) {
127142 // Complete normally to prevent crashes
128143 _loadAdCompleter! .complete ();
@@ -133,63 +148,28 @@ class _InArticleAdLoaderWidgetState extends State<InArticleAdLoaderWidget> {
133148
134149 /// Loads the in-article ad for this slot.
135150 ///
136- /// This method first checks the [InlineAdCacheService] for a pre-loaded
137- /// [InlineAd] . If found, it uses the cached ad. Otherwise, it requests a new
138- /// in-article ad from the [AdService] using `getInArticleAd` and stores it
139- /// in the cache upon success.
151+ /// This method directly requests a new in-article ad from the
152+ /// [AdService] using `getInArticleAd` . It stores the loaded ad in
153+ /// its local state (`_loadedAd` ). This widget does not use an
154+ /// external cache for its ads; its lifecycle is entirely self-managed.
155+ ///
156+ /// It also includes defensive checks (`mounted` ) to prevent `setState` calls
157+ /// on disposed widgets and ensures the `_loadAdCompleter` is always
158+ /// completed to prevent `StateError` s.
140159 Future <void > _loadAd () async {
160+ // Initialize a new completer for this loading operation.
141161 _loadAdCompleter = Completer <void >();
142162
163+ // Ensure the widget is still mounted before proceeding.
164+ // This prevents the "setState() called after dispose()" error.
143165 if (! mounted) {
144166 if (_loadAdCompleter? .isCompleted == false ) {
145167 _loadAdCompleter! .complete ();
146168 }
147169 return ;
148170 }
149171
150- // Use the slotType as the placeholderId within the context of the article.
151- final placeholderId = widget.slotType.name;
152-
153- // TODO(fulleni): This should be sourced from RemoteConfig.
154- const adMaxAge = Duration (minutes: 5 );
155-
156- var cachedAd = _adCacheService.getAd (
157- contextKey: widget.contextKey,
158- placeholderId: placeholderId,
159- );
160-
161- if (cachedAd != null ) {
162- if (DateTime .now ().difference (cachedAd.createdAt) > adMaxAge) {
163- _logger.info (
164- 'Cached in-article ad for context "${widget .contextKey }" and slot '
165- '${widget .slotType .name } is stale. Discarding and fetching new.' ,
166- );
167- _adCacheService.removeAndDisposeAd (
168- contextKey: widget.contextKey,
169- placeholderId: placeholderId,
170- );
171- cachedAd = null ;
172- } else {
173- _logger.info (
174- 'Using fresh cached in-article ad for context "${widget .contextKey }" '
175- 'and slot: ${widget .slotType .name }' ,
176- );
177- if (mounted) {
178- setState (() {
179- _loadedAd = cachedAd;
180- _isLoading = false ;
181- });
182- }
183- if (_loadAdCompleter? .isCompleted == false )
184- _loadAdCompleter! .complete ();
185- return ;
186- }
187- }
188-
189- _logger.info (
190- 'Loading new in-article ad for context "${widget .contextKey }" '
191- 'and slot: ${widget .slotType .name }' ,
192- );
172+ _logger.info ('Loading new in-article ad for slot: ${widget .slotType .name }' );
193173 try {
194174 // Get the current user role from AppBloc
195175 final appBlocState = context.read <AppBloc >().state;
@@ -205,42 +185,35 @@ class _InArticleAdLoaderWidgetState extends State<InArticleAdLoaderWidget> {
205185
206186 if (loadedAd != null ) {
207187 _logger.info (
208- 'New in-article ad loaded for context "${widget .contextKey }" '
209- 'and slot: ${widget .slotType .name }' ,
188+ 'New in-article ad loaded for slot: ${widget .slotType .name }' ,
210189 );
211- // Cache the newly loaded ad.
212- _adCacheService.setAd (
213- contextKey: widget.contextKey,
214- placeholderId: placeholderId,
215- ad: loadedAd,
216- );
217-
218190 if (mounted) {
219191 setState (() {
220192 _loadedAd = loadedAd;
221193 _isLoading = false ;
222194 });
223195 }
224- if (_loadAdCompleter? .isCompleted == false )
196+ if (_loadAdCompleter? .isCompleted == false ) {
225197 _loadAdCompleter! .complete ();
198+ }
226199 } else {
227200 _logger.warning (
228- 'Failed to load in-article ad for context " ${widget .contextKey }" '
229- 'and slot: ${ widget . slotType . name }. No ad returned.' ,
201+ 'Failed to load in-article ad for slot: ${widget .slotType . name }. '
202+ 'No ad returned.' ,
230203 );
231204 if (mounted) {
232205 setState (() {
233206 _hasError = true ;
234207 _isLoading = false ;
235208 });
236209 }
237- if (_loadAdCompleter? .isCompleted == false )
210+ if (_loadAdCompleter? .isCompleted == false ) {
238211 _loadAdCompleter! .complete ();
212+ }
239213 }
240214 } catch (e, s) {
241215 _logger.severe (
242- 'Error loading in-article ad for context "${widget .contextKey }" '
243- 'and slot: ${widget .slotType .name }: $e ' ,
216+ 'Error loading in-article ad for slot: ${widget .slotType .name }: $e ' ,
244217 e,
245218 s,
246219 );
@@ -250,7 +223,9 @@ class _InArticleAdLoaderWidgetState extends State<InArticleAdLoaderWidget> {
250223 _isLoading = false ;
251224 });
252225 }
253- if (_loadAdCompleter? .isCompleted == false ) _loadAdCompleter! .complete ();
226+ if (_loadAdCompleter? .isCompleted == false ) {
227+ _loadAdCompleter! .complete ();
228+ }
254229 }
255230 }
256231
0 commit comments