Skip to content

Commit b9b0593

Browse files
authored
Merge pull request #112 from flutter-news-app-full-source-code/sync-ad-feature-with-core-package-ad-config-interface-update
Sync ad feature with core package ad config interface update
2 parents 1501cf1 + 9076d81 commit b9b0593

13 files changed

+280
-134
lines changed

lib/ads/ad_navigator_observer.dart

Lines changed: 90 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -51,29 +51,78 @@ class AdNavigatorObserver extends NavigatorObserver {
5151
/// Tracks the number of page transitions since the last interstitial ad.
5252
int _pageTransitionCount = 0;
5353

54+
/// Stores the name of the previous route.
55+
String? _previousRouteName;
56+
5457
@override
5558
void didPush(Route<dynamic> route, Route<dynamic>? previousRoute) {
5659
super.didPush(route, previousRoute);
57-
_logger.info('Route pushed: ${route.settings.name}');
58-
if (route is PageRoute && route.settings.name != null) {
59-
_handlePageTransition();
60+
final currentRouteName = route.settings.name;
61+
_logger.info(
62+
'Route pushed: $currentRouteName (Previous: $_previousRouteName)',
63+
);
64+
if (route is PageRoute && currentRouteName != null) {
65+
_handlePageTransition(currentRouteName);
6066
}
67+
_previousRouteName = currentRouteName;
6168
}
6269

6370
@override
6471
void didPop(Route<dynamic> route, Route<dynamic>? previousRoute) {
6572
super.didPop(route, previousRoute);
66-
_logger.info('Route popped: ${route.settings.name}');
67-
if (route is PageRoute && route.settings.name != null) {
68-
_handlePageTransition();
73+
final currentRouteName = previousRoute
74+
?.settings
75+
.name; // After pop, previousRoute is the new current
76+
_logger.info(
77+
'Route popped: ${route.settings.name} (New Current: $currentRouteName)',
78+
);
79+
if (route is PageRoute && currentRouteName != null) {
80+
_handlePageTransition(currentRouteName);
6981
}
82+
_previousRouteName = currentRouteName;
7083
}
7184

72-
/// Handles a page transition event, checks ad frequency, and shows an ad if needed.
73-
void _handlePageTransition() {
74-
_pageTransitionCount++;
75-
_logger.info('Page transitioned. Current count: $_pageTransitionCount');
85+
/// Determines if a route transition is eligible for an interstitial ad.
86+
///
87+
/// An ad is considered eligible if the transition is from a content list
88+
/// (e.g., feed, search) to a detail page (e.g., article, entity details).
89+
bool _isEligibleForInterstitialAd(String currentRouteName) {
90+
// Define content list routes
91+
const contentListRoutes = {
92+
'feed',
93+
'search',
94+
'followedTopicsList',
95+
'followedSourcesList',
96+
'followedCountriesList',
97+
'accountSavedHeadlines',
98+
};
99+
100+
// Define detail page routes
101+
const detailPageRoutes = {
102+
'articleDetails',
103+
'searchArticleDetails',
104+
'accountArticleDetails',
105+
'globalArticleDetails',
106+
'entityDetails',
107+
};
108+
109+
final previous = _previousRouteName;
110+
final current = currentRouteName;
111+
112+
final isFromContentList =
113+
previous != null && contentListRoutes.contains(previous);
114+
final isToDetailPage = detailPageRoutes.contains(current);
115+
116+
_logger.info(
117+
'Eligibility check: Previous: $previous (Is Content List: $isFromContentList), '
118+
'Current: $current (Is Detail Page: $isToDetailPage)',
119+
);
76120

121+
return isFromContentList && isToDetailPage;
122+
}
123+
124+
/// Handles a page transition event, checks ad frequency, and shows an ad if needed.
125+
void _handlePageTransition(String currentRouteName) {
77126
final appState = appStateProvider();
78127
final remoteConfig = appState.remoteConfig;
79128
final user = appState.user;
@@ -87,10 +136,23 @@ class AdNavigatorObserver extends NavigatorObserver {
87136
return;
88137
}
89138

139+
// Only increment count if the transition is eligible for an interstitial ad.
140+
if (_isEligibleForInterstitialAd(currentRouteName)) {
141+
_pageTransitionCount++;
142+
_logger.info(
143+
'Eligible page transition. Current count: $_pageTransitionCount',
144+
);
145+
} else {
146+
_logger.info(
147+
'Ineligible page transition. Count remains: $_pageTransitionCount',
148+
);
149+
return; // Do not proceed if not an eligible transition
150+
}
151+
90152
final interstitialConfig =
91153
remoteConfig.adConfig.interstitialAdConfiguration;
92-
final frequencyConfig =
93-
interstitialConfig.feedInterstitialAdFrequencyConfig;
154+
final frequencyConfig = interstitialConfig
155+
.feedInterstitialAdFrequencyConfig; // Using existing name
94156

95157
// Determine the required transitions based on user role.
96158
final int requiredTransitions;
@@ -111,14 +173,16 @@ class AdNavigatorObserver extends NavigatorObserver {
111173
}
112174

113175
_logger.info(
114-
'Required transitions for user role ${user?.appRole}: $requiredTransitions',
176+
'Required transitions for user role ${user?.appRole}: $requiredTransitions. '
177+
'Current eligible transitions: $_pageTransitionCount',
115178
);
116179

117180
// Check if it's time to show an interstitial ad.
118181
if (requiredTransitions > 0 &&
119182
_pageTransitionCount >= requiredTransitions) {
120183
_logger.info('Interstitial ad due. Requesting ad.');
121-
_showInterstitialAd();
184+
unawaited(_showInterstitialAd()); // Use unawaited to not block navigation
185+
// Reset count only after an ad is due (whether it shows or fails)
122186
_pageTransitionCount = 0;
123187
}
124188
}
@@ -142,15 +206,21 @@ class AdNavigatorObserver extends NavigatorObserver {
142206
// For other environments (development, production), proceed with real ad loading.
143207
// This is a secondary check. The primary check is in _handlePageTransition.
144208
if (remoteConfig == null || !remoteConfig.adConfig.enabled) {
145-
_logger.info('Interstitial ads disabled or remote config not available.');
209+
_logger.warning(
210+
'Interstitial ads disabled or remote config not available. '
211+
'This should have been caught earlier.',
212+
);
146213
return;
147214
}
148215

149216
final adConfig = remoteConfig.adConfig;
150217
final interstitialConfig = adConfig.interstitialAdConfiguration;
151218

152219
if (!interstitialConfig.enabled) {
153-
_logger.info('Interstitial ads are specifically disabled in config.');
220+
_logger.warning(
221+
'Interstitial ads are specifically disabled in config. '
222+
'This should have been caught earlier.',
223+
);
154224
return;
155225
}
156226

@@ -192,7 +262,10 @@ class AdNavigatorObserver extends NavigatorObserver {
192262
);
193263
}
194264
} else {
195-
_logger.info('No interstitial ad loaded.');
265+
_logger.warning(
266+
'No interstitial ad loaded by AdService, even though one was due. '
267+
'Check AdService implementation and ad unit availability.',
268+
);
196269
}
197270
}
198271
}

lib/ads/ad_service.dart

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -159,14 +159,17 @@ class AdService {
159159
Future<InlineAd?> getInArticleAd({
160160
required AdConfig adConfig,
161161
required AdThemeStyle adThemeStyle,
162+
// headlineImageStyle is not directly used for in-article ad sizing,
163+
// but kept for consistency with AdService.getFeedAd.
162164
HeadlineImageStyle? headlineImageStyle,
163165
}) async {
164166
return _loadInlineAd(
165167
adConfig: adConfig,
166-
adType: adConfig.articleAdConfiguration.defaultInArticleAdType,
168+
adType: AdType.banner, // In-article ads are now always banners
167169
adThemeStyle: adThemeStyle,
168170
feedAd: false,
169171
headlineImageStyle: headlineImageStyle,
172+
bannerAdShape: adConfig.articleAdConfiguration.bannerAdShape,
170173
);
171174
}
172175

@@ -182,6 +185,7 @@ class AdService {
182185
/// - [feedAd]: A boolean indicating if this is for a feed ad (true) or in-article ad (false).
183186
/// - [headlineImageStyle]: The user's preference for feed layout,
184187
/// which can be used to request an appropriately sized ad.
188+
/// - [bannerAdShape]: The preferred shape for banner ads, used for in-article banners.
185189
///
186190
/// Returns an [InlineAd] if an ad is successfully loaded, otherwise `null`.
187191
Future<InlineAd?> _loadInlineAd({
@@ -190,6 +194,7 @@ class AdService {
190194
required AdThemeStyle adThemeStyle,
191195
required bool feedAd,
192196
HeadlineImageStyle? headlineImageStyle,
197+
BannerAdShape? bannerAdShape,
193198
}) async {
194199
// Check if ads are globally enabled and specifically for the context (feed or article).
195200
if (!adConfig.enabled ||
@@ -249,20 +254,29 @@ class AdService {
249254
);
250255
try {
251256
InlineAd? loadedAd;
257+
// Determine the effective headlineImageStyle for the ad provider.
258+
// For in-article banner ads, bannerAdShape dictates the visual style.
259+
final effectiveHeadlineImageStyle =
260+
!feedAd && adType == AdType.banner && bannerAdShape != null
261+
? (bannerAdShape == BannerAdShape.square
262+
? HeadlineImageStyle.largeThumbnail
263+
: HeadlineImageStyle.smallThumbnail)
264+
: headlineImageStyle; // Otherwise, use the provided headlineImageStyle
265+
252266
switch (adType) {
253267
case AdType.native:
254268
loadedAd = await adProvider.loadNativeAd(
255269
adPlatformIdentifiers: platformAdIdentifiers,
256270
adId: adId,
257271
adThemeStyle: adThemeStyle,
258-
headlineImageStyle: headlineImageStyle,
272+
headlineImageStyle: effectiveHeadlineImageStyle,
259273
);
260274
case AdType.banner:
261275
loadedAd = await adProvider.loadBannerAd(
262276
adPlatformIdentifiers: platformAdIdentifiers,
263277
adId: adId,
264278
adThemeStyle: adThemeStyle,
265-
headlineImageStyle: headlineImageStyle,
279+
headlineImageStyle: effectiveHeadlineImageStyle,
266280
);
267281
case AdType.interstitial:
268282
case AdType.video:

lib/ads/admob_ad_provider.dart

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,6 @@ class AdMobAdProvider implements AdProvider {
5656
_logger.info('Attempting to load native ad from unit ID: $adId');
5757

5858
// Determine the template type based on the user's feed style preference.
59-
// Use largeThumbnail for a more prominent, square-like ad.
6059
final templateType = headlineImageStyle == HeadlineImageStyle.largeThumbnail
6160
? NativeAdTemplateType.medium
6261
: NativeAdTemplateType.small;
@@ -143,7 +142,6 @@ class AdMobAdProvider implements AdProvider {
143142
_logger.info('Attempting to load banner ad from unit ID: $adId');
144143

145144
// Determine the ad size based on the user's feed style preference.
146-
// Use mediumRectangle for a more square-like ad with large thumbnails.
147145
final adSize = headlineImageStyle == HeadlineImageStyle.largeThumbnail
148146
? admob.AdSize.mediumRectangle
149147
: admob.AdSize.banner;

lib/ads/demo_ad_provider.dart

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import 'dart:async';
2+
3+
import 'package:core/core.dart';
4+
import 'package:flutter_news_app_mobile_client_full_source_code/ads/ad_provider.dart';
5+
import 'package:flutter_news_app_mobile_client_full_source_code/ads/models/ad_theme_style.dart';
6+
import 'package:flutter_news_app_mobile_client_full_source_code/ads/models/banner_ad.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/models/native_ad.dart';
9+
import 'package:logging/logging.dart';
10+
import 'package:uuid/uuid.dart';
11+
12+
/// {@template demo_ad_provider}
13+
/// A concrete implementation of [AdProvider] for the 'demo' ad platform.
14+
///
15+
/// This provider simulates ad loading for the demo environment without
16+
/// making actual ad network calls. It returns placeholder ad objects
17+
/// for native, banner, and interstitial ads.
18+
/// {@endtemplate}
19+
class DemoAdProvider implements AdProvider {
20+
/// {@macro demo_ad_provider}
21+
DemoAdProvider({Logger? logger})
22+
: _logger = logger ?? Logger('DemoAdProvider');
23+
24+
final Logger _logger;
25+
final Uuid _uuid = const Uuid();
26+
27+
@override
28+
Future<void> initialize() async {
29+
_logger.info('Demo Ad Provider initialized (no actual SDK to init).');
30+
return Future.value();
31+
}
32+
33+
@override
34+
Future<NativeAd?> loadNativeAd({
35+
required AdPlatformIdentifiers adPlatformIdentifiers,
36+
required String? adId,
37+
required AdThemeStyle adThemeStyle,
38+
HeadlineImageStyle? headlineImageStyle,
39+
}) async {
40+
_logger.info('Simulating native ad load for demo environment.');
41+
// Simulate a delay for loading.
42+
await Future<void>.delayed(const Duration(milliseconds: 300));
43+
44+
return NativeAd(
45+
id: _uuid.v4(),
46+
provider: AdPlatformType.demo,
47+
adObject: Object(), // Placeholder object
48+
templateType: headlineImageStyle == HeadlineImageStyle.largeThumbnail
49+
? NativeAdTemplateType.medium
50+
: NativeAdTemplateType.small,
51+
);
52+
}
53+
54+
@override
55+
Future<BannerAd?> loadBannerAd({
56+
required AdPlatformIdentifiers adPlatformIdentifiers,
57+
required String? adId,
58+
required AdThemeStyle adThemeStyle,
59+
HeadlineImageStyle? headlineImageStyle,
60+
}) async {
61+
_logger.info('Simulating banner ad load for demo environment.');
62+
// Simulate a delay for loading.
63+
await Future<void>.delayed(const Duration(milliseconds: 300));
64+
65+
return BannerAd(
66+
id: _uuid.v4(),
67+
provider: AdPlatformType.demo,
68+
adObject: Object(), // Placeholder object
69+
);
70+
}
71+
72+
@override
73+
Future<InterstitialAd?> loadInterstitialAd({
74+
required AdPlatformIdentifiers adPlatformIdentifiers,
75+
required String? adId,
76+
required AdThemeStyle adThemeStyle,
77+
}) async {
78+
_logger.info('Simulating interstitial ad load for demo environment.');
79+
// Simulate a delay for loading.
80+
await Future<void>.delayed(const Duration(milliseconds: 300));
81+
82+
return InterstitialAd(
83+
id: _uuid.v4(),
84+
provider: AdPlatformType.demo,
85+
adObject: Object(), // Placeholder object
86+
);
87+
}
88+
}

lib/ads/widgets/demo_banner_ad_widget.dart

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,7 @@ class DemoBannerAdWidget extends StatelessWidget {
2121
final theme = Theme.of(context);
2222

2323
// Determine the height based on the headlineImageStyle, mimicking real ad widgets.
24-
final adHeight =
25-
headlineImageStyle == HeadlineImageStyle.largeThumbnail
24+
final adHeight = headlineImageStyle == HeadlineImageStyle.largeThumbnail
2625
? 250 // Height for mediumRectangle banner
2726
: 50; // Height for standard banner
2827

lib/ads/widgets/demo_native_ad_widget.dart

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,7 @@ class DemoNativeAdWidget extends StatelessWidget {
2222
final l10n = AppLocalizations.of(context);
2323

2424
// Determine the height based on the headlineImageStyle, mimicking real ad widgets.
25-
final adHeight =
26-
headlineImageStyle == HeadlineImageStyle.largeThumbnail
25+
final adHeight = headlineImageStyle == HeadlineImageStyle.largeThumbnail
2726
? 340 // Height for medium native ad template
2827
: 120; // Height for small native ad template
2928

0 commit comments

Comments
 (0)