Skip to content

Commit e40d23d

Browse files
committed
feat(ads): implement interstitial ad manager
- Add InterstitialAdManager class to handle interstitial ad lifecycle - Implement logic to preload and show interstitial ads based on user role and app state - Integrate with AdService and AppBloc for ad configuration and user state management - Support for AdMob, local, and demo ad types - Logging for ad manager actions and events
1 parent 38e5f4f commit e40d23d

File tree

1 file changed

+243
-0
lines changed

1 file changed

+243
-0
lines changed
Lines changed: 243 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,243 @@
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_service.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/interstitial_ad.dart';
8+
import 'package:flutter_news_app_mobile_client_full_source_code/ads/widgets/widgets.dart';
9+
import 'package:flutter_news_app_mobile_client_full_source_code/app/bloc/app_bloc.dart';
10+
import 'package:google_mobile_ads/google_mobile_ads.dart' as admob;
11+
import 'package:logging/logging.dart';
12+
13+
/// {@template interstitial_ad_manager}
14+
/// A service that manages the lifecycle of interstitial ads.
15+
///
16+
/// This manager listens to the [AppBloc] to stay aware of the current
17+
/// [RemoteConfig] and user state. It proactively pre-loads an interstitial
18+
/// ad when conditions are met and provides a mechanism to show it upon
19+
/// an explicit trigger from the UI.
20+
/// {@endtemplate}
21+
class InterstitialAdManager {
22+
/// {@macro interstitial_ad_manager}
23+
InterstitialAdManager({
24+
required AppBloc appBloc,
25+
required AdService adService,
26+
Logger? logger,
27+
}) : _appBloc = appBloc,
28+
_adService = adService,
29+
_logger = logger ?? Logger('InterstitialAdManager') {
30+
// Listen to the AppBloc stream to react to state changes.
31+
_appBlocSubscription = _appBloc.stream.listen(_onAppStateChanged);
32+
// Initialize with the current state.
33+
_onAppStateChanged(_appBloc.state);
34+
}
35+
36+
final AppBloc _appBloc;
37+
final AdService _adService;
38+
final Logger _logger;
39+
40+
late final StreamSubscription<AppState> _appBlocSubscription;
41+
42+
/// The currently pre-loaded interstitial ad.
43+
InterstitialAd? _preloadedAd;
44+
45+
/// Tracks the number of eligible page transitions since the last ad was shown.
46+
int _transitionCount = 0;
47+
48+
/// The current remote configuration for ads.
49+
AdConfig? _adConfig;
50+
51+
/// The current user role.
52+
AppUserRole? _userRole;
53+
54+
/// Disposes the manager and cancels stream subscriptions.
55+
void dispose() {
56+
_appBlocSubscription.cancel();
57+
_disposePreloadedAd();
58+
}
59+
60+
/// Handles changes in the [AppState].
61+
void _onAppStateChanged(AppState state) {
62+
final newAdConfig = state.remoteConfig?.adConfig;
63+
final newUserRole = state.user?.appRole;
64+
65+
// If the ad config or user role has changed, update internal state
66+
// and potentially pre-load a new ad.
67+
if (newAdConfig != _adConfig || newUserRole != _userRole) {
68+
_logger.info(
69+
'Ad config or user role changed. Updating internal state.',
70+
);
71+
_adConfig = newAdConfig;
72+
_userRole = newUserRole;
73+
// A config change might mean we need to load an ad now.
74+
_maybePreloadAd();
75+
}
76+
}
77+
78+
/// Pre-loads an interstitial ad if one is not already loaded and conditions are met.
79+
Future<void> _maybePreloadAd() async {
80+
if (_preloadedAd != null) {
81+
_logger.info('An interstitial ad is already pre-loaded. Skipping.');
82+
return;
83+
}
84+
85+
final adConfig = _adConfig;
86+
if (adConfig == null ||
87+
!adConfig.enabled ||
88+
!adConfig.interstitialAdConfiguration.enabled) {
89+
_logger.info('Interstitial ads are disabled. Skipping pre-load.');
90+
return;
91+
}
92+
93+
_logger.info('Attempting to pre-load an interstitial ad...');
94+
try {
95+
// We need a BuildContext to get the theme for AdThemeStyle.
96+
// Since this is a service, we get it from the AppBloc's navigatorKey.
97+
final context = _appBloc.navigatorKey.currentContext;
98+
if (context == null) {
99+
_logger.warning(
100+
'BuildContext not available from navigatorKey. Cannot create AdThemeStyle.',
101+
);
102+
return;
103+
}
104+
final adThemeStyle = AdThemeStyle.fromTheme(Theme.of(context));
105+
106+
final ad = await _adService.getInterstitialAd(
107+
adConfig: adConfig,
108+
adThemeStyle: adThemeStyle,
109+
);
110+
111+
if (ad != null) {
112+
_preloadedAd = ad;
113+
_logger.info('Interstitial ad pre-loaded successfully.');
114+
} else {
115+
_logger.warning('Failed to pre-load interstitial ad.');
116+
}
117+
} catch (e, s) {
118+
_logger.severe('Error pre-loading interstitial ad: $e', e, s);
119+
}
120+
}
121+
122+
/// Disposes the currently pre-loaded ad to release its resources.
123+
void _disposePreloadedAd() {
124+
if (_preloadedAd?.provider == AdPlatformType.admob &&
125+
_preloadedAd?.adObject is admob.InterstitialAd) {
126+
_logger.info('Disposing pre-loaded AdMob interstitial ad.');
127+
(_preloadedAd!.adObject as admob.InterstitialAd).dispose();
128+
}
129+
_preloadedAd = null;
130+
}
131+
132+
/// Called by the UI before an ad-eligible navigation occurs.
133+
///
134+
/// This method increments the transition counter and shows a pre-loaded ad
135+
/// if the frequency criteria are met.
136+
Future<void> onPotentialAdTrigger({required BuildContext context}) async {
137+
_transitionCount++;
138+
_logger.info('Potential ad trigger. Transition count: $_transitionCount');
139+
140+
final adConfig = _adConfig;
141+
if (adConfig == null) {
142+
_logger.warning('No ad config available. Cannot determine ad frequency.');
143+
return;
144+
}
145+
146+
final frequencyConfig =
147+
adConfig.interstitialAdConfiguration.feedInterstitialAdFrequencyConfig;
148+
final requiredTransitions = _getRequiredTransitions(frequencyConfig);
149+
150+
if (requiredTransitions > 0 && _transitionCount >= requiredTransitions) {
151+
_logger.info('Transition count meets threshold. Attempting to show ad.');
152+
await _showAd(context);
153+
_transitionCount = 0; // Reset counter after showing (or attempting to show)
154+
} else {
155+
_logger.info(
156+
'Transition count ($_transitionCount) has not met threshold ($requiredTransitions).',
157+
);
158+
}
159+
}
160+
161+
/// Shows the pre-loaded interstitial ad.
162+
Future<void> _showAd(BuildContext context) async {
163+
if (_preloadedAd == null) {
164+
_logger.warning('Show ad called, but no ad is pre-loaded. Pre-loading now.');
165+
// Attempt a last-minute load if no ad is ready.
166+
await _maybePreloadAd();
167+
if (_preloadedAd == null) {
168+
_logger.severe('Last-minute ad load failed. Cannot show ad.');
169+
return;
170+
}
171+
}
172+
173+
final adToShow = _preloadedAd!;
174+
_preloadedAd = null; // Clear the pre-loaded ad before showing
175+
176+
try {
177+
switch (adToShow.provider) {
178+
case AdPlatformType.admob:
179+
await _showAdMobAd(adToShow);
180+
case AdPlatformType.local:
181+
await _showLocalAd(context, adToShow);
182+
case AdPlatformType.demo:
183+
await _showDemoAd(context);
184+
}
185+
} catch (e, s) {
186+
_logger.severe('Error showing interstitial ad: $e', e, s);
187+
} finally {
188+
// After the ad is shown or fails to show, dispose of it and
189+
// start pre-loading the next one for the next opportunity.
190+
_disposePreloadedAd(); // Ensure the ad object is disposed
191+
_maybePreloadAd();
192+
}
193+
}
194+
195+
Future<void> _showAdMobAd(InterstitialAd ad) async {
196+
if (ad.adObject is! admob.InterstitialAd) return;
197+
final admobAd = ad.adObject as admob.InterstitialAd
198+
199+
..fullScreenContentCallback = admob.FullScreenContentCallback(
200+
onAdShowedFullScreenContent: (ad) =>
201+
_logger.info('AdMob ad showed full screen.'),
202+
onAdDismissedFullScreenContent: (ad) {
203+
_logger.info('AdMob ad dismissed.');
204+
ad.dispose();
205+
},
206+
onAdFailedToShowFullScreenContent: (ad, error) {
207+
_logger.severe('AdMob ad failed to show: $error');
208+
ad.dispose();
209+
},
210+
);
211+
await admobAd.show();
212+
}
213+
214+
Future<void> _showLocalAd(BuildContext context, InterstitialAd ad) async {
215+
if (ad.adObject is! LocalInterstitialAd) return;
216+
await showDialog<void>(
217+
context: context,
218+
builder: (_) =>
219+
LocalInterstitialAdDialog(localInterstitialAd: ad.adObject as LocalInterstitialAd),
220+
);
221+
}
222+
223+
Future<void> _showDemoAd(BuildContext context) async {
224+
await showDialog<void>(
225+
context: context,
226+
builder: (_) => const DemoInterstitialAdDialog(),
227+
);
228+
}
229+
230+
/// Determines the required number of transitions based on the user's role.
231+
int _getRequiredTransitions(InterstitialAdFrequencyConfig config) {
232+
switch (_userRole) {
233+
case AppUserRole.guestUser:
234+
return config.guestTransitionsBeforeShowingInterstitialAds;
235+
case AppUserRole.standardUser:
236+
return config.standardUserTransitionsBeforeShowingInterstitialAds;
237+
case AppUserRole.premiumUser:
238+
return config.premiumUserTransitionsBeforeShowingInterstitialAds;
239+
case null:
240+
return config.guestTransitionsBeforeShowingInterstitialAds;
241+
}
242+
}
243+
}

0 commit comments

Comments
 (0)