Skip to content

Commit b381539

Browse files
committed
feat(headlines-feed): implement HeadlinesFilterBloc
- Add new BLoC for managing centralized headlines filter feature - Implement logic for fetching filter options (topics, sources, countries) - Handle user interactions with filter UI - Integrate with AppBloc for user content preferences - Add event handlers for various filter-related actions
1 parent ddb7852 commit b381539

File tree

1 file changed

+207
-0
lines changed

1 file changed

+207
-0
lines changed
Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
import 'dart:async';
2+
3+
import 'package:bloc/bloc.dart';
4+
import 'package:bloc_concurrency/bloc_concurrency.dart';
5+
import 'package:core/core.dart';
6+
import 'package:data_repository/data_repository.dart';
7+
import 'package:equatable/equatable.dart';
8+
import 'package:flutter_news_app_mobile_client_full_source_code/app/bloc/app_bloc.dart';
9+
import 'package:logging/logging.dart';
10+
11+
part 'headlines_filter_event.dart';
12+
part 'headlines_filter_state.dart';
13+
14+
/// {@template headlines_filter_bloc}
15+
/// Manages the state for the centralized headlines filter feature.
16+
///
17+
/// This BLoC is responsible for fetching all available filter options
18+
/// (topics, sources, countries) and managing the user's temporary selections
19+
/// as they interact with the filter UI. It also integrates with the [AppBloc]
20+
/// to access user-specific content preferences for "followed items" functionality.
21+
/// {@endtemplate}
22+
class HeadlinesFilterBloc extends Bloc<HeadlinesFilterEvent, HeadlinesFilterState> {
23+
/// {@macro headlines_filter_bloc}
24+
///
25+
/// Requires repositories for topics, sources, and countries, as well as
26+
/// the [AppBloc] to access user content preferences.
27+
HeadlinesFilterBloc({
28+
required DataRepository<Topic> topicsRepository,
29+
required DataRepository<Source> sourcesRepository,
30+
required DataRepository<Country> countriesRepository,
31+
required AppBloc appBloc,
32+
}) : _topicsRepository = topicsRepository,
33+
_sourcesRepository = sourcesRepository,
34+
_countriesRepository = countriesRepository,
35+
_appBloc = appBloc,
36+
_logger = Logger('HeadlinesFilterBloc'),
37+
super(const HeadlinesFilterState()) {
38+
on<FilterDataLoaded>(_onFilterDataLoaded, transformer: restartable());
39+
on<FilterTopicToggled>(_onFilterTopicToggled);
40+
on<FilterSourceToggled>(_onFilterSourceToggled);
41+
on<FilterCountryToggled>(_onFilterCountryToggled);
42+
on<FollowedItemsFilterToggled>(_onFollowedItemsFilterToggled);
43+
on<FilterSelectionsCleared>(_onFilterSelectionsCleared);
44+
}
45+
46+
final DataRepository<Topic> _topicsRepository;
47+
final DataRepository<Source> _sourcesRepository;
48+
final DataRepository<Country> _countriesRepository;
49+
final AppBloc _appBloc;
50+
final Logger _logger;
51+
52+
/// Handles the [FilterDataLoaded] event, fetching all necessary filter data.
53+
///
54+
/// This method fetches all available topics, sources, and countries.
55+
/// It also initializes the selected items based on the `initialSelected`
56+
/// lists provided in the event, typically from the current filter state
57+
/// of the `HeadlinesFeedBloc`.
58+
Future<void> _onFilterDataLoaded(
59+
FilterDataLoaded event,
60+
Emitter<HeadlinesFilterState> emit,
61+
) async {
62+
if (state.status == HeadlinesFilterStatus.loading ||
63+
state.status == HeadlinesFilterStatus.success) {
64+
return;
65+
}
66+
67+
emit(state.copyWith(status: HeadlinesFilterStatus.loading));
68+
69+
try {
70+
final allTopicsResponse = await _topicsRepository.readAll(
71+
sort: [const SortOption('name', SortOrder.asc)],
72+
);
73+
final allSourcesResponse = await _sourcesRepository.readAll(
74+
sort: [const SortOption('name', SortOrder.asc)],
75+
);
76+
final allCountriesResponse = await _countriesRepository.readAll(
77+
filter: {'hasActiveSources': true}, // Only countries with active sources
78+
sort: [const SortOption('name', SortOrder.asc)],
79+
);
80+
81+
emit(
82+
state.copyWith(
83+
status: HeadlinesFilterStatus.success,
84+
allTopics: allTopicsResponse.items,
85+
allSources: allSourcesResponse.items,
86+
allCountries: allCountriesResponse.items,
87+
selectedTopics: Set.from(event.initialSelectedTopics),
88+
selectedSources: Set.from(event.initialSelectedSources),
89+
selectedCountries: Set.from(event.initialSelectedCountries),
90+
isUsingFollowedItems: event.isUsingFollowedItems,
91+
clearError: true,
92+
),
93+
);
94+
} on HttpException catch (e) {
95+
_logger.severe('Failed to load filter data (HttpException): $e');
96+
emit(state.copyWith(status: HeadlinesFilterStatus.failure, error: e));
97+
} catch (e, s) {
98+
_logger.severe('Unexpected error loading filter data.', e, s);
99+
emit(
100+
state.copyWith(
101+
status: HeadlinesFilterStatus.failure,
102+
error: UnknownException(e.toString()),
103+
),
104+
);
105+
}
106+
}
107+
108+
/// Handles the [FilterTopicToggled] event, updating the selected topics.
109+
void _onFilterTopicToggled(
110+
FilterTopicToggled event,
111+
Emitter<HeadlinesFilterState> emit,
112+
) {
113+
final updatedSelectedTopics = Set<Topic>.from(state.selectedTopics);
114+
if (event.isSelected) {
115+
updatedSelectedTopics.add(event.topic);
116+
} else {
117+
updatedSelectedTopics.remove(event.topic);
118+
}
119+
emit(
120+
state.copyWith(
121+
selectedTopics: updatedSelectedTopics,
122+
isUsingFollowedItems: false, // Toggling individual item clears followed filter
123+
),
124+
);
125+
}
126+
127+
/// Handles the [FilterSourceToggled] event, updating the selected sources.
128+
void _onFilterSourceToggled(
129+
FilterSourceToggled event,
130+
Emitter<HeadlinesFilterState> emit,
131+
) {
132+
final updatedSelectedSources = Set<Source>.from(state.selectedSources);
133+
if (event.isSelected) {
134+
updatedSelectedSources.add(event.source);
135+
} else {
136+
updatedSelectedSources.remove(event.source);
137+
}
138+
emit(
139+
state.copyWith(
140+
selectedSources: updatedSelectedSources,
141+
isUsingFollowedItems: false, // Toggling individual item clears followed filter
142+
),
143+
);
144+
}
145+
146+
/// Handles the [FilterCountryToggled] event, updating the selected countries.
147+
void _onFilterCountryToggled(
148+
FilterCountryToggled event,
149+
Emitter<HeadlinesFilterState> emit,
150+
) {
151+
final updatedSelectedCountries = Set<Country>.from(state.selectedCountries);
152+
if (event.isSelected) {
153+
updatedSelectedCountries.add(event.country);
154+
} else {
155+
updatedSelectedCountries.remove(event.country);
156+
}
157+
emit(
158+
state.copyWith(
159+
selectedCountries: updatedSelectedCountries,
160+
isUsingFollowedItems: false, // Toggling individual item clears followed filter
161+
),
162+
);
163+
}
164+
165+
/// Handles the [FollowedItemsFilterToggled] event, applying or clearing
166+
/// followed items as filters.
167+
void _onFollowedItemsFilterToggled(
168+
FollowedItemsFilterToggled event,
169+
Emitter<HeadlinesFilterState> emit,
170+
) {
171+
if (event.isUsingFollowedItems) {
172+
final userPreferences = _appBloc.state.userContentPreferences;
173+
emit(
174+
state.copyWith(
175+
selectedTopics: Set.from(userPreferences?.followedTopics ?? []),
176+
selectedSources: Set.from(userPreferences?.followedSources ?? []),
177+
selectedCountries: Set.from(userPreferences?.followedCountries ?? []),
178+
isUsingFollowedItems: true,
179+
),
180+
);
181+
} else {
182+
emit(
183+
state.copyWith(
184+
selectedTopics: {},
185+
selectedSources: {},
186+
selectedCountries: {},
187+
isUsingFollowedItems: false,
188+
),
189+
);
190+
}
191+
}
192+
193+
/// Handles the [FilterSelectionsCleared] event, clearing all filter selections.
194+
void _onFilterSelectionsCleared(
195+
FilterSelectionsCleared event,
196+
Emitter<HeadlinesFilterState> emit,
197+
) {
198+
emit(
199+
state.copyWith(
200+
selectedTopics: {},
201+
selectedSources: {},
202+
selectedCountries: {},
203+
isUsingFollowedItems: false,
204+
),
205+
);
206+
}
207+
}

0 commit comments

Comments
 (0)