@@ -3,201 +3,219 @@ import 'dart:async';
3
3
import 'package:bloc/bloc.dart' ;
4
4
import 'package:bloc_concurrency/bloc_concurrency.dart' ;
5
5
import 'package:equatable/equatable.dart' ;
6
- import 'package:ht_categories_client/ht_categories_client.dart' ;
7
- import 'package:ht_countries_client/ht_countries_client.dart' ;
8
6
import 'package:ht_headlines_client/ht_headlines_client.dart' ; // Import for Headline and Exceptions
9
7
import 'package:ht_headlines_repository/ht_headlines_repository.dart' ;
10
8
import 'package:ht_main/headlines-feed/models/headline_filter.dart' ;
11
- import 'package:ht_sources_client/ht_sources_client.dart' ;
12
9
13
10
part 'headlines_feed_event.dart' ;
14
11
part 'headlines_feed_state.dart' ;
15
12
16
13
/// {@template headlines_feed_bloc}
17
- /// A Bloc that manages the headlines feed.
14
+ /// Manages the state for the headlines feed feature .
18
15
///
19
- /// It handles fetching and refreshing headlines data using the
20
- /// [HtHeadlinesRepository] .
16
+ /// Handles fetching headlines, applying filters, pagination, and refreshing
17
+ /// the feed using the provided [HtHeadlinesRepository] .
21
18
/// {@endtemplate}
22
19
class HeadlinesFeedBloc extends Bloc <HeadlinesFeedEvent , HeadlinesFeedState > {
23
20
/// {@macro headlines_feed_bloc}
21
+ ///
22
+ /// Requires a [HtHeadlinesRepository] to interact with the data layer.
24
23
HeadlinesFeedBloc ({required HtHeadlinesRepository headlinesRepository})
25
24
: _headlinesRepository = headlinesRepository,
26
25
super (HeadlinesFeedLoading ()) {
27
26
on < HeadlinesFeedFetchRequested > (
28
27
_onHeadlinesFeedFetchRequested,
29
- transformer: sequential (),
28
+ transformer:
29
+ sequential (), // Ensures fetch requests are processed one by one
30
30
);
31
31
on < HeadlinesFeedRefreshRequested > (
32
32
_onHeadlinesFeedRefreshRequested,
33
- transformer: restartable (),
33
+ transformer:
34
+ restartable (), // Ensures only the latest refresh is processed
34
35
);
35
- on < HeadlinesFeedFilterChanged > (_onHeadlinesFeedFilterChanged);
36
+ on < HeadlinesFeedFiltersApplied > (_onHeadlinesFeedFiltersApplied);
37
+ on < HeadlinesFeedFiltersCleared > (_onHeadlinesFeedFiltersCleared);
36
38
}
37
39
38
40
final HtHeadlinesRepository _headlinesRepository;
39
41
42
+ /// The number of headlines to fetch per page during pagination or initial load.
40
43
static const _headlinesFetchLimit = 10 ;
41
44
42
- Future <void > _onHeadlinesFeedFilterChanged (
43
- HeadlinesFeedFilterChanged event,
45
+ /// Handles the [HeadlinesFeedFiltersApplied] event.
46
+ ///
47
+ /// Emits [HeadlinesFeedLoading] state, then fetches the first page of
48
+ /// headlines using the filters provided in the event. Updates the state
49
+ /// with the new headlines and the applied filter. Emits [HeadlinesFeedError]
50
+ /// if fetching fails.
51
+ Future <void > _onHeadlinesFeedFiltersApplied (
52
+ HeadlinesFeedFiltersApplied event,
44
53
Emitter <HeadlinesFeedState > emit,
45
54
) async {
46
- emit (HeadlinesFeedLoading ());
55
+ emit (HeadlinesFeedLoading ()); // Show loading for filter application
47
56
try {
48
- // Use list-based filters from the event
49
57
final response = await _headlinesRepository.getHeadlines (
50
58
limit: _headlinesFetchLimit,
51
- categories: event.categories,
52
- sources: event.sources,
53
- eventCountries: event.eventCountries,
59
+ categories: event.filter. categories,
60
+ sources: event.filter. sources,
61
+ eventCountries: event.filter. eventCountries,
54
62
);
55
- final newFilter =
56
- (state is HeadlinesFeedLoaded )
57
- ? (state as HeadlinesFeedLoaded ).filter.copyWith (
58
- // Update copyWith call
59
- categories: event.categories,
60
- sources: event.sources,
61
- eventCountries: event.eventCountries,
62
- )
63
- : HeadlineFilter (
64
- // Update constructor call
65
- categories: event.categories,
66
- sources: event.sources,
67
- eventCountries: event.eventCountries,
68
- );
69
63
emit (
70
64
HeadlinesFeedLoaded (
71
65
headlines: response.items,
72
66
hasMore: response.hasMore,
73
67
cursor: response.cursor,
74
- filter: newFilter,
68
+ filter: event.filter, // Store the applied filter
75
69
),
76
70
);
77
71
} on HeadlinesFetchException catch (e) {
78
72
emit (HeadlinesFeedError (message: e.message));
79
- } catch (_) {
73
+ } catch (e, st) {
74
+ // Log the error and stack trace for unexpected errors
75
+ // Consider using a proper logging framework
76
+ print ('Unexpected error in _onHeadlinesFeedFiltersApplied: $e \n $st ' );
80
77
emit (const HeadlinesFeedError (message: 'An unexpected error occurred' ));
81
78
}
82
79
}
83
80
84
- /// Handles [HeadlinesFeedFetchRequested] events .
81
+ /// Handles clearing all applied filters .
85
82
///
86
- /// Fetches headlines from the repository and emits
87
- /// [HeadlinesFeedLoading] , and either [HeadlinesFeedLoaded] or
88
- /// [HeadlinesFeedError] states.
83
+ /// Fetches the first page of headlines without any filters.
84
+ Future <void > _onHeadlinesFeedFiltersCleared (
85
+ HeadlinesFeedFiltersCleared event,
86
+ Emitter <HeadlinesFeedState > emit,
87
+ ) async {
88
+ emit (HeadlinesFeedLoading ()); // Show loading indicator
89
+ try {
90
+ // Fetch the first page with no filters
91
+ final response = await _headlinesRepository.getHeadlines (
92
+ limit: _headlinesFetchLimit,
93
+ );
94
+ emit (
95
+ HeadlinesFeedLoaded (
96
+ headlines: response.items,
97
+ hasMore: response.hasMore,
98
+ cursor: response.cursor,
99
+ ),
100
+ );
101
+ } on HeadlinesFetchException catch (e) {
102
+ emit (HeadlinesFeedError (message: e.message));
103
+ } catch (e, st) {
104
+ // Log the error and stack trace for unexpected errors
105
+ print ('Unexpected error in _onHeadlinesFeedFiltersCleared: $e \n $st ' );
106
+ emit (const HeadlinesFeedError (message: 'An unexpected error occurred' ));
107
+ }
108
+ }
109
+
110
+ /// Handles the [HeadlinesFeedFetchRequested] event for initial load and pagination.
111
+ ///
112
+ /// Determines if it's an initial load or pagination based on the current state
113
+ /// and the presence of a cursor in the event. Fetches headlines using the
114
+ /// currently active filter stored in the state. Emits appropriate loading
115
+ /// states ([HeadlinesFeedLoading] or [HeadlinesFeedLoadingSilently] ) and
116
+ /// updates the state with fetched headlines or an error.
89
117
Future <void > _onHeadlinesFeedFetchRequested (
90
118
HeadlinesFeedFetchRequested event,
91
119
Emitter <HeadlinesFeedState > emit,
92
120
) async {
93
- if (state is HeadlinesFeedLoaded &&
94
- (state as HeadlinesFeedLoaded ).hasMore) {
95
- final currentState = state as HeadlinesFeedLoaded ;
96
- emit (HeadlinesFeedLoadingSilently ());
97
- try {
98
- // Use list-based filters from the current state's filter
99
- final response = await _headlinesRepository.getHeadlines (
100
- limit: _headlinesFetchLimit,
101
- startAfterId: currentState.cursor,
102
- categories: currentState.filter.categories,
103
- sources: currentState.filter.sources,
104
- eventCountries: currentState.filter.eventCountries,
105
- );
106
- emit (
107
- HeadlinesFeedLoaded (
108
- headlines: currentState.headlines + response.items,
109
- hasMore: response.hasMore,
110
- cursor: response.cursor,
111
- filter: currentState.filter,
112
- ),
113
- );
114
- } on HeadlinesFetchException catch (e) {
115
- emit (HeadlinesFeedError (message: e.message));
116
- } catch (_) {
117
- emit (const HeadlinesFeedError (message: 'An unexpected error occurred' ));
121
+ // Determine current filter and cursor based on state
122
+ var currentFilter = const HeadlineFilter ();
123
+ var currentCursor =
124
+ event.cursor; // Use event's cursor if provided (for pagination)
125
+ var currentHeadlines = < Headline > [];
126
+ var isPaginating = false ;
127
+
128
+ if (state is HeadlinesFeedLoaded ) {
129
+ final loadedState = state as HeadlinesFeedLoaded ;
130
+ currentFilter = loadedState.filter;
131
+ // Only use state's cursor if event's cursor is null (i.e., not explicit pagination request)
132
+ currentCursor ?? = loadedState.cursor;
133
+ currentHeadlines = loadedState.headlines;
134
+ // Check if we should paginate
135
+ isPaginating = event.cursor != null && loadedState.hasMore;
136
+ if (isPaginating && state is HeadlinesFeedLoadingSilently ) {
137
+ return ; // Avoid concurrent pagination
138
+ }
139
+ if (! loadedState.hasMore && event.cursor != null ) {
140
+ return ; // Don't fetch if no more items
118
141
}
142
+ } else if (state is HeadlinesFeedLoading ||
143
+ state is HeadlinesFeedLoadingSilently ) {
144
+ // Avoid concurrent fetches if already loading, unless it's explicit pagination
145
+ if (event.cursor == null ) return ;
146
+ }
147
+
148
+ // Emit appropriate loading state
149
+ if (isPaginating) {
150
+ emit (HeadlinesFeedLoadingSilently ());
119
151
} else {
152
+ // Initial load or load after error/clear
120
153
emit (HeadlinesFeedLoading ());
121
- try {
122
- // Use list-based filters from the current state's filter (if loaded)
123
- final response = await _headlinesRepository.getHeadlines (
124
- limit: _headlinesFetchLimit,
125
- categories:
126
- state is HeadlinesFeedLoaded
127
- ? (state as HeadlinesFeedLoaded ).filter.categories
128
- : null ,
129
- sources:
130
- state is HeadlinesFeedLoaded
131
- ? (state as HeadlinesFeedLoaded ).filter.sources
132
- : null ,
133
- eventCountries:
134
- state is HeadlinesFeedLoaded
135
- ? (state as HeadlinesFeedLoaded ).filter.eventCountries
136
- : null ,
137
- );
138
- emit (
139
- HeadlinesFeedLoaded (
140
- headlines: response.items,
141
- hasMore: response.hasMore,
142
- cursor: response.cursor,
143
- filter:
144
- state is HeadlinesFeedLoaded
145
- ? (state as HeadlinesFeedLoaded ).filter
146
- : const HeadlineFilter (),
147
- ),
148
- );
149
- } on HeadlinesFetchException catch (e) {
150
- emit (HeadlinesFeedError (message: e.message));
151
- } catch (_) {
152
- emit (const HeadlinesFeedError (message: 'An unexpected error occurred' ));
153
- }
154
+ currentHeadlines = []; // Reset headlines on non-pagination fetch
155
+ }
156
+
157
+ try {
158
+ final response = await _headlinesRepository.getHeadlines (
159
+ limit: _headlinesFetchLimit,
160
+ startAfterId: currentCursor, // Use determined cursor
161
+ categories: currentFilter.categories,
162
+ sources: currentFilter.sources,
163
+ eventCountries: currentFilter.eventCountries,
164
+ );
165
+ emit (
166
+ HeadlinesFeedLoaded (
167
+ // Append if paginating, otherwise replace
168
+ headlines:
169
+ isPaginating ? currentHeadlines + response.items : response.items,
170
+ hasMore: response.hasMore,
171
+ cursor: response.cursor,
172
+ filter: currentFilter, // Preserve the filter
173
+ ),
174
+ );
175
+ } on HeadlinesFetchException catch (e) {
176
+ emit (HeadlinesFeedError (message: e.message));
177
+ } catch (e, st) {
178
+ print ('Unexpected error in _onHeadlinesFeedFetchRequested: $e \n $st ' );
179
+ emit (const HeadlinesFeedError (message: 'An unexpected error occurred' ));
154
180
}
155
181
}
156
182
157
- /// Handles [HeadlinesFeedRefreshRequested] events.
183
+ /// Handles [HeadlinesFeedRefreshRequested] events for pull-to-refresh .
158
184
///
159
- /// Fetches headlines from the repository and emits
160
- /// [HeadlinesFeedLoading] , and either [HeadlinesFeedLoaded] or
161
- /// [HeadlinesFeedError] states.
162
- ///
163
- /// Uses `restartable` transformer to ensure that only the latest
164
- /// refresh request is processed.
185
+ /// Fetches the first page of headlines using the currently applied filter (if any).
186
+ /// Uses `restartable` transformer to ensure only the latest request is processed.
165
187
Future <void > _onHeadlinesFeedRefreshRequested (
166
188
HeadlinesFeedRefreshRequested event,
167
189
Emitter <HeadlinesFeedState > emit,
168
190
) async {
169
- emit (HeadlinesFeedLoading ());
191
+ emit (HeadlinesFeedLoading ()); // Show loading indicator for refresh
192
+
193
+ // Determine the filter currently applied in the state
194
+ var currentFilter = const HeadlineFilter ();
195
+ if (state is HeadlinesFeedLoaded ) {
196
+ currentFilter = (state as HeadlinesFeedLoaded ).filter;
197
+ }
198
+
170
199
try {
171
- // Use list-based filters from the current state's filter (if loaded)
200
+ // Fetch the first page using the current filter
172
201
final response = await _headlinesRepository.getHeadlines (
173
- limit: 20 , // Consider using _headlinesFetchLimit here too?
174
- categories:
175
- state is HeadlinesFeedLoaded
176
- ? (state as HeadlinesFeedLoaded ).filter.categories
177
- : null ,
178
- sources:
179
- state is HeadlinesFeedLoaded
180
- ? (state as HeadlinesFeedLoaded ).filter.sources
181
- : null ,
182
- eventCountries:
183
- state is HeadlinesFeedLoaded
184
- ? (state as HeadlinesFeedLoaded ).filter.eventCountries
185
- : null ,
202
+ limit: _headlinesFetchLimit,
203
+ categories: currentFilter.categories,
204
+ sources: currentFilter.sources,
205
+ eventCountries: currentFilter.eventCountries,
186
206
);
187
207
emit (
188
208
HeadlinesFeedLoaded (
189
- headlines: response.items,
209
+ headlines: response.items, // Replace headlines on refresh
190
210
hasMore: response.hasMore,
191
211
cursor: response.cursor,
192
- filter:
193
- state is HeadlinesFeedLoaded
194
- ? (state as HeadlinesFeedLoaded ).filter
195
- : const HeadlineFilter (),
212
+ filter: currentFilter, // Preserve the filter
196
213
),
197
214
);
198
215
} on HeadlinesFetchException catch (e) {
199
216
emit (HeadlinesFeedError (message: e.message));
200
- } catch (_) {
217
+ } catch (e, st) {
218
+ print ('Unexpected error in _onHeadlinesFeedRefreshRequested: $e \n $st ' );
201
219
emit (const HeadlinesFeedError (message: 'An unexpected error occurred' ));
202
220
}
203
221
}
0 commit comments