Skip to content

Commit 61999ca

Browse files
committed
refactor(feed): update headlines bloc to use new decorator service
Refactors `HeadlinesFeedBloc` to integrate the new, self-contained `FeedDecoratorService` and the existing `AdService`. - The BLoC now uses a two-step process for feed decoration: first, it calls the new `FeedDecoratorService` to inject a `DecoratorPlaceholder`, and then it passes the result to the `AdService` to inject `AdPlaceholder`s. - All logic related to `FeedDecoratorResult` and dispatching `AppUserFeedDecoratorShown` events has been removed, as this responsibility is now fully delegated to the `FeedDecoratorLoaderWidget`. - The BLoC's constructor and dependencies have been updated to reflect these changes.
1 parent e21b527 commit 61999ca

File tree

1 file changed

+63
-106
lines changed

1 file changed

+63
-106
lines changed

lib/headlines-feed/bloc/headlines_feed_bloc.dart

Lines changed: 63 additions & 106 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,14 @@ import 'package:collection/collection.dart';
66
import 'package:core/core.dart';
77
import 'package:data_repository/data_repository.dart';
88
import 'package:equatable/equatable.dart';
9+
import 'package:flutter_news_app_mobile_client_full_source_code/ads/ad_service.dart';
910
import 'package:flutter_news_app_mobile_client_full_source_code/ads/inline_ad_cache_service.dart';
1011
import 'package:flutter_news_app_mobile_client_full_source_code/ads/models/ad_theme_style.dart';
1112
import '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';
1214
import 'package:flutter_news_app_mobile_client_full_source_code/headlines-feed/models/cached_feed.dart';
1315
import 'package:flutter_news_app_mobile_client_full_source_code/headlines-feed/models/headline_filter.dart';
1416
import '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';
1617
import 'package:logging/logging.dart';
1718

1819
part '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

Comments
 (0)