Skip to content

Commit 36ba973

Browse files
committed
feat(ui): refactor SearchableDropdownFormField to be BLoC-aware
Refactored `SearchableDropdownFormField` to make it a fully reactive component that rebuilds its internal search dialog based on the state of a provided BLoC. - Made the widget generic over BLoC and State types (`<T, B extends BlocBase<S>, S>`). - Replaced static `items`, `hasMore`, and `isLoading` parameters with constructor-injected "extractor" functions (`itemsExtractor`, `hasMoreExtractor`, `isLoadingExtractor`). These functions derive the necessary data from the BLoC's state. - The internal `_SearchableSelectionDialog` now uses a `BlocBuilder` to listen for state changes from the provided BLoC, ensuring the list of items and loading indicators are always up-to-date.
1 parent 1b13a0f commit 36ba973

File tree

1 file changed

+52
-31
lines changed

1 file changed

+52
-31
lines changed

lib/shared/widgets/searchable_dropdown_form_field.dart

Lines changed: 52 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import 'package:flutter/material.dart';
2+
import 'package:flutter_bloc/flutter_bloc.dart';
23
import 'package:ui_kit/ui_kit.dart';
34

45
/// A generic type for the builder function that creates list items in the
@@ -21,17 +22,19 @@ typedef SearchableDropdownSelectedItemBuilder<T> = Widget Function(
2122
/// This widget is generic and can be used for any type [T]. It requires
2223
/// builders for constructing the list items and the selected item display,
2324
/// as well as callbacks to handle searching and pagination.
24-
class SearchableDropdownFormField<T> extends FormField<T> {
25+
class SearchableDropdownFormField<T, B extends BlocBase<S>, S>
26+
extends FormField<T> {
2527
/// {@macro searchable_dropdown_form_field}
2628
SearchableDropdownFormField({
27-
required List<T> items,
29+
required B bloc,
30+
required List<T> Function(S state) itemsExtractor,
31+
required bool Function(S state) hasMoreExtractor,
32+
required bool Function(S state) isLoadingExtractor,
2833
required ValueChanged<T?> onChanged,
2934
required ValueChanged<String> onSearchChanged,
3035
required VoidCallback onLoadMore,
3136
required SearchableDropdownItemBuilder<T> itemBuilder,
3237
required SearchableDropdownSelectedItemBuilder<T> selectedItemBuilder,
33-
required bool hasMore,
34-
bool? isLoading,
3538
super.key,
3639
T? initialValue,
3740
String? labelText,
@@ -49,13 +52,14 @@ class SearchableDropdownFormField<T> extends FormField<T> {
4952
onTap: () async {
5053
final selectedItem = await showDialog<T>(
5154
context: state.context,
52-
builder: (context) => _SearchableSelectionDialog<T>(
53-
items: items,
55+
builder: (context) => _SearchableSelectionDialog<T, B, S>(
56+
bloc: bloc,
57+
itemsExtractor: itemsExtractor,
58+
hasMoreExtractor: hasMoreExtractor,
59+
isLoadingExtractor: isLoadingExtractor,
5460
onSearchChanged: onSearchChanged,
5561
onLoadMore: onLoadMore,
5662
itemBuilder: itemBuilder,
57-
hasMore: hasMore,
58-
isLoading: isLoading ?? false,
5963
searchHintText: searchHintText,
6064
noItemsFoundText: noItemsFoundText,
6165
),
@@ -83,35 +87,38 @@ class SearchableDropdownFormField<T> extends FormField<T> {
8387
}
8488

8589
/// The modal dialog that contains the searchable and paginated list.
86-
class _SearchableSelectionDialog<T> extends StatefulWidget {
90+
class _SearchableSelectionDialog<T, B extends BlocBase<S>, S>
91+
extends StatefulWidget {
8792
const _SearchableSelectionDialog({
88-
required this.items,
93+
required this.bloc,
94+
required this.itemsExtractor,
95+
required this.hasMoreExtractor,
96+
required this.isLoadingExtractor,
8997
required this.onSearchChanged,
9098
required this.onLoadMore,
9199
required this.itemBuilder,
92-
required this.hasMore,
93-
required this.isLoading,
94100
this.searchHintText,
95101
this.noItemsFoundText,
96102
super.key,
97103
});
98104

99-
final List<T> items;
105+
final B bloc;
106+
final List<T> Function(S state) itemsExtractor;
107+
final bool Function(S state) hasMoreExtractor;
108+
final bool Function(S state) isLoadingExtractor;
100109
final ValueChanged<String> onSearchChanged;
101110
final VoidCallback onLoadMore;
102111
final SearchableDropdownItemBuilder<T> itemBuilder;
103-
final bool hasMore;
104-
final bool isLoading;
105112
final String? searchHintText;
106113
final String? noItemsFoundText;
107114

108115
@override
109-
State<_SearchableSelectionDialog<T>> createState() =>
110-
_SearchableSelectionDialogState<T>();
116+
State<_SearchableSelectionDialog<T, B, S>> createState() =>
117+
_SearchableSelectionDialogState<T, B, S>();
111118
}
112119

113-
class _SearchableSelectionDialogState<T>
114-
extends State<_SearchableSelectionDialog<T>> {
120+
class _SearchableSelectionDialogState<T, B extends BlocBase<S>, S>
121+
extends State<_SearchableSelectionDialog<T, B, S>> {
115122
final _scrollController = ScrollController();
116123
final _searchController = TextEditingController();
117124

@@ -168,7 +175,16 @@ class _SearchableSelectionDialogState<T>
168175
),
169176
const SizedBox(height: AppSpacing.md),
170177
Expanded(
171-
child: _buildList(),
178+
child: BlocBuilder<B, S>(
179+
bloc: widget.bloc,
180+
builder: (context, state) {
181+
final items = widget.itemsExtractor(state);
182+
final hasMore = widget.hasMoreExtractor(state);
183+
final isLoading = widget.isLoadingExtractor(state);
184+
185+
return _buildList(items, hasMore, isLoading);
186+
},
187+
),
172188
),
173189
],
174190
),
@@ -177,29 +193,34 @@ class _SearchableSelectionDialogState<T>
177193
);
178194
}
179195

180-
Widget _buildList() {
181-
if (widget.isLoading && widget.items.isEmpty) {
196+
Widget _buildList(List<T> items, bool hasMore, bool isLoading) {
197+
if (isLoading && items.isEmpty) {
182198
return const Center(child: CircularProgressIndicator());
183199
}
184200

185-
if (widget.items.isEmpty) {
201+
if (items.isEmpty) {
186202
return Center(
187203
child: Text(widget.noItemsFoundText ?? 'No items found.'),
188204
);
189205
}
190206

191207
return ListView.builder(
192208
controller: _scrollController,
193-
itemCount:
194-
widget.hasMore ? widget.items.length + 1 : widget.items.length,
209+
itemCount: items.length + (hasMore ? 1 : 0),
195210
itemBuilder: (context, index) {
196-
if (index >= widget.items.length) {
197-
return const Padding(
198-
padding: EdgeInsets.symmetric(vertical: AppSpacing.md),
199-
child: Center(child: CircularProgressIndicator()),
200-
);
211+
if (index >= items.length) {
212+
// This is the last item, which is the loading indicator.
213+
// It's only shown if we have more items and are currently loading.
214+
return isLoading
215+
? const Padding(
216+
padding: EdgeInsets.symmetric(vertical: AppSpacing.md),
217+
child: Center(child: CircularProgressIndicator()),
218+
)
219+
: const SizedBox.shrink();
201220
}
202-
final item = widget.items[index];
221+
222+
// This is a regular item.
223+
final item = items[index];
203224
return InkWell(
204225
onTap: () => Navigator.of(context).pop(item),
205226
child: widget.itemBuilder(context, item),

0 commit comments

Comments
 (0)