@@ -8,85 +8,62 @@ import 'package:flutter_bloc/flutter_bloc.dart';
88import 'package:flutter_news_app_mobile_client_full_source_code/ads/models/ad_theme_style.dart' ;
99import 'package:flutter_news_app_mobile_client_full_source_code/app/bloc/app_bloc.dart' ;
1010import 'package:flutter_news_app_mobile_client_full_source_code/headlines-feed/bloc/headlines_feed_bloc.dart' ;
11+ import 'package:flutter_news_app_mobile_client_full_source_code/headlines-feed/bloc/headlines_filter_bloc.dart' ;
1112import 'package:flutter_news_app_mobile_client_full_source_code/headlines-feed/models/headline_filter.dart' ;
1213import 'package:flutter_news_app_mobile_client_full_source_code/l10n/l10n.dart' ;
1314import 'package:flutter_news_app_mobile_client_full_source_code/router/routes.dart' ;
1415import 'package:go_router/go_router.dart' ;
1516import 'package:ui_kit/ui_kit.dart' ;
1617
17- // Keys for passing data to/from SourceFilterPage
18- const String keySelectedSources = 'selectedSources' ;
19-
2018/// {@template headlines_filter_page}
2119/// A full-screen dialog page for selecting headline filters.
2220///
2321/// Allows users to navigate to specific pages for selecting categories,
2422/// sources, and event countries. Manages the temporary state of these
2523/// selections before applying them to the [HeadlinesFeedBloc] .
2624/// {@endtemplate}
27- class HeadlinesFilterPage extends StatefulWidget {
25+ class HeadlinesFilterPage extends StatelessWidget {
2826 /// {@macro headlines_filter_page}
2927 const HeadlinesFilterPage ({super .key});
3028
3129 @override
32- State <HeadlinesFilterPage > createState () => _HeadlinesFilterPageState ();
33- }
34-
35- class _HeadlinesFilterPageState extends State <HeadlinesFilterPage > {
36- /// Temporary state for filter selections within this modal flow.
37- /// These hold the selections made by the user *while* this filter page
38- /// and its sub-pages (Category, Source, Country) are open.
39- /// They are initialized from the main [HeadlinesFeedBloc] 's current filter
40- /// and are only applied back to the BLoC when the user taps 'Apply'.
41- late List <Topic > _tempSelectedTopics;
42- late List <Source > _tempSelectedSources;
43- late List <Country > _tempSelectedEventCountries;
44-
45- /// Flag to indicate if the "Apply my followed items" filter is active.
46- bool _useFollowedFilters = false ;
47-
48- @override
49- void initState () {
50- super .initState ();
51- final headlinesFeedState = BlocProvider .of <HeadlinesFeedBloc >(
52- context,
53- ).state;
54-
55- final currentFilter = headlinesFeedState.filter;
56- _tempSelectedTopics = List .from (currentFilter.topics ?? []);
57- _tempSelectedSources = List .from (currentFilter.sources ?? []);
58- _tempSelectedEventCountries = List .from (currentFilter.eventCountries ?? []);
30+ Widget build (BuildContext context) {
31+ // Access the HeadlinesFeedBloc to get the current filter state for initialization.
32+ final headlinesFeedBloc = context.read <HeadlinesFeedBloc >();
33+ final currentFilter = headlinesFeedBloc.state.filter;
5934
60- _useFollowedFilters = currentFilter.isFromFollowedItems;
35+ return BlocProvider (
36+ create: (context) => HeadlinesFilterBloc (
37+ topicsRepository: context.read <DataRepository <Topic >>(),
38+ sourcesRepository: context.read <DataRepository <Source >>(),
39+ countriesRepository: context.read <DataRepository <Country >>(),
40+ appBloc: context.read <AppBloc >(),
41+ )..add (
42+ FilterDataLoaded (
43+ initialSelectedTopics: currentFilter.topics ?? [],
44+ initialSelectedSources: currentFilter.sources ?? [],
45+ initialSelectedCountries: currentFilter.eventCountries ?? [],
46+ isUsingFollowedItems: currentFilter.isFromFollowedItems,
47+ ),
48+ ),
49+ child: const _HeadlinesFilterView (),
50+ );
6151 }
52+ }
6253
63- /// Clears all temporary filter selections.
64- void _clearTemporaryFilters () {
65- setState (() {
66- _tempSelectedTopics = [];
67- _tempSelectedSources = [];
68- _tempSelectedEventCountries = [];
69- });
70- }
54+ class _HeadlinesFilterView extends StatelessWidget {
55+ const _HeadlinesFilterView ();
7156
7257 /// Builds a [ListTile] representing a filter criterion (e.g., Categories).
7358 ///
7459 /// Displays the criterion [title] , the number of currently selected items
7560 /// ([selectedCount] ), and navigates to the corresponding selection page
7661 /// specified by [routeName] when tapped.
77- ///
78- /// Uses [currentSelection] to pass the current temporary selection state
79- /// (e.g., `_tempSelectedSources` ) to the corresponding criterion selection page
80- /// (e.g., `SourceFilterPage` ) via the `extra` parameter of `context.pushNamed` .
81- /// Updates the temporary state via the [onResult] callback when the
82- /// criterion page pops with a result (the user tapped 'Apply' on that page).
83- Widget _buildFilterTile <T >({
62+ Widget _buildFilterTile ({
8463 required BuildContext context,
8564 required String title,
8665 required int selectedCount,
8766 required String routeName,
88- required List <T > currentSelectionData,
89- required void Function (List <T >)? onResult,
9067 bool enabled = true ,
9168 }) {
9269 final l10n = AppLocalizationsX (context).l10n;
@@ -103,14 +80,10 @@ class _HeadlinesFilterPageState extends State<HeadlinesFilterPage> {
10380 trailing: const Icon (Icons .chevron_right),
10481 enabled: enabled,
10582 onTap: enabled
106- ? () async {
107- final result = await context.pushNamed <List <T >>(
108- routeName,
109- extra: currentSelectionData,
110- );
111- if (result != null && onResult != null ) {
112- onResult (result);
113- }
83+ ? () {
84+ // Navigate to the child filter page. The child page will read
85+ // the current selections from HeadlinesFilterBloc directly.
86+ context.pushNamed (routeName);
11487 }
11588 : null ,
11689 );
@@ -121,10 +94,6 @@ class _HeadlinesFilterPageState extends State<HeadlinesFilterPage> {
12194 final l10n = AppLocalizationsX (context).l10n;
12295 final theme = Theme .of (context);
12396
124- // Determine if the "Apply my followed items" feature is active.
125- // This will disable the individual filter tiles.
126- final isFollowedFilterActive = _useFollowedFilters;
127-
12897 return Scaffold (
12998 appBar: AppBar (
13099 leading: IconButton (
@@ -139,22 +108,15 @@ class _HeadlinesFilterPageState extends State<HeadlinesFilterPage> {
139108 icon: const Icon (Icons .refresh),
140109 tooltip: l10n.headlinesFeedFilterResetButton,
141110 onPressed: () {
142- context.read <HeadlinesFeedBloc >().add (
143- HeadlinesFeedFiltersCleared (
144- adThemeStyle: AdThemeStyle .fromTheme (Theme .of (context)),
145- ),
146- );
147- // Also reset local state for the "Apply my followed items"
148- setState (() {
149- _useFollowedFilters = false ;
150- _clearTemporaryFilters ();
151- });
152- context.pop ();
111+ context.read <HeadlinesFilterBloc >().add (
112+ const FilterSelectionsCleared (),
113+ );
153114 },
154115 ),
155116 // Apply My Followed Items Button
156- BlocBuilder <AppBloc , AppState >(
157- builder: (context, appState) {
117+ BlocBuilder <HeadlinesFilterBloc , HeadlinesFilterState >(
118+ builder: (context, filterState) {
119+ final appState = context.watch <AppBloc >().state;
158120 final followedTopics =
159121 appState.userContentPreferences? .followedTopics ?? [];
160122 final followedSources =
@@ -167,24 +129,21 @@ class _HeadlinesFilterPageState extends State<HeadlinesFilterPage> {
167129 followedCountries.isNotEmpty;
168130
169131 return IconButton (
170- icon: _useFollowedFilters
132+ icon: filterState.isUsingFollowedItems
171133 ? const Icon (Icons .favorite)
172134 : const Icon (Icons .favorite_border),
173- color: _useFollowedFilters ? theme.colorScheme.primary : null ,
135+ color: filterState.isUsingFollowedItems
136+ ? theme.colorScheme.primary
137+ : null ,
174138 tooltip: l10n.headlinesFeedFilterApplyFollowedLabel,
175139 onPressed: hasFollowedItems
176140 ? () {
177- setState (() {
178- _useFollowedFilters = ! _useFollowedFilters;
179- if (_useFollowedFilters) {
180- _tempSelectedTopics = List .from (followedTopics);
181- _tempSelectedSources = List .from (followedSources);
182- _tempSelectedEventCountries =
183- List .from (followedCountries);
184- } else {
185- _clearTemporaryFilters ();
186- }
187- });
141+ context.read <HeadlinesFilterBloc >().add (
142+ FollowedItemsFilterToggled (
143+ isUsingFollowedItems:
144+ ! filterState.isUsingFollowedItems,
145+ ),
146+ );
188147 }
189148 : () {
190149 ScaffoldMessenger .of (context)
@@ -206,67 +165,92 @@ class _HeadlinesFilterPageState extends State<HeadlinesFilterPage> {
206165 icon: const Icon (Icons .check),
207166 tooltip: l10n.headlinesFeedFilterApplyButton,
208167 onPressed: () {
168+ final filterState = context.read <HeadlinesFilterBloc >().state;
209169 final newFilter = HeadlineFilter (
210- topics: _tempSelectedTopics .isNotEmpty
211- ? _tempSelectedTopics
170+ topics: filterState.selectedTopics .isNotEmpty
171+ ? filterState.selectedTopics. toList ()
212172 : null ,
213- sources: _tempSelectedSources .isNotEmpty
214- ? _tempSelectedSources
173+ sources: filterState.selectedSources .isNotEmpty
174+ ? filterState.selectedSources. toList ()
215175 : null ,
216- eventCountries: _tempSelectedEventCountries .isNotEmpty
217- ? _tempSelectedEventCountries
176+ eventCountries: filterState.selectedCountries .isNotEmpty
177+ ? filterState.selectedCountries. toList ()
218178 : null ,
219- isFromFollowedItems: _useFollowedFilters ,
179+ isFromFollowedItems: filterState.isUsingFollowedItems ,
220180 );
221181 context.read <HeadlinesFeedBloc >().add (
222- HeadlinesFeedFiltersApplied (
223- filter: newFilter,
224- adThemeStyle: AdThemeStyle .fromTheme (Theme .of (context)),
225- ),
226- );
182+ HeadlinesFeedFiltersApplied (
183+ filter: newFilter,
184+ adThemeStyle: AdThemeStyle .fromTheme (Theme .of (context)),
185+ ),
186+ );
227187 context.pop ();
228188 },
229189 ),
230190 ],
231191 ),
232- body: ListView (
233- padding: const EdgeInsets .symmetric (vertical: AppSpacing .md),
234- children: [
235- const Divider (),
236- _buildFilterTile <Topic >(
237- context: context,
238- title: l10n.headlinesFeedFilterTopicLabel,
239- enabled: ! isFollowedFilterActive,
240- selectedCount: _tempSelectedTopics.length,
241- routeName: Routes .feedFilterTopicsName,
242- currentSelectionData: _tempSelectedTopics,
243- onResult: (result) {
244- setState (() => _tempSelectedTopics = result);
245- },
246- ),
247- _buildFilterTile <Source >(
248- context: context,
249- title: l10n.headlinesFeedFilterSourceLabel,
250- enabled: ! isFollowedFilterActive,
251- selectedCount: _tempSelectedSources.length,
252- routeName: Routes .feedFilterSourcesName,
253- currentSelectionData: _tempSelectedSources,
254- onResult: (result) {
255- setState (() => _tempSelectedSources = result);
256- },
257- ),
258- _buildFilterTile <Country >(
259- context: context,
260- title: l10n.headlinesFeedFilterEventCountryLabel,
261- enabled: ! isFollowedFilterActive,
262- selectedCount: _tempSelectedEventCountries.length,
263- routeName: Routes .feedFilterEventCountriesName,
264- currentSelectionData: _tempSelectedEventCountries,
265- onResult: (result) {
266- setState (() => _tempSelectedEventCountries = result);
267- },
268- ),
269- ],
192+ body: BlocBuilder <HeadlinesFilterBloc , HeadlinesFilterState >(
193+ builder: (context, filterState) {
194+ // Determine if the "Apply my followed items" feature is active.
195+ // This will disable the individual filter tiles.
196+ final isFollowedFilterActive = filterState.isUsingFollowedItems;
197+
198+ if (filterState.status == HeadlinesFilterStatus .loading) {
199+ return LoadingStateWidget (
200+ icon: Icons .filter_list,
201+ headline: l10n.headlinesFeedFilterLoadingHeadline,
202+ subheadline: l10n.pleaseWait,
203+ );
204+ }
205+
206+ if (filterState.status == HeadlinesFilterStatus .failure) {
207+ return FailureStateWidget (
208+ exception: filterState.error ??
209+ const UnknownException ('Failed to load filter data.' ),
210+ onRetry: () {
211+ final headlinesFeedBloc = context.read <HeadlinesFeedBloc >();
212+ final currentFilter = headlinesFeedBloc.state.filter;
213+ context.read <HeadlinesFilterBloc >().add (
214+ FilterDataLoaded (
215+ initialSelectedTopics: currentFilter.topics ?? [],
216+ initialSelectedSources: currentFilter.sources ?? [],
217+ initialSelectedCountries:
218+ currentFilter.eventCountries ?? [],
219+ isUsingFollowedItems: currentFilter.isFromFollowedItems,
220+ ),
221+ );
222+ },
223+ );
224+ }
225+
226+ return ListView (
227+ padding: const EdgeInsets .symmetric (vertical: AppSpacing .md),
228+ children: [
229+ const Divider (),
230+ _buildFilterTile (
231+ context: context,
232+ title: l10n.headlinesFeedFilterTopicLabel,
233+ enabled: ! isFollowedFilterActive,
234+ selectedCount: filterState.selectedTopics.length,
235+ routeName: Routes .feedFilterTopicsName,
236+ ),
237+ _buildFilterTile (
238+ context: context,
239+ title: l10n.headlinesFeedFilterSourceLabel,
240+ enabled: ! isFollowedFilterActive,
241+ selectedCount: filterState.selectedSources.length,
242+ routeName: Routes .feedFilterSourcesName,
243+ ),
244+ _buildFilterTile (
245+ context: context,
246+ title: l10n.headlinesFeedFilterEventCountryLabel,
247+ enabled: ! isFollowedFilterActive,
248+ selectedCount: filterState.selectedCountries.length,
249+ routeName: Routes .feedFilterEventCountriesName,
250+ ),
251+ ],
252+ );
253+ },
270254 ),
271255 );
272256 }
0 commit comments