Skip to content

Commit 3f7ef81

Browse files
committed
feat(feed): create dedicated source list filter page
Introduces a new `SourceListFilterPage` to provide a dedicated UI for filtering the list of sources by headquarters and type. This page is a self-contained `StatefulWidget` that manages its own local state for the filter criteria. This architectural change decouples the filter criteria UI from the source selection UI, paving the way for a cleaner implementation on the main `SourceFilterPage` and resolving the underlying cause of the source type selection bug.
1 parent ee6f284 commit 3f7ef81

File tree

1 file changed

+222
-0
lines changed

1 file changed

+222
-0
lines changed

source_list_filter_page.dart

Lines changed: 222 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,222 @@
1+
// ignore_for_file: lines_longer_than_80_chars
2+
3+
import 'package:core/core.dart';
4+
import 'package:flutter/material.dart';
5+
import 'package:flutter_news_app_mobile_client_full_source_code/l10n/app_localizations.dart';
6+
import 'package:flutter_news_app_mobile_client_full_source_code/l10n/l10n.dart';
7+
import 'package:ui_kit/ui_kit.dart';
8+
9+
/// {@template source_list_filter_page}
10+
/// A dedicated page for selecting filter criteria for the source list.
11+
///
12+
/// This page allows users to filter sources by their headquarters country
13+
/// and by their type (e.g., News Agency, Blog). It manages its own local
14+
/// state and returns the selected criteria to the previous page.
15+
/// {@endtemplate}
16+
class SourceListFilterPage extends StatefulWidget {
17+
/// {@macro source_list_filter_page}
18+
const SourceListFilterPage({
19+
required this.allCountries,
20+
required this.allSourceTypes,
21+
required this.initialSelectedHeadquarterCountries,
22+
required this.initialSelectedSourceTypes,
23+
super.key,
24+
});
25+
26+
/// All available countries to be used as headquarters filter options.
27+
final List<Country> allCountries;
28+
29+
/// All available source types to be used as filter options.
30+
final List<SourceType> allSourceTypes;
31+
32+
/// The set of headquarters countries that were initially selected.
33+
final Set<Country> initialSelectedHeadquarterCountries;
34+
35+
/// The set of source types that were initially selected.
36+
final Set<SourceType> initialSelectedSourceTypes;
37+
38+
@override
39+
State<SourceListFilterPage> createState() => _SourceListFilterPageState();
40+
}
41+
42+
class _SourceListFilterPageState extends State<SourceListFilterPage> {
43+
late final Set<Country> _selectedHeadquarterCountries;
44+
late final Set<SourceType> _selectedSourceTypes;
45+
46+
@override
47+
void initState() {
48+
super.initState();
49+
// Initialize the local state with the initial selections passed in.
50+
_selectedHeadquarterCountries = Set.from(
51+
widget.initialSelectedHeadquarterCountries,
52+
);
53+
_selectedSourceTypes = Set.from(widget.initialSelectedSourceTypes);
54+
}
55+
56+
@override
57+
Widget build(BuildContext context) {
58+
final l10n = AppLocalizationsX(context).l10n;
59+
final theme = Theme.of(context);
60+
final textTheme = theme.textTheme;
61+
62+
return Scaffold(
63+
appBar: AppBar(
64+
title: Text(
65+
l10n.sourceListFilterPageTitle,
66+
style: textTheme.titleLarge,
67+
),
68+
actions: [
69+
// Apply button returns the selected criteria to the previous page.
70+
IconButton(
71+
icon: const Icon(Icons.check),
72+
tooltip: l10n.headlinesFeedFilterApplyButton,
73+
onPressed: () {
74+
Navigator.of(context).pop({
75+
'countries': _selectedHeadquarterCountries,
76+
'types': _selectedSourceTypes,
77+
});
78+
},
79+
),
80+
],
81+
),
82+
body: ListView(
83+
padding: const EdgeInsets.symmetric(vertical: AppSpacing.md),
84+
children: [
85+
// Section for filtering by headquarters country.
86+
_buildSectionHeader(
87+
context,
88+
l10n.headlinesFeedFilterSourceCountryLabel,
89+
),
90+
_buildCountryCapsules(context, widget.allCountries, l10n, textTheme),
91+
const SizedBox(height: AppSpacing.lg),
92+
93+
// Section for filtering by source type.
94+
_buildSectionHeader(context, l10n.headlinesFeedFilterSourceTypeLabel),
95+
_buildSourceTypeCapsules(
96+
context,
97+
widget.allSourceTypes,
98+
l10n,
99+
textTheme,
100+
),
101+
],
102+
),
103+
);
104+
}
105+
106+
/// Builds the header for a filter section.
107+
Widget _buildSectionHeader(BuildContext context, String title) {
108+
return Padding(
109+
padding: const EdgeInsets.symmetric(horizontal: AppSpacing.paddingMedium),
110+
child: Text(
111+
title,
112+
style: Theme.of(
113+
context,
114+
).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold),
115+
),
116+
);
117+
}
118+
119+
/// Builds the horizontal list of [ChoiceChip] widgets for countries.
120+
Widget _buildCountryCapsules(
121+
BuildContext context,
122+
List<Country> allCountries,
123+
AppLocalizations l10n,
124+
TextTheme textTheme,
125+
) {
126+
return SizedBox(
127+
height: AppSpacing.xl + AppSpacing.md,
128+
child: ListView.builder(
129+
scrollDirection: Axis.horizontal,
130+
padding: const EdgeInsets.symmetric(
131+
horizontal: AppSpacing.paddingMedium,
132+
vertical: AppSpacing.sm,
133+
),
134+
itemCount: allCountries.length + 1,
135+
itemBuilder: (context, index) {
136+
if (index == 0) {
137+
// The 'All' chip.
138+
return ChoiceChip(
139+
label: Text(l10n.headlinesFeedFilterAllLabel),
140+
labelStyle: textTheme.labelLarge,
141+
selected: _selectedHeadquarterCountries.isEmpty,
142+
onSelected: (_) => setState(_selectedHeadquarterCountries.clear),
143+
);
144+
}
145+
final country = allCountries[index - 1];
146+
return Padding(
147+
padding: const EdgeInsets.only(left: AppSpacing.sm),
148+
child: ChoiceChip(
149+
avatar: country.flagUrl.isNotEmpty
150+
? CircleAvatar(
151+
backgroundImage: NetworkImage(country.flagUrl),
152+
radius: AppSpacing.sm + AppSpacing.xs,
153+
)
154+
: null,
155+
label: Text(country.name),
156+
labelStyle: textTheme.labelLarge,
157+
selected: _selectedHeadquarterCountries.contains(country),
158+
onSelected: (isSelected) {
159+
setState(() {
160+
if (isSelected) {
161+
_selectedHeadquarterCountries.add(country);
162+
} else {
163+
_selectedHeadquarterCountries.remove(country);
164+
}
165+
});
166+
},
167+
),
168+
);
169+
},
170+
),
171+
);
172+
}
173+
174+
/// Builds the horizontal list of [ChoiceChip] widgets for source types.
175+
Widget _buildSourceTypeCapsules(
176+
BuildContext context,
177+
List<SourceType> allSourceTypes,
178+
AppLocalizations l10n,
179+
TextTheme textTheme,
180+
) {
181+
return SizedBox(
182+
height: AppSpacing.xl + AppSpacing.md,
183+
child: ListView.builder(
184+
scrollDirection: Axis.horizontal,
185+
padding: const EdgeInsets.symmetric(
186+
horizontal: AppSpacing.paddingMedium,
187+
vertical: AppSpacing.sm,
188+
),
189+
itemCount: allSourceTypes.length + 1,
190+
itemBuilder: (context, index) {
191+
if (index == 0) {
192+
// The 'All' chip.
193+
return ChoiceChip(
194+
label: Text(l10n.headlinesFeedFilterAllLabel),
195+
labelStyle: textTheme.labelLarge,
196+
selected: _selectedSourceTypes.isEmpty,
197+
onSelected: (_) => setState(_selectedSourceTypes.clear),
198+
);
199+
}
200+
final sourceType = allSourceTypes[index - 1];
201+
return Padding(
202+
padding: const EdgeInsets.only(left: AppSpacing.sm),
203+
child: ChoiceChip(
204+
label: Text(sourceType.name),
205+
labelStyle: textTheme.labelLarge,
206+
selected: _selectedSourceTypes.contains(sourceType),
207+
onSelected: (isSelected) {
208+
setState(() {
209+
if (isSelected) {
210+
_selectedSourceTypes.add(sourceType);
211+
} else {
212+
_selectedSourceTypes.remove(sourceType);
213+
}
214+
});
215+
},
216+
),
217+
);
218+
},
219+
),
220+
);
221+
}
222+
}

0 commit comments

Comments
 (0)