Skip to content

Commit 997e462

Browse files
committed
refactor(ads): isolate ad injection logic in AdService
Moves the `injectAdPlaceholders` method from the old `FeedDecoratorService` to the `AdService`. This change correctly assigns the responsibility of injecting ad placeholders into a feed to the `AdService`, cleaning up the architecture and adhering to the single responsibility principle. The `AdService` is now the sole authority on all ad-related operations, from loading ads to injecting their placeholders.
1 parent 1b2b1ac commit 997e462

File tree

1 file changed

+137
-0
lines changed

1 file changed

+137
-0
lines changed

lib/ads/ad_service.dart

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
import 'package:core/core.dart';
22
import 'package:flutter_news_app_mobile_client_full_source_code/ads/ad_provider.dart';
3+
import 'package:flutter_news_app_mobile_client_full_source_code/ads/models/ad_placeholder.dart';
34
import 'package:flutter_news_app_mobile_client_full_source_code/ads/models/ad_theme_style.dart';
45
import 'package:flutter_news_app_mobile_client_full_source_code/ads/models/inline_ad.dart';
56
import 'package:flutter_news_app_mobile_client_full_source_code/ads/models/interstitial_ad.dart';
67
import 'package:flutter_news_app_mobile_client_full_source_code/app/config/app_environment.dart';
78
import 'package:logging/logging.dart';
9+
import 'package:uuid/uuid.dart';
810

911
/// {@template ad_service}
1012
/// A service responsible for managing and providing ads to the application.
@@ -13,6 +15,9 @@ import 'package:logging/logging.dart';
1315
/// and the underlying ad network providers (e.g., AdMob, Local). It handles
1416
/// requesting different types of ads (inline native/banner, full-screen interstitial)
1517
/// and wrapping them in appropriate generic models for use throughout the app.
18+
///
19+
/// It is also responsible for injecting stateless `AdPlaceholder` markers into
20+
/// lists of feed items based on remote configuration rules.
1621
/// {@endtemplate}
1722
class AdService {
1823
/// {@macro ad_service}
@@ -30,6 +35,7 @@ class AdService {
3035
final Map<AdPlatformType, AdProvider> _adProviders;
3136
final AppEnvironment _environment;
3237
final Logger _logger;
38+
final Uuid _uuid = const Uuid();
3339

3440
// Configurable retry parameters for ad loading.
3541
// TODO(fulleni): Make this configurable through the remote config.
@@ -439,4 +445,135 @@ class AdService {
439445
);
440446
return null;
441447
}
448+
449+
/// Injects stateless [AdPlaceholder] markers into a list of [FeedItem]s
450+
/// based on configured ad frequency rules.
451+
///
452+
/// This method ensures that ad placeholders for *inline* ads (native and banner)
453+
/// are placed according to the `adPlacementInterval` (initial buffer before
454+
/// the first ad) and `adFrequency` (subsequent ad spacing). It correctly
455+
/// accounts for content items and decorators, ignoring previously injected
456+
/// ad placeholders when calculating placement.
457+
///
458+
/// [feedItems]: The list of feed items (headlines, other decorators)
459+
/// to inject ad placeholders into.
460+
/// [user]: The current authenticated user, used to determine ad configuration.
461+
/// [adConfig]: The remote configuration for ad display rules.
462+
/// [imageStyle]: The desired image style for the ad, used to determine
463+
/// the placeholder's template type.
464+
/// [adThemeStyle]: The current theme style for ads, passed through to the
465+
/// AdLoaderWidget for consistent styling.
466+
/// [processedContentItemCount]: The count of *content items* (non-ad,
467+
/// non-decorator) that have already been processed in previous feed
468+
/// loads/pages. This is crucial for maintaining correct ad placement
469+
/// across pagination.
470+
///
471+
/// Returns a new list of [FeedItem] objects, interspersed with ad placeholders.
472+
Future<List<FeedItem>> injectAdPlaceholders({
473+
required List<FeedItem> feedItems,
474+
required User? user,
475+
required AdConfig adConfig,
476+
required HeadlineImageStyle imageStyle,
477+
required AdThemeStyle adThemeStyle,
478+
int processedContentItemCount = 0,
479+
}) async {
480+
// If feed ads are not enabled in the remote config, return the original list.
481+
if (!adConfig.feedAdConfiguration.enabled) {
482+
return feedItems;
483+
}
484+
485+
final userRole = user?.appRole ?? AppUserRole.guestUser;
486+
487+
// Determine ad frequency rules based on user role.
488+
final feedAdFrequencyConfig =
489+
adConfig.feedAdConfiguration.visibleTo[userRole];
490+
491+
// Default to 0 for adFrequency and adPlacementInterval if no config is found
492+
// for the user role, effectively disabling ads for that role.
493+
final adFrequency = feedAdFrequencyConfig?.adFrequency ?? 0;
494+
final adPlacementInterval = feedAdFrequencyConfig?.adPlacementInterval ?? 0;
495+
496+
// If ad frequency is zero or less, no ads should be injected.
497+
if (adFrequency <= 0) {
498+
return feedItems;
499+
}
500+
501+
final result = <FeedItem>[];
502+
// This counter tracks the absolute number of *content items* (headlines,
503+
// topics, sources, countries, and decorators) processed so far, including
504+
// those from previous pages. This is key for accurate ad placement.
505+
var currentContentItemCount = processedContentItemCount;
506+
507+
// Get the primary ad platform and its identifiers
508+
final primaryAdPlatform = adConfig.primaryAdPlatform;
509+
final platformAdIdentifiers =
510+
adConfig.platformAdIdentifiers[primaryAdPlatform];
511+
if (platformAdIdentifiers == null) {
512+
_logger.warning(
513+
'No AdPlatformIdentifiers found for primary platform: $primaryAdPlatform. '
514+
'Cannot inject ad placeholders.',
515+
);
516+
return feedItems;
517+
}
518+
519+
// Get the ad type for feed ads (native or banner)
520+
final feedAdType = adConfig.feedAdConfiguration.adType;
521+
522+
for (final item in feedItems) {
523+
result.add(item);
524+
525+
// Only increment the content item counter if the current item is
526+
// a primary content type or a decorator (not an ad placeholder).
527+
// This ensures ad placement is based purely on content/decorator density.
528+
if (item is! AdPlaceholder) {
529+
currentContentItemCount++;
530+
}
531+
532+
// Check if an ad should be injected at the current position.
533+
// An ad is injected if:
534+
// 1. We have passed the initial placement interval.
535+
// 2. The number of content items *after* the initial interval is a
536+
// multiple of the ad frequency.
537+
if (currentContentItemCount >= adPlacementInterval &&
538+
(currentContentItemCount - adPlacementInterval) % adFrequency == 0) {
539+
String? adIdentifier;
540+
541+
// Determine the specific ad ID based on the feed ad type.
542+
switch (feedAdType) {
543+
case AdType.native:
544+
adIdentifier = platformAdIdentifiers.feedNativeAdId;
545+
case AdType.banner:
546+
adIdentifier = platformAdIdentifiers.feedBannerAdId;
547+
case AdType.interstitial:
548+
case AdType.video:
549+
// Interstitial and video ads are not injected into the feed.
550+
_logger.warning(
551+
'Attempted to inject $feedAdType ad into feed. This is not supported.',
552+
);
553+
adIdentifier = null;
554+
}
555+
556+
if (adIdentifier != null) {
557+
// The actual ad loading will be handled by a dedicated `AdLoaderWidget`
558+
// in the UI layer when this placeholder scrolls into view. This
559+
// decouples ad loading from the BLoC's state and allows for efficient
560+
// caching and disposal of native ad resources.
561+
result.add(
562+
AdPlaceholder(
563+
id: _uuid.v4(),
564+
adPlatformType: primaryAdPlatform,
565+
adType: feedAdType,
566+
adId: adIdentifier,
567+
),
568+
);
569+
} else {
570+
_logger.warning(
571+
'No valid ad ID found for platform $primaryAdPlatform and type '
572+
'$feedAdType. Ad placeholder not injected.',
573+
);
574+
}
575+
}
576+
}
577+
return result;
578+
}
442579
}

0 commit comments

Comments
 (0)