Skip to content

Commit a2f5ac1

Browse files
committed
refactor(ads): improve AdNavigatorObserver testability and modularity
- Replace direct AppBloc dependency with AppStateProvider function - Implement page transition counting logic internally - Enhance interstitial ad showing logic with detailed frequency checks - Update method names and access modifiers for clarity
1 parent 2773151 commit a2f5ac1

File tree

1 file changed

+99
-35
lines changed

1 file changed

+99
-35
lines changed

lib/ads/ad_navigator_observer.dart

Lines changed: 99 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,15 @@ import 'package:flutter_news_app_mobile_client_full_source_code/ads/ad_service.d
66
import 'package:flutter_news_app_mobile_client_full_source_code/ads/models/ad_theme_style.dart';
77
import 'package:flutter_news_app_mobile_client_full_source_code/ads/models/interstitial_ad.dart';
88
import 'package:flutter_news_app_mobile_client_full_source_code/app/bloc/app_bloc.dart';
9-
import 'package:logging/logging.dart';
109
import 'package:google_mobile_ads/google_mobile_ads.dart' as admob;
10+
import 'package:logging/logging.dart';
11+
12+
/// A function that provides the current [AppState].
13+
///
14+
/// This is used for dependency injection to decouple the [AdNavigatorObserver]
15+
/// from a direct dependency on the [AppBloc] instance, making it more
16+
/// testable and reusable.
17+
typedef AppStateProvider = AppState Function();
1118

1219
/// {@template ad_navigator_observer}
1320
/// A [NavigatorObserver] that listens to route changes and triggers
@@ -18,54 +25,112 @@ import 'package:google_mobile_ads/google_mobile_ads.dart' as admob;
1825
/// 2. Requesting an interstitial ad from the [AdService] when the criteria are met.
1926
/// 3. Showing the interstitial ad to the user.
2027
///
21-
/// It interacts with the [AppBloc] to get the current [RemoteConfig] and
22-
/// user's ad frequency settings, and to dispatch events for page transitions.
28+
/// It retrieves the current [AppState] via the [appStateProvider] to get the
29+
/// latest [RemoteConfig] and user's ad frequency settings.
2330
/// {@endtemplate}
2431
class AdNavigatorObserver extends NavigatorObserver {
2532
/// {@macro ad_navigator_observer}
2633
AdNavigatorObserver({
27-
required this.appBloc,
34+
required this.appStateProvider,
2835
required this.adService,
2936
required AdThemeStyle adThemeStyle,
3037
Logger? logger,
31-
}) : _logger = logger ?? Logger('AdNavigatorObserver'),
32-
_adThemeStyle = adThemeStyle;
38+
}) : _logger = logger ?? Logger('AdNavigatorObserver'),
39+
_adThemeStyle = adThemeStyle;
40+
41+
/// A function that provides the current [AppState].
42+
final AppStateProvider appStateProvider;
3343

34-
final AppBloc appBloc;
44+
/// The service responsible for fetching and loading ads.
3545
final AdService adService;
46+
3647
final Logger _logger;
3748
final AdThemeStyle _adThemeStyle;
3849

50+
/// Tracks the number of page transitions since the last interstitial ad.
51+
int _pageTransitionCount = 0;
52+
3953
@override
4054
void didPush(Route<dynamic> route, Route<dynamic>? previousRoute) {
4155
super.didPush(route, previousRoute);
4256
_logger.info('Route pushed: ${route.settings.name}');
43-
_handlePageTransition(route);
57+
if (route is PageRoute && route.settings.name != null) {
58+
_handlePageTransition();
59+
}
4460
}
4561

4662
@override
4763
void didPop(Route<dynamic> route, Route<dynamic>? previousRoute) {
4864
super.didPop(route, previousRoute);
4965
_logger.info('Route popped: ${route.settings.name}');
50-
_handlePageTransition(route);
66+
if (route is PageRoute && route.settings.name != null) {
67+
_handlePageTransition();
68+
}
5169
}
5270

53-
/// Handles a page transition event.
54-
///
55-
/// Dispatches an [AppPageTransitioned] event to the [AppBloc] to update
56-
/// the transition count and potentially trigger an interstitial ad.
57-
void _handlePageTransition(Route<dynamic> route) {
58-
if (route is PageRoute && route.settings.name != null) {
59-
appBloc.add(const AppPageTransitioned());
71+
/// Handles a page transition event, checks ad frequency, and shows an ad if needed.
72+
void _handlePageTransition() {
73+
_pageTransitionCount++;
74+
_logger.info('Page transitioned. Current count: $_pageTransitionCount');
75+
76+
final appState = appStateProvider();
77+
final remoteConfig = appState.remoteConfig;
78+
final user = appState.user;
79+
80+
// Only proceed if remote config is available, ads are globally enabled,
81+
// and interstitial ads are enabled in the config.
82+
if (remoteConfig == null ||
83+
!remoteConfig.adConfig.enabled ||
84+
!remoteConfig.adConfig.interstitialAdConfiguration.enabled) {
85+
_logger.info('Interstitial ads are not enabled or config not ready.');
86+
return;
87+
}
88+
89+
final interstitialConfig =
90+
remoteConfig.adConfig.interstitialAdConfiguration;
91+
final frequencyConfig =
92+
interstitialConfig.feedInterstitialAdFrequencyConfig;
93+
94+
// Determine the required transitions based on user role.
95+
final int requiredTransitions;
96+
switch (user?.appRole) {
97+
case AppUserRole.guestUser:
98+
requiredTransitions =
99+
frequencyConfig.guestTransitionsBeforeShowingInterstitialAds;
100+
break;
101+
case AppUserRole.standardUser:
102+
requiredTransitions =
103+
frequencyConfig.standardUserTransitionsBeforeShowingInterstitialAds;
104+
break;
105+
case AppUserRole.premiumUser:
106+
requiredTransitions =
107+
frequencyConfig.premiumUserTransitionsBeforeShowingInterstitialAds;
108+
break;
109+
case null:
110+
// If user is null, default to guest user settings.
111+
requiredTransitions =
112+
frequencyConfig.guestTransitionsBeforeShowingInterstitialAds;
113+
break;
114+
}
115+
116+
_logger.info(
117+
'Required transitions for user role ${user?.appRole}: $requiredTransitions',
118+
);
119+
120+
// Check if it's time to show an interstitial ad.
121+
if (requiredTransitions > 0 &&
122+
_pageTransitionCount >= requiredTransitions) {
123+
_logger.info('Interstitial ad due. Requesting ad.');
124+
_showInterstitialAd();
125+
_pageTransitionCount = 0; // Reset count after showing
60126
}
61127
}
62128

63129
/// Requests and shows an interstitial ad if conditions are met.
64-
///
65-
/// This method is called by the [AppBloc] when it determines an ad is due.
66-
Future<void> showInterstitialAd() async {
67-
final remoteConfig = appBloc.state.remoteConfig;
130+
Future<void> _showInterstitialAd() async {
131+
final remoteConfig = appStateProvider().remoteConfig;
68132

133+
// This is a secondary check. The primary check is in _handlePageTransition.
69134
if (remoteConfig == null || !remoteConfig.adConfig.enabled) {
70135
_logger.info('Interstitial ads disabled or remote config not available.');
71136
return;
@@ -91,21 +156,20 @@ class AdNavigatorObserver extends NavigatorObserver {
91156
if (interstitialAd.provider == AdPlatformType.admob &&
92157
interstitialAd.adObject is admob.InterstitialAd) {
93158
final admobInterstitialAd =
94-
interstitialAd.adObject as admob.InterstitialAd;
95-
admobInterstitialAd.fullScreenContentCallback =
96-
admob.FullScreenContentCallback(
97-
onAdDismissedFullScreenContent: (ad) {
98-
_logger.info('Interstitial Ad dismissed.');
99-
ad.dispose();
100-
},
101-
onAdFailedToShowFullScreenContent: (ad, error) {
102-
_logger.severe('Interstitial Ad failed to show: $error');
103-
ad.dispose();
104-
},
105-
onAdShowedFullScreenContent: (ad) {
106-
_logger.info('Interstitial Ad showed.');
107-
},
108-
);
159+
interstitialAd.adObject as admob.InterstitialAd
160+
..fullScreenContentCallback = admob.FullScreenContentCallback(
161+
onAdDismissedFullScreenContent: (ad) {
162+
_logger.info('Interstitial Ad dismissed.');
163+
ad.dispose();
164+
},
165+
onAdFailedToShowFullScreenContent: (ad, error) {
166+
_logger.severe('Interstitial Ad failed to show: $error');
167+
ad.dispose();
168+
},
169+
onAdShowedFullScreenContent: (ad) {
170+
_logger.info('Interstitial Ad showed.');
171+
},
172+
);
109173
await admobInterstitialAd.show();
110174
} else if (interstitialAd.provider == AdPlatformType.local &&
111175
interstitialAd.adObject is LocalInterstitialAd) {

0 commit comments

Comments
 (0)