Skip to content

Commit a357a07

Browse files
committed
feat(ads): implement AdLoaderWidget for native ad loading and caching
- Create self-contained AdLoaderWidget for loading and displaying native ads - Integrate AdCacheService for efficient ad caching and retrieval - Handle loading states, errors, and successful ad loads within the widget - Decouple ad loading from BLoC to improve scrolling performance in lists - Use hardcoded imageStyle for now, allowing for future configurability
1 parent 79eea98 commit a357a07

File tree

2 files changed

+148
-0
lines changed

2 files changed

+148
-0
lines changed

lib/ads/models/models.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
export 'ad_feed_item.dart';
2+
export 'ad_placeholder.dart';
23
export 'native_ad.dart';
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
import 'package:core/core.dart';
2+
import 'package:flutter/material.dart';
3+
import 'package:flutter_news_app_mobile_client_full_source_code/ads/ad_cache_service.dart';
4+
import 'package:flutter_news_app_mobile_client_full_source_code/ads/ad_service.dart';
5+
import 'package:flutter_news_app_mobile_client_full_source_code/ads/models/ad_placeholder.dart';
6+
import 'package:flutter_news_app_mobile_client_full_source_code/ads/models/ad_theme_style.dart';
7+
import 'package:flutter_news_app_mobile_client_full_source_code/ads/models/native_ad.dart';
8+
import 'package:flutter_news_app_mobile_client_full_source_code/ads/widgets/admob_native_ad_widget.dart';
9+
import 'package:flutter_news_app_mobile_client_full_source_code/ads/widgets/placeholder_ad_widget.dart';
10+
import 'package:logging/logging.dart';
11+
import 'package:ui_kit/ui_kit.dart';
12+
13+
/// {@template ad_loader_widget}
14+
/// A self-contained widget responsible for loading and displaying a native ad.
15+
///
16+
/// This widget handles the entire lifecycle of a single ad slot in the feed.
17+
/// It attempts to retrieve a cached ad first. If no ad is cached, it requests
18+
/// a new one from the [AdService]. It manages its own loading and error states.
19+
///
20+
/// This approach decouples ad loading from the BLoC and ensures that native
21+
/// ad resources are managed efficiently, preventing crashes and improving
22+
/// scrolling performance in lists.
23+
/// {@endtemplate}
24+
class AdLoaderWidget extends StatefulWidget {
25+
/// {@macro ad_loader_widget}
26+
const AdLoaderWidget({
27+
required this.adPlaceholder,
28+
required this.adService,
29+
required this.adThemeStyle,
30+
super.key,
31+
});
32+
33+
/// The stateless placeholder representing this ad slot.
34+
final AdPlaceholder adPlaceholder;
35+
36+
/// The service responsible for loading ads from ad networks.
37+
final AdService adService;
38+
39+
/// The current theme style for ads, used during ad loading.
40+
final AdThemeStyle adThemeStyle;
41+
42+
@override
43+
State<AdLoaderWidget> createState() => _AdLoaderWidgetState();
44+
}
45+
46+
class _AdLoaderWidgetState extends State<AdLoaderWidget> {
47+
NativeAd? _loadedAd;
48+
bool _isLoading = true;
49+
bool _hasError = false;
50+
final Logger _logger = Logger('AdLoaderWidget');
51+
final AdCacheService _adCacheService = AdCacheService();
52+
53+
@override
54+
void initState() {
55+
super.initState();
56+
_loadAd();
57+
}
58+
59+
/// Loads the native ad for this slot.
60+
///
61+
/// This method first checks the [AdCacheService] for a pre-loaded ad.
62+
/// If found, it uses the cached ad. Otherwise, it requests a new ad
63+
/// from the [AdService] and stores it in the cache upon success.
64+
Future<void> _loadAd() async {
65+
setState(() {
66+
_isLoading = true;
67+
_hasError = false;
68+
});
69+
70+
// Attempt to retrieve the ad from the cache first.
71+
final cachedAd = _adCacheService.getAd(widget.adPlaceholder.id);
72+
73+
if (cachedAd != null) {
74+
_logger.info('Using cached ad for placeholder ID: ${widget.adPlaceholder.id}');
75+
setState(() {
76+
_loadedAd = cachedAd;
77+
_isLoading = false;
78+
});
79+
return;
80+
}
81+
82+
_logger.info('Loading new ad for placeholder ID: ${widget.adPlaceholder.id}');
83+
try {
84+
// Request a new native ad from the AdService.
85+
// The imageStyle is hardcoded to largeThumbnail for now, but could be
86+
// made configurable based on the feed's display preferences.
87+
final adFeedItem = await widget.adService.getAd(
88+
imageStyle: HeadlineImageStyle.largeThumbnail,
89+
adThemeStyle: widget.adThemeStyle,
90+
);
91+
92+
if (adFeedItem != null) {
93+
_logger.info('New ad loaded for placeholder ID: ${widget.adPlaceholder.id}');
94+
// Store the newly loaded ad in the cache.
95+
_adCacheService.setAd(widget.adPlaceholder.id, adFeedItem.nativeAd);
96+
setState(() {
97+
_loadedAd = adFeedItem.nativeAd;
98+
_isLoading = false;
99+
});
100+
} else {
101+
_logger.warning('Failed to load ad for placeholder ID: ${widget.adPlaceholder.id}. No ad returned.');
102+
setState(() {
103+
_hasError = true;
104+
_isLoading = false;
105+
});
106+
}
107+
} catch (e, s) {
108+
_logger.severe(
109+
'Error loading ad for placeholder ID: ${widget.adPlaceholder.id}: $e',
110+
e,
111+
s,
112+
);
113+
setState(() {
114+
_hasError = true;
115+
_isLoading = false;
116+
});
117+
}
118+
}
119+
120+
@override
121+
Widget build(BuildContext context) {
122+
if (_isLoading) {
123+
// Show a shimmer or loading indicator while the ad is being loaded.
124+
return const Padding(
125+
padding: EdgeInsets.symmetric(
126+
horizontal: AppSpacing.paddingMedium,
127+
vertical: AppSpacing.xs,
128+
),
129+
child: AspectRatio(
130+
aspectRatio: 16 / 9, // Common aspect ratio for ads
131+
child: Card(
132+
child: Center(
133+
child: CircularProgressIndicator(strokeWidth: 2),
134+
),
135+
),
136+
),
137+
);
138+
} else if (_hasError || _loadedAd == null) {
139+
// Show a placeholder or error message if ad loading failed.
140+
return const PlaceholderAdWidget();
141+
} else {
142+
// If an ad is successfully loaded, display it using the appropriate widget.
143+
// The AdmobNativeAdWidget is responsible for rendering the native ad object.
144+
return AdmobNativeAdWidget(nativeAd: _loadedAd!);
145+
}
146+
}
147+
}

0 commit comments

Comments
 (0)