Skip to content

Commit bb84557

Browse files
committed
feat(shared): create generic MultiSelectSearchPage
Introduces a new reusable `MultiSelectSearchPage` widget. This generic, stateful widget provides a standard UI for selecting multiple items from a long, searchable list. This component will be used to improve the user experience for filtering by country on the source filter page, replacing the non-scalable horizontal `ChoiceChip` list with a more conventional and user-friendly vertical list with search functionality.
1 parent 4df5d76 commit bb84557

File tree

1 file changed

+126
-0
lines changed

1 file changed

+126
-0
lines changed
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
import 'package:flutter/material.dart';
2+
import 'package:flutter_news_app_mobile_client_full_source_code/l10n/l10n.dart';
3+
import 'package:ui_kit/ui_kit.dart';
4+
5+
/// {@template multi_select_search_page}
6+
/// A generic and reusable page for selecting multiple items from a searchable
7+
/// list.
8+
///
9+
/// This widget is designed to be pushed onto the navigation stack and return
10+
/// a `Set<T>` of the selected items when popped.
11+
/// {@endtemplate}
12+
class MultiSelectSearchPage<T> extends StatefulWidget {
13+
/// {@macro multi_select_search_page}
14+
const MultiSelectSearchPage({
15+
required this.title,
16+
required this.allItems,
17+
required this.initialSelectedItems,
18+
required this.itemBuilder,
19+
super.key,
20+
});
21+
22+
/// The title displayed in the `AppBar`.
23+
final String title;
24+
25+
/// The complete list of items of type [T] to be displayed and filtered.
26+
final List<T> allItems;
27+
28+
/// The initial set of selected items.
29+
final Set<T> initialSelectedItems;
30+
31+
/// A function that returns the display string for an item of type [T].
32+
final String Function(T item) itemBuilder;
33+
34+
@override
35+
State<MultiSelectSearchPage<T>> createState() =>
36+
_MultiSelectSearchPageState<T>();
37+
}
38+
39+
class _MultiSelectSearchPageState<T> extends State<MultiSelectSearchPage<T>> {
40+
late final Set<T> _selectedItems;
41+
final _searchController = TextEditingController();
42+
String _searchQuery = '';
43+
44+
@override
45+
void initState() {
46+
super.initState();
47+
_selectedItems = Set<T>.from(widget.initialSelectedItems);
48+
_searchController.addListener(() {
49+
setState(() => _searchQuery = _searchController.text);
50+
});
51+
}
52+
53+
@override
54+
void dispose() {
55+
_searchController.dispose();
56+
super.dispose();
57+
}
58+
59+
@override
60+
Widget build(BuildContext context) {
61+
final l10n = AppLocalizationsX(context).l10n;
62+
final theme = Theme.of(context);
63+
64+
final filteredItems = widget.allItems.where((item) {
65+
final itemName = widget.itemBuilder(item).toLowerCase();
66+
return itemName.contains(_searchQuery.toLowerCase());
67+
}).toList();
68+
69+
return Scaffold(
70+
appBar: AppBar(
71+
title: Text(widget.title, style: theme.textTheme.titleLarge),
72+
actions: [
73+
IconButton(
74+
icon: const Icon(Icons.check),
75+
tooltip: l10n.headlinesFeedFilterApplyButton,
76+
onPressed: () => Navigator.of(context).pop(_selectedItems),
77+
),
78+
],
79+
),
80+
body: Column(
81+
children: [
82+
Padding(
83+
padding: const EdgeInsets.all(AppSpacing.md),
84+
child: TextFormField(
85+
controller: _searchController,
86+
decoration: InputDecoration(
87+
hintText: l10n.searchHintTextGeneric,
88+
prefixIcon: const Icon(Icons.search),
89+
border: const OutlineInputBorder(
90+
borderRadius: BorderRadius.all(
91+
Radius.circular(AppSpacing.sm),
92+
),
93+
),
94+
),
95+
),
96+
),
97+
Expanded(
98+
child: ListView.builder(
99+
itemCount: filteredItems.length,
100+
itemBuilder: (context, index) {
101+
final item = filteredItems[index];
102+
final isSelected = _selectedItems.contains(item);
103+
return CheckboxListTile(
104+
title: Text(widget.itemBuilder(item)),
105+
value: isSelected,
106+
onChanged: (bool? value) {
107+
if (value != null) {
108+
setState(() {
109+
if (value) {
110+
_selectedItems.add(item);
111+
} else {
112+
_selectedItems.remove(item);
113+
}
114+
});
115+
}
116+
},
117+
controlAffinity: ListTileControlAffinity.leading,
118+
);
119+
},
120+
),
121+
),
122+
],
123+
),
124+
);
125+
}
126+
}

0 commit comments

Comments
 (0)