Skip to content

Commit ac0424c

Browse files
authored
Merge pull request #109 from flutter-news-app-full-source-code/sync-the-client-with-the-new-remote-ad-config-structure
Sync the client with the new remote ad config structure
2 parents 3716d7a + b1e46e5 commit ac0424c

39 files changed

+2411
-851
lines changed

lib/account/bloc/account_bloc.dart

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -38,11 +38,11 @@ class AccountBloc extends Bloc<AccountEvent, AccountState> {
3838
.entityUpdated
3939
.where((type) => type == UserContentPreferences)
4040
.listen((_) {
41-
// If there's a current user, reload their preferences.
42-
if (state.user?.id != null) {
43-
add(AccountLoadUserPreferences(userId: state.user!.id));
44-
}
45-
});
41+
// If there's a current user, reload their preferences.
42+
if (state.user?.id != null) {
43+
add(AccountLoadUserPreferences(userId: state.user!.id));
44+
}
45+
});
4646

4747
// Register event handlers
4848
on<AccountUserChanged>(_onAccountUserChanged);

lib/ads/ad_cache_service.dart

Lines changed: 0 additions & 95 deletions
This file was deleted.

lib/ads/ad_navigator_observer.dart

Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
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/widgets/widgets.dart';
8+
import 'package:flutter_news_app_mobile_client_full_source_code/app/bloc/app_bloc.dart';
9+
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();
18+
19+
/// {@template ad_navigator_observer}
20+
/// A [NavigatorObserver] that listens to route changes and triggers
21+
/// interstitial ad display based on [RemoteConfig] settings.
22+
///
23+
/// This observer is responsible for:
24+
/// 1. Tracking page transitions to determine when an interstitial ad should be shown.
25+
/// 2. Requesting an interstitial ad from the [AdService] when the criteria are met.
26+
/// 3. Showing the interstitial ad to the user.
27+
///
28+
/// It retrieves the current [AppState] via the [appStateProvider] to get the
29+
/// latest [RemoteConfig] and user's ad frequency settings.
30+
/// {@endtemplate}
31+
class AdNavigatorObserver extends NavigatorObserver {
32+
/// {@macro ad_navigator_observer}
33+
AdNavigatorObserver({
34+
required this.appStateProvider,
35+
required this.adService,
36+
required AdThemeStyle adThemeStyle,
37+
Logger? logger,
38+
}) : _logger = logger ?? Logger('AdNavigatorObserver'),
39+
_adThemeStyle = adThemeStyle;
40+
41+
/// A function that provides the current [AppState].
42+
final AppStateProvider appStateProvider;
43+
44+
/// The service responsible for fetching and loading ads.
45+
final AdService adService;
46+
47+
final Logger _logger;
48+
final AdThemeStyle _adThemeStyle;
49+
50+
/// Tracks the number of page transitions since the last interstitial ad.
51+
int _pageTransitionCount = 0;
52+
53+
@override
54+
void didPush(Route<dynamic> route, Route<dynamic>? previousRoute) {
55+
super.didPush(route, previousRoute);
56+
_logger.info('Route pushed: ${route.settings.name}');
57+
if (route is PageRoute && route.settings.name != null) {
58+
_handlePageTransition();
59+
}
60+
}
61+
62+
@override
63+
void didPop(Route<dynamic> route, Route<dynamic>? previousRoute) {
64+
super.didPop(route, previousRoute);
65+
_logger.info('Route popped: ${route.settings.name}');
66+
if (route is PageRoute && route.settings.name != null) {
67+
_handlePageTransition();
68+
}
69+
}
70+
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+
case AppUserRole.standardUser:
101+
requiredTransitions =
102+
frequencyConfig.standardUserTransitionsBeforeShowingInterstitialAds;
103+
case AppUserRole.premiumUser:
104+
requiredTransitions =
105+
frequencyConfig.premiumUserTransitionsBeforeShowingInterstitialAds;
106+
case null:
107+
// If user is null, default to guest user settings.
108+
requiredTransitions =
109+
frequencyConfig.guestTransitionsBeforeShowingInterstitialAds;
110+
}
111+
112+
_logger.info(
113+
'Required transitions for user role ${user?.appRole}: $requiredTransitions',
114+
);
115+
116+
// Check if it's time to show an interstitial ad.
117+
if (requiredTransitions > 0 &&
118+
_pageTransitionCount >= requiredTransitions) {
119+
_logger.info('Interstitial ad due. Requesting ad.');
120+
_showInterstitialAd();
121+
_pageTransitionCount = 0; // Reset count after showing
122+
}
123+
}
124+
125+
/// Requests and shows an interstitial ad if conditions are met.
126+
Future<void> _showInterstitialAd() async {
127+
final remoteConfig = appStateProvider().remoteConfig;
128+
129+
// This is a secondary check. The primary check is in _handlePageTransition.
130+
if (remoteConfig == null || !remoteConfig.adConfig.enabled) {
131+
_logger.info('Interstitial ads disabled or remote config not available.');
132+
return;
133+
}
134+
135+
final adConfig = remoteConfig.adConfig;
136+
final interstitialConfig = adConfig.interstitialAdConfiguration;
137+
138+
if (!interstitialConfig.enabled) {
139+
_logger.info('Interstitial ads are specifically disabled in config.');
140+
return;
141+
}
142+
143+
_logger.info('Attempting to load interstitial ad...');
144+
final interstitialAd = await adService.getInterstitialAd(
145+
adConfig: adConfig,
146+
adThemeStyle: _adThemeStyle,
147+
);
148+
149+
if (interstitialAd != null) {
150+
_logger.info('Interstitial ad loaded. Showing...');
151+
// Show the AdMob interstitial ad.
152+
if (interstitialAd.provider == AdPlatformType.admob &&
153+
interstitialAd.adObject is admob.InterstitialAd) {
154+
final admobInterstitialAd =
155+
interstitialAd.adObject as admob.InterstitialAd
156+
..fullScreenContentCallback = admob.FullScreenContentCallback(
157+
onAdDismissedFullScreenContent: (ad) {
158+
_logger.info('Interstitial Ad dismissed.');
159+
ad.dispose();
160+
},
161+
onAdFailedToShowFullScreenContent: (ad, error) {
162+
_logger.severe('Interstitial Ad failed to show: $error');
163+
ad.dispose();
164+
},
165+
onAdShowedFullScreenContent: (ad) {
166+
_logger.info('Interstitial Ad showed.');
167+
},
168+
);
169+
await admobInterstitialAd.show();
170+
} else if (interstitialAd.provider == AdPlatformType.local &&
171+
interstitialAd.adObject is LocalInterstitialAd) {
172+
_logger.info('Showing local interstitial ad.');
173+
await showDialog<void>(
174+
context: navigator!.context,
175+
builder: (context) => LocalInterstitialAdDialog(
176+
localInterstitialAd: interstitialAd.adObject as LocalInterstitialAd,
177+
),
178+
);
179+
}
180+
} else {
181+
_logger.info('No interstitial ad loaded.');
182+
}
183+
}
184+
}

lib/ads/ad_provider.dart

Lines changed: 47 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import 'package:core/core.dart';
22
import 'package:flutter_news_app_mobile_client_full_source_code/ads/models/ad_theme_style.dart';
3-
import 'package:flutter_news_app_mobile_client_full_source_code/ads/models/native_ad.dart'
4-
as app_native_ad;
3+
import 'package:flutter_news_app_mobile_client_full_source_code/ads/models/banner_ad.dart';
4+
import 'package:flutter_news_app_mobile_client_full_source_code/ads/models/interstitial_ad.dart';
5+
import 'package:flutter_news_app_mobile_client_full_source_code/ads/models/native_ad.dart';
56

67
/// {@template ad_provider}
78
/// An abstract class defining the interface for any ad network provider.
@@ -20,19 +21,54 @@ abstract class AdProvider {
2021
/// It handles any necessary setup for the specific ad network.
2122
Future<void> initialize();
2223

23-
/// Loads a native ad, optionally tailoring it to a specific style.
24+
/// Loads an inline native ad, optionally tailoring it to a specific style.
2425
///
25-
/// Returns a [app_native_ad.NativeAd] object if an ad is successfully loaded,
26-
/// otherwise returns `null`.
26+
/// Returns a [NativeAd] object if an ad is successfully loaded,
27+
/// otherwise returns `null`. This method is intended for native ads
28+
/// that are displayed directly within content feeds.
2729
///
28-
/// The [imageStyle] is used to select an appropriate native ad template
29-
/// that best matches the visual density of the surrounding content.
30+
/// The [adPlatformIdentifiers] provides the platform-specific ad unit IDs.
31+
/// The [adId] is the specific identifier for the ad slot (e.g., native ad unit ID).
3032
/// The [adThemeStyle] provides UI-agnostic theme properties for ad styling.
31-
Future<app_native_ad.NativeAd?> loadNativeAd({
32-
required HeadlineImageStyle imageStyle,
33+
/// The [headlineImageStyle] provides the user's preference for feed layout,
34+
/// which can be used to request an appropriately sized ad.
35+
Future<NativeAd?> loadNativeAd({
36+
required AdPlatformIdentifiers adPlatformIdentifiers,
37+
required String? adId,
3338
required AdThemeStyle adThemeStyle,
39+
HeadlineImageStyle? headlineImageStyle,
3440
});
3541

36-
// Future methods for other ad types (e.g., interstitial, banner)
37-
// can be added here as needed in the future.
42+
/// Loads an inline banner ad.
43+
///
44+
/// Returns a [BannerAd] object if an ad is successfully loaded,
45+
/// otherwise returns `null`. This method is intended for banner ads
46+
/// that are displayed directly within content feeds.
47+
///
48+
/// The [adPlatformIdentifiers] provides the platform-specific ad unit IDs.
49+
/// The [adId] is the specific identifier for the ad slot (e.g., banner ad unit ID).
50+
/// The [adThemeStyle] provides UI-agnostic theme properties for ad styling.
51+
/// The [headlineImageStyle] provides the user's preference for feed layout,
52+
/// which can be used to request an appropriately sized ad.
53+
Future<BannerAd?> loadBannerAd({
54+
required AdPlatformIdentifiers adPlatformIdentifiers,
55+
required String? adId,
56+
required AdThemeStyle adThemeStyle,
57+
HeadlineImageStyle? headlineImageStyle,
58+
});
59+
60+
/// Loads a full-screen interstitial ad.
61+
///
62+
/// Returns an [InterstitialAd] object if an ad is successfully loaded,
63+
/// otherwise returns `null`. This method is intended for interstitial ads
64+
/// that are displayed as full-screen overlays, typically on route changes.
65+
///
66+
/// The [adPlatformIdentifiers] provides the platform-specific ad unit IDs.
67+
/// The [adId] is the specific identifier for the ad slot (e.g., interstitial ad unit ID).
68+
/// The [adThemeStyle] provides UI-agnostic theme properties for ad styling.
69+
Future<InterstitialAd?> loadInterstitialAd({
70+
required AdPlatformIdentifiers adPlatformIdentifiers,
71+
required String? adId,
72+
required AdThemeStyle adThemeStyle,
73+
});
3874
}

0 commit comments

Comments
 (0)