1- import 'dart:async' ;
2-
31import 'package:bb_mobile/core/themes/app_theme.dart' ;
42import 'package:bb_mobile/core/utils/build_context_x.dart' ;
3+ import 'package:bb_mobile/core/widgets/bb_refresh_indicator.dart' ;
54import 'package:bb_mobile/core/widgets/buttons/button.dart' ;
65import 'package:bb_mobile/features/recipients/frameworks/ui/widgets/jurisdiction_dropdown.dart' ;
76import 'package:bb_mobile/features/recipients/frameworks/ui/widgets/recipients_list_tile.dart' ;
@@ -21,11 +20,7 @@ class RecipientsListTab extends StatefulWidget {
2120}
2221
2322class RecipientsListTabState extends State <RecipientsListTab > {
24- String ? _jurisdictionFilter;
25- String _searchQuery = '' ;
26- List <RecipientViewModel >? _recipients;
2723 RecipientViewModel ? _selectedRecipient;
28- late StreamSubscription <RecipientsState > _stateSubscription;
2924 late ScrollController _scrollController;
3025 late TextEditingController _searchController;
3126
@@ -35,26 +30,6 @@ class RecipientsListTabState extends State<RecipientsListTab> {
3530 _searchController = TextEditingController ();
3631 _scrollController = ScrollController ();
3732 _scrollController.addListener (_onScroll);
38-
39- final bloc = context.read <RecipientsBloc >();
40- // Listen for changes in the RecipientsBloc state to update the recipients list
41- _stateSubscription = bloc.stream.listen ((state) {
42- setState (() {
43- _recipients = _applyFilters (
44- state.filteredRecipientsByJurisdiction (_jurisdictionFilter),
45- );
46- _jurisdictionFilter = state.availableJurisdictions.length == 1
47- ? state.availableJurisdictions.first
48- : _jurisdictionFilter;
49- });
50- });
51- // Initialize the recipients list
52- _recipients = _applyFilters (
53- bloc.state.filteredRecipientsByJurisdiction (_jurisdictionFilter),
54- );
55- _jurisdictionFilter = bloc.state.availableJurisdictions.length == 1
56- ? bloc.state.availableJurisdictions.first
57- : _jurisdictionFilter;
5833 }
5934
6035 void _onScroll () {
@@ -64,38 +39,21 @@ class RecipientsListTabState extends State<RecipientsListTab> {
6439 }
6540 }
6641
67- List <RecipientViewModel >? _applyFilters (
68- List <RecipientViewModel >? recipients,
69- ) {
70- if (recipients == null ) return null ;
71-
72- if (_searchQuery.isEmpty) return recipients;
73-
74- final searchLower = _searchQuery.toLowerCase ();
75- final filtered = recipients.where ((recipient) {
76- final displayName = recipient.displayName? .toLowerCase () ?? '' ;
77- final label = recipient.label? .toLowerCase () ?? '' ;
78- return displayName.contains (searchLower) || label.contains (searchLower);
79- }).toList ();
80-
81- // If search returns no results and there are more recipients to load, trigger loading
82- if (filtered.isEmpty &&
83- _searchQuery.isNotEmpty &&
84- context.read <RecipientsBloc >().state.hasMoreRecipientsToLoad) {
85- // Schedule loading more recipients after this build completes
86- WidgetsBinding .instance.addPostFrameCallback ((_) {
87- context.read <RecipientsBloc >().add (const RecipientsEvent .moreLoaded ());
88- });
89- }
90-
91- return filtered;
42+ Future <void > _onRefresh () async {
43+ final bloc = context.read <RecipientsBloc >();
44+ bloc.add (const RecipientsEvent .refreshed ());
45+ // orElse guards against the bloc closing (route popped) before completion,
46+ // which would otherwise throw an unhandled StateError on this future.
47+ await bloc.stream.firstWhere (
48+ (state) => ! state.isLoadingRecipients,
49+ orElse: () => bloc.state,
50+ );
9251 }
9352
9453 @override
9554 void dispose () {
9655 _searchController.dispose ();
9756 _scrollController.dispose ();
98- _stateSubscription.cancel ();
9957 super .dispose ();
10058 }
10159
@@ -110,39 +68,28 @@ class RecipientsListTabState extends State<RecipientsListTab> {
11068 decoration: InputDecoration (
11169 hintText: context.loc.recipientsSearchHint,
11270 prefixIcon: const Icon (Icons .search),
113- suffixIcon: _searchQuery.isNotEmpty
114- ? IconButton (
115- icon: const Icon (Icons .clear),
116- onPressed: () {
117- setState (() {
71+ suffixIcon: ValueListenableBuilder <TextEditingValue >(
72+ valueListenable: _searchController,
73+ builder: (context, value, _) => value.text.isEmpty
74+ ? const SizedBox .shrink ()
75+ : IconButton (
76+ icon: const Icon (Icons .clear),
77+ onPressed: () {
11878 _searchController.clear ();
119- _searchQuery = '' ;
120- _recipients = _applyFilters (
121- context
122- .read <RecipientsBloc >()
123- .state
124- .filteredRecipientsByJurisdiction (
125- _jurisdictionFilter,
126- ),
79+ context.read <RecipientsBloc >().add (
80+ const RecipientsEvent .searchChanged ('' ),
12781 );
128- });
129- },
130- )
131- : null ,
82+ },
83+ ),
84+ ),
13285 border: OutlineInputBorder (
13386 borderRadius: BorderRadius .circular (8.0 ),
13487 ),
13588 ),
13689 onChanged: (value) {
137- setState (() {
138- _searchQuery = value;
139- _recipients = _applyFilters (
140- context
141- .read <RecipientsBloc >()
142- .state
143- .filteredRecipientsByJurisdiction (_jurisdictionFilter),
144- );
145- });
90+ context.read <RecipientsBloc >().add (
91+ RecipientsEvent .searchChanged (value),
92+ );
14693 },
14794 ),
14895 const Gap (16.0 ),
@@ -151,54 +98,35 @@ class RecipientsListTabState extends State<RecipientsListTab> {
15198 style: context.font.bodyMedium,
15299 ),
153100 const Gap (8.0 ),
154- JurisdictionsDropdown (
155- selectedJurisdiction: _jurisdictionFilter,
156- includeAllOption: true ,
157- onChanged: (newJurisdiction) {
158- setState (() {
159- _jurisdictionFilter = newJurisdiction;
160- _recipients = _applyFilters (
161- context
162- .read <RecipientsBloc >()
163- .state
164- .filteredRecipientsByJurisdiction (newJurisdiction),
165- );
166- _selectedRecipient =
167- newJurisdiction == null ||
168- _selectedRecipient? .jurisdictionCode == newJurisdiction
169- ? _selectedRecipient
170- : null ;
171- });
101+ BlocSelector <RecipientsBloc , RecipientsState , String ?>(
102+ selector: (state) => state.jurisdictionFilter,
103+ builder: (context, selected) {
104+ return JurisdictionsDropdown (
105+ selectedJurisdiction: selected,
106+ includeAllOption: true ,
107+ onChanged: (newJurisdiction) {
108+ context.read <RecipientsBloc >().add (
109+ RecipientsEvent .jurisdictionChanged (newJurisdiction),
110+ );
111+ if (newJurisdiction != null &&
112+ _selectedRecipient? .jurisdictionCode != newJurisdiction) {
113+ setState (() => _selectedRecipient = null );
114+ }
115+ },
116+ );
172117 },
173118 ),
174119 const Gap (16.0 ),
175120 Expanded (
176- child: _recipients == null
177- ? const Center (child: CircularProgressIndicator ())
178- : _recipients! .isEmpty
179- ? Center (
180- child: Text (
181- context.loc.recipientsListEmpty,
182- style: context.font.bodyLarge,
183- ),
184- )
185- : ListView .builder (
186- controller: _scrollController,
187- itemBuilder: (context, index) {
188- final recipient = _recipients! [index];
189- return RecipientsListTile (
190- recipient: recipient,
191- selected: _selectedRecipient == recipient,
192- onTap: () {
193- setState (() {
194- _selectedRecipient = recipient;
195- });
196- },
197- );
198- },
199- shrinkWrap: true ,
200- itemCount: _recipients! .length,
201- ),
121+ child: BlocBuilder <RecipientsBloc , RecipientsState >(
122+ builder: (context, state) {
123+ final recipients = state.selectableRecipients ?? const [];
124+ return BBRefreshIndicator (
125+ onRefresh: _onRefresh,
126+ child: _buildListContent (state, recipients),
127+ );
128+ },
129+ ),
202130 ),
203131 if (widget.hookError != null )
204132 Padding (
@@ -240,4 +168,47 @@ class RecipientsListTabState extends State<RecipientsListTab> {
240168 ],
241169 );
242170 }
171+
172+ Widget _buildListContent (
173+ RecipientsState state,
174+ List <RecipientViewModel > recipients,
175+ ) {
176+ if (recipients.isEmpty) {
177+ if (state.isLoadingRecipients) {
178+ return const Center (child: CircularProgressIndicator ());
179+ }
180+
181+ final message = state.failedToLoadRecipients != null
182+ ? context.loc.recipientsListLoadError
183+ : context.loc.recipientsListEmpty;
184+ // Wrap in a scrollable so pull-to-refresh works while empty.
185+ return ListView (
186+ physics: const AlwaysScrollableScrollPhysics (),
187+ children: [
188+ const Gap (96.0 ),
189+ Center (
190+ child: Text (message, style: context.font.bodyLarge),
191+ ),
192+ ],
193+ );
194+ }
195+
196+ return ListView .builder (
197+ controller: _scrollController,
198+ physics: const AlwaysScrollableScrollPhysics (),
199+ itemBuilder: (context, index) {
200+ final recipient = recipients[index];
201+ return RecipientsListTile (
202+ recipient: recipient,
203+ selected: _selectedRecipient == recipient,
204+ onTap: () {
205+ setState (() {
206+ _selectedRecipient = recipient;
207+ });
208+ },
209+ );
210+ },
211+ itemCount: recipients.length,
212+ );
213+ }
243214}
0 commit comments