@@ -6,13 +6,14 @@ import 'package:collection/collection.dart';
66import 'package:core/core.dart' ;
77import 'package:data_repository/data_repository.dart' ;
88import 'package:equatable/equatable.dart' ;
9+ import 'package:flutter_news_app_mobile_client_full_source_code/ads/ad_service.dart' ;
910import 'package:flutter_news_app_mobile_client_full_source_code/ads/inline_ad_cache_service.dart' ;
1011import 'package:flutter_news_app_mobile_client_full_source_code/ads/models/ad_theme_style.dart' ;
1112import 'package:flutter_news_app_mobile_client_full_source_code/app/bloc/app_bloc.dart' ;
13+ import 'package:flutter_news_app_mobile_client_full_source_code/feed_decorators/services/feed_decorator_service.dart' ;
1214import 'package:flutter_news_app_mobile_client_full_source_code/headlines-feed/models/cached_feed.dart' ;
1315import 'package:flutter_news_app_mobile_client_full_source_code/headlines-feed/models/headline_filter.dart' ;
1416import 'package:flutter_news_app_mobile_client_full_source_code/headlines-feed/services/feed_cache_service.dart' ;
15- import 'package:flutter_news_app_mobile_client_full_source_code/shared/services/feed_decorator_service.dart' ;
1617import 'package:logging/logging.dart' ;
1718
1819part 'headlines_feed_event.dart' ;
@@ -23,16 +24,18 @@ part 'headlines_feed_state.dart';
2324///
2425/// This BLoC is the central orchestrator for the news feed. Its core
2526/// responsibilities include:
26- /// - Fetching, filtering, and displaying headlines from a [ DataRepository] .
27- /// - Injecting dynamic content such as ads and promotional items using the
28- /// [ FeedDecoratorService] .
27+ /// - Fetching, filtering, and displaying headlines from a ` DataRepository` .
28+ /// - Injecting dynamic content placeholders for ads and non-ad decorators
29+ /// using the `AdService` and the new ` FeedDecoratorService` .
2930/// - Implementing a robust, session-based in-memory caching strategy via
3031/// [FeedCacheService] to provide a highly responsive user experience.
3132///
3233/// ### Feed Decoration and Ad Injection:
33- /// On major feed loads (initial load, refresh, or new filter), this BLoC
34- /// uses the [FeedDecoratorService] to intersperse the headline content with
35- /// other `FeedItem` types, such as ads and promotional call-to-action items.
34+ /// On major feed loads (initial load, refresh, or new filter), this BLoC now
35+ /// uses a two-step process:
36+ /// 1. The new `FeedDecoratorService` injects a single `DecoratorPlaceholder` .
37+ /// 2. The `AdService` injects multiple `AdPlaceholder` items.
38+ /// The UI is then responsible for rendering loader widgets for these placeholders.
3639///
3740/// ### Caching and Data Flow Scenarios:
3841///
@@ -63,12 +66,14 @@ class HeadlinesFeedBloc extends Bloc<HeadlinesFeedEvent, HeadlinesFeedState> {
6366 HeadlinesFeedBloc ({
6467 required DataRepository <Headline > headlinesRepository,
6568 required FeedDecoratorService feedDecoratorService,
69+ required AdService adService,
6670 required AppBloc appBloc,
6771 required InlineAdCacheService inlineAdCacheService,
6872 required FeedCacheService feedCacheService,
6973 UserContentPreferences ? initialUserContentPreferences,
7074 }) : _headlinesRepository = headlinesRepository,
7175 _feedDecoratorService = feedDecoratorService,
76+ _adService = adService,
7277 _appBloc = appBloc,
7378 _inlineAdCacheService = inlineAdCacheService,
7479 _feedCacheService = feedCacheService,
@@ -132,6 +137,7 @@ class HeadlinesFeedBloc extends Bloc<HeadlinesFeedEvent, HeadlinesFeedState> {
132137
133138 final DataRepository <Headline > _headlinesRepository;
134139 final FeedDecoratorService _feedDecoratorService;
140+ final AdService _adService;
135141 final AppBloc _appBloc;
136142 final InlineAdCacheService _inlineAdCacheService;
137143 final FeedCacheService _feedCacheService;
@@ -243,19 +249,17 @@ class HeadlinesFeedBloc extends Bloc<HeadlinesFeedEvent, HeadlinesFeedState> {
243249 sort: [const SortOption ('updatedAt' , SortOrder .desc)],
244250 );
245251
246- // For pagination, only inject ad placeholders, not feed actions.
247- final newProcessedFeedItems = await _feedDecoratorService
248- .injectAdPlaceholders (
249- feedItems: headlineResponse.items,
250- user: currentUser,
251- adConfig: remoteConfig.adConfig,
252- imageStyle:
253- _appBloc.state.settings! .feedPreferences.headlineImageStyle,
254- adThemeStyle: event.adThemeStyle,
255- processedContentItemCount: cachedFeed.feedItems
256- .whereType <Headline >()
257- .length,
258- );
252+ // For pagination, only inject ad placeholders.
253+ final newProcessedFeedItems = await _adService.injectAdPlaceholders (
254+ feedItems: headlineResponse.items,
255+ user: currentUser,
256+ adConfig: remoteConfig.adConfig,
257+ imageStyle: _appBloc.state.settings! .feedPreferences.headlineImageStyle,
258+ adThemeStyle: event.adThemeStyle,
259+ processedContentItemCount: cachedFeed.feedItems
260+ .whereType <Headline >()
261+ .length,
262+ );
259263
260264 _logger.fine (
261265 'Pagination: Appending ${newProcessedFeedItems .length } new items to '
@@ -405,22 +409,25 @@ class HeadlinesFeedBloc extends Bloc<HeadlinesFeedEvent, HeadlinesFeedState> {
405409 'Refresh: Performing full decoration for filter "$filterKey ".' ,
406410 );
407411 final userPreferences = _appBloc.state.userContentPreferences;
412+ final settings = _appBloc.state.settings;
408413
409- // For a major load, use the full decoration pipeline.
410- final decorationResult = await _feedDecoratorService.decorateFeed (
411- headlines: headlineResponse.items,
412- user: currentUser,
414+ // Step 1: Inject the non-ad decorator placeholder.
415+ final feedWithDecorator = _feedDecoratorService.decorateFeed (
416+ feedItems: headlineResponse.items,
413417 remoteConfig: appConfig,
414- followedTopicIds:
415- userPreferences? .followedTopics.map ((t) => t.id).toList () ?? [],
416- followedSourceIds:
417- userPreferences? .followedSources.map ((s) => s.id).toList () ?? [],
418- imageStyle: _appBloc.state.settings! .feedPreferences.headlineImageStyle,
418+ );
419+
420+ // Step 2: Inject ad placeholders into the resulting list.
421+ final fullyDecoratedFeed = await _adService.injectAdPlaceholders (
422+ feedItems: feedWithDecorator,
423+ user: currentUser,
424+ adConfig: appConfig.adConfig,
425+ imageStyle: settings! .feedPreferences.headlineImageStyle,
419426 adThemeStyle: event.adThemeStyle,
420427 );
421428
422429 final newCachedFeed = CachedFeed (
423- feedItems: decorationResult.decoratedItems ,
430+ feedItems: fullyDecoratedFeed ,
424431 hasMore: headlineResponse.hasMore,
425432 cursor: headlineResponse.cursor,
426433 lastRefreshedAt: DateTime .now (),
@@ -437,26 +444,6 @@ class HeadlinesFeedBloc extends Bloc<HeadlinesFeedEvent, HeadlinesFeedState> {
437444 filter: state.filter,
438445 ),
439446 );
440-
441- // If a feed decorator was injected, notify AppBloc to update its status.
442- final injectedDecorator = decorationResult.injectedDecorator;
443- if (injectedDecorator != null && currentUser? .id != null ) {
444- if (injectedDecorator is CallToActionItem ) {
445- _appBloc.add (
446- AppUserFeedDecoratorShown (
447- userId: currentUser! .id,
448- feedDecoratorType: injectedDecorator.decoratorType,
449- ),
450- );
451- } else if (injectedDecorator is ContentCollectionItem ) {
452- _appBloc.add (
453- AppUserFeedDecoratorShown (
454- userId: currentUser! .id,
455- feedDecoratorType: injectedDecorator.decoratorType,
456- ),
457- );
458- }
459- }
460447 } on HttpException catch (e) {
461448 emit (state.copyWith (status: HeadlinesFeedStatus .failure, error: e));
462449 }
@@ -548,21 +535,25 @@ class HeadlinesFeedBloc extends Bloc<HeadlinesFeedEvent, HeadlinesFeedState> {
548535 );
549536
550537 final userPreferences = _appBloc.state.userContentPreferences;
538+ final settings = _appBloc.state.settings;
551539
552- final decorationResult = await _feedDecoratorService. decorateFeed (
553- headlines : headlineResponse.items,
554- user : currentUser ,
540+ // Step 1: Inject the non-ad decorator placeholder.
541+ final feedWithDecorator = _feedDecoratorService. decorateFeed (
542+ feedItems : headlineResponse.items ,
555543 remoteConfig: appConfig,
556- followedTopicIds:
557- userPreferences? .followedTopics.map ((t) => t.id).toList () ?? [],
558- followedSourceIds:
559- userPreferences? .followedSources.map ((s) => s.id).toList () ?? [],
560- imageStyle: _appBloc.state.settings! .feedPreferences.headlineImageStyle,
544+ );
545+
546+ // Step 2: Inject ad placeholders into the resulting list.
547+ final fullyDecoratedFeed = await _adService.injectAdPlaceholders (
548+ feedItems: feedWithDecorator,
549+ user: currentUser,
550+ adConfig: appConfig.adConfig,
551+ imageStyle: settings! .feedPreferences.headlineImageStyle,
561552 adThemeStyle: event.adThemeStyle,
562553 );
563554
564555 final newCachedFeed = CachedFeed (
565- feedItems: decorationResult.decoratedItems ,
556+ feedItems: fullyDecoratedFeed ,
566557 hasMore: headlineResponse.hasMore,
567558 cursor: headlineResponse.cursor,
568559 lastRefreshedAt: DateTime .now (),
@@ -578,25 +569,6 @@ class HeadlinesFeedBloc extends Bloc<HeadlinesFeedEvent, HeadlinesFeedState> {
578569 cursor: newCachedFeed.cursor,
579570 ),
580571 );
581-
582- final injectedDecorator = decorationResult.injectedDecorator;
583- if (injectedDecorator != null && currentUser? .id != null ) {
584- if (injectedDecorator is CallToActionItem ) {
585- _appBloc.add (
586- AppUserFeedDecoratorShown (
587- userId: currentUser! .id,
588- feedDecoratorType: injectedDecorator.decoratorType,
589- ),
590- );
591- } else if (injectedDecorator is ContentCollectionItem ) {
592- _appBloc.add (
593- AppUserFeedDecoratorShown (
594- userId: currentUser! .id,
595- feedDecoratorType: injectedDecorator.decoratorType,
596- ),
597- );
598- }
599- }
600572 } on HttpException catch (e) {
601573 emit (state.copyWith (status: HeadlinesFeedStatus .failure, error: e));
602574 }
@@ -660,21 +632,25 @@ class HeadlinesFeedBloc extends Bloc<HeadlinesFeedEvent, HeadlinesFeedState> {
660632 );
661633
662634 final userPreferences = _appBloc.state.userContentPreferences;
635+ final settings = _appBloc.state.settings;
663636
664- final decorationResult = await _feedDecoratorService. decorateFeed (
665- headlines : headlineResponse.items,
666- user : currentUser ,
637+ // Step 1: Inject the non-ad decorator placeholder.
638+ final feedWithDecorator = _feedDecoratorService. decorateFeed (
639+ feedItems : headlineResponse.items ,
667640 remoteConfig: appConfig,
668- followedTopicIds:
669- userPreferences? .followedTopics.map ((t) => t.id).toList () ?? [],
670- followedSourceIds:
671- userPreferences? .followedSources.map ((s) => s.id).toList () ?? [],
672- imageStyle: _appBloc.state.settings! .feedPreferences.headlineImageStyle,
641+ );
642+
643+ // Step 2: Inject ad placeholders into the resulting list.
644+ final fullyDecoratedFeed = await _adService.injectAdPlaceholders (
645+ feedItems: feedWithDecorator,
646+ user: currentUser,
647+ adConfig: appConfig.adConfig,
648+ imageStyle: settings! .feedPreferences.headlineImageStyle,
673649 adThemeStyle: event.adThemeStyle,
674650 );
675651
676652 final newCachedFeed = CachedFeed (
677- feedItems: decorationResult.decoratedItems ,
653+ feedItems: fullyDecoratedFeed ,
678654 hasMore: headlineResponse.hasMore,
679655 cursor: headlineResponse.cursor,
680656 lastRefreshedAt: DateTime .now (),
@@ -690,25 +666,6 @@ class HeadlinesFeedBloc extends Bloc<HeadlinesFeedEvent, HeadlinesFeedState> {
690666 cursor: newCachedFeed.cursor,
691667 ),
692668 );
693-
694- final injectedDecorator = decorationResult.injectedDecorator;
695- if (injectedDecorator != null && currentUser? .id != null ) {
696- if (injectedDecorator is CallToActionItem ) {
697- _appBloc.add (
698- AppUserFeedDecoratorShown (
699- userId: currentUser! .id,
700- feedDecoratorType: injectedDecorator.decoratorType,
701- ),
702- );
703- } else if (injectedDecorator is ContentCollectionItem ) {
704- _appBloc.add (
705- AppUserFeedDecoratorShown (
706- userId: currentUser! .id,
707- feedDecoratorType: injectedDecorator.decoratorType,
708- ),
709- );
710- }
711- }
712669 } on HttpException catch (e) {
713670 emit (state.copyWith (status: HeadlinesFeedStatus .failure, error: e));
714671 }
0 commit comments