Skip to content

Commit fe33f62

Browse files
committed
refactor(headlines-feed): implement HeadlinesFilterBloc for state management
- Replace local state management with HeadlinesFilterBloc - Remove temporary filter selection variables and logic - Add loading and failure states handling - Update UI to reflect new state management approach - Refactor filter tile building and selection logic
1 parent fa60c98 commit fe33f62

File tree

1 file changed

+121
-137
lines changed

1 file changed

+121
-137
lines changed

lib/headlines-feed/view/headlines_filter_page.dart

Lines changed: 121 additions & 137 deletions
Original file line numberDiff line numberDiff line change
@@ -8,85 +8,62 @@ import 'package:flutter_bloc/flutter_bloc.dart';
88
import 'package:flutter_news_app_mobile_client_full_source_code/ads/models/ad_theme_style.dart';
99
import 'package:flutter_news_app_mobile_client_full_source_code/app/bloc/app_bloc.dart';
1010
import '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';
1112
import 'package:flutter_news_app_mobile_client_full_source_code/headlines-feed/models/headline_filter.dart';
1213
import 'package:flutter_news_app_mobile_client_full_source_code/l10n/l10n.dart';
1314
import 'package:flutter_news_app_mobile_client_full_source_code/router/routes.dart';
1415
import 'package:go_router/go_router.dart';
1516
import '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

Comments
 (0)