diff --git a/lib/content_management/view/create_headline_page.dart b/lib/content_management/view/create_headline_page.dart index 8311014..70dfc29 100644 --- a/lib/content_management/view/create_headline_page.dart +++ b/lib/content_management/view/create_headline_page.dart @@ -214,12 +214,15 @@ class _CreateHeadlineViewState extends State<_CreateHeadlineView> { .add(CreateHeadlineTopicChanged(value)), ), const SizedBox(height: AppSpacing.lg), - SearchableDropdownFormField( + SearchableDropdownFormField( labelText: l10n.countryName, - items: state.countries, + bloc: context.read(), initialValue: state.eventCountry, - hasMore: state.countriesHasMore, - isLoading: state.status == CreateHeadlineStatus.loading, + itemsExtractor: (state) => state.countries, + hasMoreExtractor: (state) => state.countriesHasMore, + isLoadingExtractor: (state) => + state.status == CreateHeadlineStatus.loading, onChanged: (value) => context .read() .add(CreateHeadlineCountryChanged(value)), diff --git a/lib/content_management/view/create_source_page.dart b/lib/content_management/view/create_source_page.dart index a623b2b..82c921c 100644 --- a/lib/content_management/view/create_source_page.dart +++ b/lib/content_management/view/create_source_page.dart @@ -161,12 +161,15 @@ class _CreateSourceViewState extends State<_CreateSourceView> { .add(CreateSourceUrlChanged(value)), ), const SizedBox(height: AppSpacing.lg), - SearchableDropdownFormField( + SearchableDropdownFormField( labelText: l10n.language, - items: state.languages, + bloc: context.read(), initialValue: state.language, - hasMore: state.languagesHasMore, - isLoading: state.status == CreateSourceStatus.loading, + itemsExtractor: (state) => state.languages, + hasMoreExtractor: (state) => state.languagesHasMore, + isLoadingExtractor: (state) => + state.status == CreateSourceStatus.loading, onChanged: (value) => context .read() .add(CreateSourceLanguageChanged(value)), @@ -206,12 +209,15 @@ class _CreateSourceViewState extends State<_CreateSourceView> { .add(CreateSourceTypeChanged(value)), ), const SizedBox(height: AppSpacing.lg), - SearchableDropdownFormField( + SearchableDropdownFormField( labelText: l10n.headquarters, - items: state.countries, + bloc: context.read(), initialValue: state.headquarters, - hasMore: state.countriesHasMore, - isLoading: state.status == CreateSourceStatus.loading, + itemsExtractor: (state) => state.countries, + hasMoreExtractor: (state) => state.countriesHasMore, + isLoadingExtractor: (state) => + state.status == CreateSourceStatus.loading, onChanged: (value) => context .read() .add(CreateSourceHeadquartersChanged(value)), diff --git a/lib/content_management/view/edit_headline_page.dart b/lib/content_management/view/edit_headline_page.dart index 2b9cc85..b0b9df2 100644 --- a/lib/content_management/view/edit_headline_page.dart +++ b/lib/content_management/view/edit_headline_page.dart @@ -282,12 +282,15 @@ class _EditHeadlineViewState extends State<_EditHeadlineView> { .add(EditHeadlineTopicChanged(value)), ), const SizedBox(height: AppSpacing.lg), - SearchableDropdownFormField( + SearchableDropdownFormField( labelText: l10n.countryName, - items: state.countries, + bloc: context.read(), initialValue: selectedCountry, - hasMore: state.countriesHasMore, - isLoading: state.status == EditHeadlineStatus.loading, + itemsExtractor: (state) => state.countries, + hasMoreExtractor: (state) => state.countriesHasMore, + isLoadingExtractor: (state) => + state.status == EditHeadlineStatus.loading, onChanged: (value) => context .read() .add(EditHeadlineCountryChanged(value)), diff --git a/lib/content_management/view/edit_source_page.dart b/lib/content_management/view/edit_source_page.dart index bbdb844..22529b3 100644 --- a/lib/content_management/view/edit_source_page.dart +++ b/lib/content_management/view/edit_source_page.dart @@ -191,12 +191,15 @@ class _EditSourceViewState extends State<_EditSourceView> { ), ), const SizedBox(height: AppSpacing.lg), - SearchableDropdownFormField( + SearchableDropdownFormField( labelText: l10n.language, - items: state.languages, + bloc: context.read(), initialValue: state.language, - hasMore: state.languagesHasMore, - isLoading: state.status == EditSourceStatus.loading, + itemsExtractor: (state) => state.languages, + hasMoreExtractor: (state) => state.languagesHasMore, + isLoadingExtractor: (state) => + state.status == EditSourceStatus.loading, onChanged: (value) => context .read() .add(EditSourceLanguageChanged(value)), @@ -236,12 +239,15 @@ class _EditSourceViewState extends State<_EditSourceView> { ), ), const SizedBox(height: AppSpacing.lg), - SearchableDropdownFormField( + SearchableDropdownFormField( labelText: l10n.headquarters, - items: state.countries, + bloc: context.read(), initialValue: state.headquarters, - hasMore: state.countriesHasMore, - isLoading: state.status == EditSourceStatus.loading, + itemsExtractor: (state) => state.countries, + hasMoreExtractor: (state) => state.countriesHasMore, + isLoadingExtractor: (state) => + state.status == EditSourceStatus.loading, onChanged: (value) => context .read() .add(EditSourceHeadquartersChanged(value)), diff --git a/lib/shared/widgets/searchable_dropdown_form_field.dart b/lib/shared/widgets/searchable_dropdown_form_field.dart index d5887ee..e97b730 100644 --- a/lib/shared/widgets/searchable_dropdown_form_field.dart +++ b/lib/shared/widgets/searchable_dropdown_form_field.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:ui_kit/ui_kit.dart'; /// A generic type for the builder function that creates list items in the @@ -21,17 +22,19 @@ typedef SearchableDropdownSelectedItemBuilder = Widget Function( /// This widget is generic and can be used for any type [T]. It requires /// builders for constructing the list items and the selected item display, /// as well as callbacks to handle searching and pagination. -class SearchableDropdownFormField extends FormField { +class SearchableDropdownFormField, S> + extends FormField { /// {@macro searchable_dropdown_form_field} SearchableDropdownFormField({ - required List items, + required B bloc, + required List Function(S state) itemsExtractor, + required bool Function(S state) hasMoreExtractor, + required bool Function(S state) isLoadingExtractor, required ValueChanged onChanged, required ValueChanged onSearchChanged, required VoidCallback onLoadMore, required SearchableDropdownItemBuilder itemBuilder, required SearchableDropdownSelectedItemBuilder selectedItemBuilder, - required bool hasMore, - bool? isLoading, super.key, T? initialValue, String? labelText, @@ -49,13 +52,14 @@ class SearchableDropdownFormField extends FormField { onTap: () async { final selectedItem = await showDialog( context: state.context, - builder: (context) => _SearchableSelectionDialog( - items: items, + builder: (context) => _SearchableSelectionDialog( + bloc: bloc, + itemsExtractor: itemsExtractor, + hasMoreExtractor: hasMoreExtractor, + isLoadingExtractor: isLoadingExtractor, onSearchChanged: onSearchChanged, onLoadMore: onLoadMore, itemBuilder: itemBuilder, - hasMore: hasMore, - isLoading: isLoading ?? false, searchHintText: searchHintText, noItemsFoundText: noItemsFoundText, ), @@ -83,35 +87,38 @@ class SearchableDropdownFormField extends FormField { } /// The modal dialog that contains the searchable and paginated list. -class _SearchableSelectionDialog extends StatefulWidget { +class _SearchableSelectionDialog, S> + extends StatefulWidget { const _SearchableSelectionDialog({ - required this.items, + required this.bloc, + required this.itemsExtractor, + required this.hasMoreExtractor, + required this.isLoadingExtractor, required this.onSearchChanged, required this.onLoadMore, required this.itemBuilder, - required this.hasMore, - required this.isLoading, this.searchHintText, this.noItemsFoundText, super.key, }); - final List items; + final B bloc; + final List Function(S state) itemsExtractor; + final bool Function(S state) hasMoreExtractor; + final bool Function(S state) isLoadingExtractor; final ValueChanged onSearchChanged; final VoidCallback onLoadMore; final SearchableDropdownItemBuilder itemBuilder; - final bool hasMore; - final bool isLoading; final String? searchHintText; final String? noItemsFoundText; @override - State<_SearchableSelectionDialog> createState() => - _SearchableSelectionDialogState(); + State<_SearchableSelectionDialog> createState() => + _SearchableSelectionDialogState(); } -class _SearchableSelectionDialogState - extends State<_SearchableSelectionDialog> { +class _SearchableSelectionDialogState, S> + extends State<_SearchableSelectionDialog> { final _scrollController = ScrollController(); final _searchController = TextEditingController(); @@ -168,7 +175,16 @@ class _SearchableSelectionDialogState ), const SizedBox(height: AppSpacing.md), Expanded( - child: _buildList(), + child: BlocBuilder( + bloc: widget.bloc, + builder: (context, state) { + final items = widget.itemsExtractor(state); + final hasMore = widget.hasMoreExtractor(state); + final isLoading = widget.isLoadingExtractor(state); + + return _buildList(items, hasMore, isLoading); + }, + ), ), ], ), @@ -177,12 +193,12 @@ class _SearchableSelectionDialogState ); } - Widget _buildList() { - if (widget.isLoading && widget.items.isEmpty) { + Widget _buildList(List items, bool hasMore, bool isLoading) { + if (isLoading && items.isEmpty) { return const Center(child: CircularProgressIndicator()); } - if (widget.items.isEmpty) { + if (items.isEmpty) { return Center( child: Text(widget.noItemsFoundText ?? 'No items found.'), ); @@ -190,16 +206,21 @@ class _SearchableSelectionDialogState return ListView.builder( controller: _scrollController, - itemCount: - widget.hasMore ? widget.items.length + 1 : widget.items.length, + itemCount: items.length + (hasMore ? 1 : 0), itemBuilder: (context, index) { - if (index >= widget.items.length) { - return const Padding( - padding: EdgeInsets.symmetric(vertical: AppSpacing.md), - child: Center(child: CircularProgressIndicator()), - ); + if (index >= items.length) { + // This is the last item, which is the loading indicator. + // It's only shown if we have more items and are currently loading. + return isLoading + ? const Padding( + padding: EdgeInsets.symmetric(vertical: AppSpacing.md), + child: Center(child: CircularProgressIndicator()), + ) + : const SizedBox.shrink(); } - final item = widget.items[index]; + + // This is a regular item. + final item = items[index]; return InkWell( onTap: () => Navigator.of(context).pop(item), child: widget.itemBuilder(context, item),