Skip to content

Commit 485b677

Browse files
committed
feat(ads): implement in-article ad loader widget
- Add InArticleAdLoaderWidget to handle the entire lifecycle of a single in-article ad slot - Implement caching mechanism for in-article ads using AdCacheService - Decouple ad loading from BLoC to improve native ad resource management - Add loading and error state handling within the widget - Ensure efficient scrolling performance in lists with self-contained ad loading and displaying
1 parent 2ceb028 commit 485b677

File tree

1 file changed

+221
-0
lines changed

1 file changed

+221
-0
lines changed
Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,221 @@
1+
import 'dart:async';
2+
3+
import 'package:core/core.dart';
4+
import 'package:flutter/material.dart';
5+
import 'package:flutter_news_app_mobile_client_full_source_code/ads/ad_cache_service.dart';
6+
import '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/models/ad_theme_style.dart';
8+
import 'package:flutter_news_app_mobile_client_full_source_code/ads/models/ad_feed_item.dart';
9+
import 'package:flutter_news_app_mobile_client_full_source_code/ads/models/inline_ad.dart';
10+
import 'package:flutter_news_app_mobile_client_full_source_code/ads/widgets/ad_feed_item_widget.dart';
11+
import 'package:flutter_news_app_mobile_client_full_source_code/ads/widgets/placeholder_ad_widget.dart';
12+
import 'package:logging/logging.dart';
13+
import 'package:ui_kit/ui_kit.dart';
14+
15+
/// {@template in_article_ad_loader_widget}
16+
/// A self-contained widget responsible for loading and displaying an in-article ad.
17+
///
18+
/// This widget handles the entire lifecycle of a single in-article ad slot.
19+
/// It attempts to retrieve a cached [InlineAd] first. If no ad is cached,
20+
/// it requests a new one from the [AdService] using `getInArticleAd` and stores
21+
/// it in the cache upon success. It manages its own loading and error states.
22+
///
23+
/// This approach decouples ad loading from the BLoC and ensures that native
24+
/// ad resources are managed efficiently, preventing crashes and improving
25+
/// scrolling performance in lists.
26+
/// {@endtemplate}
27+
class InArticleAdLoaderWidget extends StatefulWidget {
28+
/// {@macro in_article_ad_loader_widget}
29+
const InArticleAdLoaderWidget({
30+
required this.slotConfiguration,
31+
required this.adService,
32+
required this.adThemeStyle,
33+
required this.adConfig,
34+
super.key,
35+
});
36+
37+
/// The configuration for this specific in-article ad slot.
38+
final InArticleAdSlotConfiguration slotConfiguration;
39+
40+
/// The service responsible for loading ads from ad networks.
41+
final AdService adService;
42+
43+
/// The current theme style for ads, used during ad loading.
44+
final AdThemeStyle adThemeStyle;
45+
46+
/// The full remote configuration for ads, used to determine ad loading rules.
47+
final AdConfig adConfig;
48+
49+
@override
50+
State<InArticleAdLoaderWidget> createState() =>
51+
_InArticleAdLoaderWidgetState();
52+
}
53+
54+
class _InArticleAdLoaderWidgetState extends State<InArticleAdLoaderWidget> {
55+
InlineAd? _loadedAd;
56+
bool _isLoading = true;
57+
bool _hasError = false;
58+
final Logger _logger = Logger('InArticleAdLoaderWidget');
59+
final AdCacheService _adCacheService = AdCacheService();
60+
61+
Completer<void>? _loadAdCompleter;
62+
63+
@override
64+
void initState() {
65+
super.initState();
66+
_loadAd();
67+
}
68+
69+
@override
70+
void didUpdateWidget(covariant InArticleAdLoaderWidget oldWidget) {
71+
super.didUpdateWidget(oldWidget);
72+
// If the slotConfiguration changes, it means this widget is being reused
73+
// for a different ad slot. We need to cancel any ongoing load for the old
74+
// ad and initiate a new load for the new ad.
75+
// Also, if the adConfig changes, we should re-evaluate and potentially reload.
76+
if (widget.slotConfiguration != oldWidget.slotConfiguration ||
77+
widget.adConfig != oldWidget.adConfig) {
78+
_logger.info(
79+
'InArticleAdLoaderWidget updated for new slot configuration or AdConfig changed. Re-loading ad.',
80+
);
81+
if (_loadAdCompleter != null && !_loadAdCompleter!.isCompleted) {
82+
_loadAdCompleter?.completeError(
83+
StateError('Ad loading cancelled: Widget updated with new config.'),
84+
);
85+
}
86+
_loadAdCompleter = null;
87+
88+
setState(() {
89+
_loadedAd = null;
90+
_isLoading = true;
91+
_hasError = false;
92+
});
93+
_loadAd();
94+
}
95+
}
96+
97+
@override
98+
void dispose() {
99+
if (_loadAdCompleter != null && !_loadAdCompleter!.isCompleted) {
100+
_loadAdCompleter?.completeError(
101+
StateError('Ad loading cancelled: Widget disposed.'),
102+
);
103+
}
104+
_loadAdCompleter = null;
105+
super.dispose();
106+
}
107+
108+
/// Loads the in-article ad for this slot.
109+
///
110+
/// This method first checks the [AdCacheService] for a pre-loaded [InlineAd].
111+
/// If found, it uses the cached ad. Otherwise, it requests a new in-article ad
112+
/// from the [AdService] using `getInArticleAd` and stores it in the cache
113+
/// upon success.
114+
Future<void> _loadAd() async {
115+
_loadAdCompleter = Completer<void>();
116+
117+
if (!mounted) return;
118+
119+
// In-article ads are typically unique to their slot, so we use the slotType
120+
// as part of the cache key to differentiate them.
121+
final cacheKey = 'in_article_ad_${widget.slotConfiguration.slotType.name}';
122+
final cachedAd = _adCacheService.getAd(cacheKey);
123+
124+
if (cachedAd != null) {
125+
_logger.info(
126+
'Using cached in-article ad for slot: ${widget.slotConfiguration.slotType.name}',
127+
);
128+
if (!mounted) return;
129+
setState(() {
130+
_loadedAd = cachedAd;
131+
_isLoading = false;
132+
});
133+
if (_loadAdCompleter?.isCompleted == false) {
134+
_loadAdCompleter!.complete();
135+
}
136+
return;
137+
}
138+
139+
_logger.info(
140+
'Loading new in-article ad for slot: ${widget.slotConfiguration.slotType.name}',
141+
);
142+
try {
143+
// Call AdService.getInArticleAd with the full AdConfig.
144+
final loadedAd = await widget.adService.getInArticleAd(
145+
adConfig: widget.adConfig,
146+
adThemeStyle: widget.adThemeStyle,
147+
);
148+
149+
if (loadedAd != null) {
150+
_logger.info(
151+
'New in-article ad loaded for slot: ${widget.slotConfiguration.slotType.name}',
152+
);
153+
_adCacheService.setAd(cacheKey, loadedAd);
154+
if (!mounted) return;
155+
setState(() {
156+
_loadedAd = loadedAd;
157+
_isLoading = false;
158+
});
159+
if (_loadAdCompleter?.isCompleted == false) {
160+
_loadAdCompleter!.complete();
161+
}
162+
} else {
163+
_logger.warning(
164+
'Failed to load in-article ad for slot: ${widget.slotConfiguration.slotType.name}. '
165+
'No ad returned.',
166+
);
167+
if (!mounted) return;
168+
setState(() {
169+
_hasError = true;
170+
_isLoading = false;
171+
});
172+
if (_loadAdCompleter?.isCompleted == false) {
173+
_loadAdCompleter?.completeError(
174+
StateError('Failed to load in-article ad: No ad returned.'),
175+
);
176+
}
177+
}
178+
} catch (e, s) {
179+
_logger.severe(
180+
'Error loading in-article ad for slot: ${widget.slotConfiguration.slotType.name}: $e',
181+
e,
182+
s,
183+
);
184+
if (!mounted) return;
185+
setState(() {
186+
_hasError = true;
187+
_isLoading = false;
188+
});
189+
if (_loadAdCompleter?.isCompleted == false) {
190+
_loadAdCompleter?.completeError(e);
191+
}
192+
}
193+
}
194+
195+
@override
196+
Widget build(BuildContext context) {
197+
if (_isLoading) {
198+
return const Padding(
199+
padding: EdgeInsets.symmetric(
200+
horizontal: AppSpacing.paddingMedium,
201+
vertical: AppSpacing.xs,
202+
),
203+
child: AspectRatio(
204+
aspectRatio: 16 / 9,
205+
child: Card(
206+
child: Center(child: CircularProgressIndicator(strokeWidth: 2)),
207+
),
208+
),
209+
);
210+
} else if (_hasError || _loadedAd == null) {
211+
return const PlaceholderAdWidget();
212+
} else {
213+
return AdFeedItemWidget(
214+
adFeedItem: AdFeedItem(
215+
id: widget.slotConfiguration.slotType.name,
216+
inlineAd: _loadedAd!,
217+
),
218+
);
219+
}
220+
}
221+
}

0 commit comments

Comments
 (0)