Skip to content

Commit 6cb7032

Browse files
authored
Merge pull request #94 from flutter-news-app-full-source-code/fix-headlines-feed
Fix headlines feed
2 parents b03cce3 + ba9507a commit 6cb7032

File tree

9 files changed

+63
-138
lines changed

9 files changed

+63
-138
lines changed

lib/headlines-feed/bloc/headlines_feed_bloc.dart

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -86,11 +86,10 @@ class HeadlinesFeedBloc extends Bloc<HeadlinesFeedEvent, HeadlinesFeedState> {
8686
r'$in': filter.eventCountries!.map((c) => c.id).toList(),
8787
};
8888
}
89-
if (filter.sourceCountries?.isNotEmpty ?? false) {
90-
queryFilter['source.headquarters.id'] = {
91-
r'$in': filter.sourceCountries!.map((c) => c.id).toList(),
92-
};
93-
}
89+
// Note: The `selectedSourceCountryIsoCodes` and `selectedSourceSourceTypes`
90+
// fields are used exclusively for UI-side filtering on the `SourceFilterPage`
91+
// and are not included in the backend query for headlines. Source filtering
92+
// is performed solely by `source.id` when specific sources are selected.
9493
return queryFilter;
9594
}
9695

lib/headlines-feed/bloc/sources_filter_bloc.dart

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -42,16 +42,17 @@ class SourcesFilterBloc extends Bloc<SourcesFilterEvent, SourcesFilterState> {
4242
.map((s) => s.id)
4343
.toSet();
4444

45-
// Use the passed-in initial capsule selections directly
46-
final initialSelectedCountryIsoCodes =
47-
event.initialSelectedCountryIsoCodes;
48-
final initialSelectedSourceTypes = event.initialSelectedSourceTypes;
45+
// The initial country and source type capsule selections are ephemeral
46+
// to the UI of the SourceFilterPage and are not passed via the event.
47+
// They are initialized as empty sets here, meaning the filter starts
48+
// with all countries and source types selected by default in the UI.
49+
final initialSelectedCountryIsoCodes = <String>{};
50+
final initialSelectedSourceTypes = <SourceType>{};
4951

5052
final allAvailableSources = (await _sourcesRepository.readAll()).items;
5153

5254
// Initially, display all sources. Capsules are visually set but don't filter the list yet.
5355
// Filtering will occur if a capsule is manually toggled.
54-
// However, if initial capsule filters ARE provided, we should respect them for the initial display.
5556
final displayableSources = _getFilteredSources(
5657
allSources: allAvailableSources,
5758
selectedCountries: initialSelectedCountryIsoCodes,

lib/headlines-feed/bloc/sources_filter_event.dart

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,22 +9,27 @@ abstract class SourcesFilterEvent extends Equatable {
99
List<Object?> get props => [];
1010
}
1111

12+
/// {@template load_source_filter_data}
13+
/// Event triggered to load the initial data for the source filter page.
14+
///
15+
/// This event is dispatched when the `SourceFilterPage` is initialized.
16+
/// It fetches all available countries and sources, and initializes the
17+
/// internal state with any `initialSelectedSources` passed from the
18+
/// `HeadlinesFilterPage`. The country and source type capsule selections
19+
/// are ephemeral to the `SourceFilterPage` and are not passed via this event.
20+
/// {@endtemplate}
1221
class LoadSourceFilterData extends SourcesFilterEvent {
22+
/// {@macro load_source_filter_data}
1323
const LoadSourceFilterData({
1424
this.initialSelectedSources = const [],
15-
this.initialSelectedCountryIsoCodes = const {},
16-
this.initialSelectedSourceTypes = const {},
1725
});
1826

27+
/// The list of sources that were initially selected on the previous page.
1928
final List<Source> initialSelectedSources;
20-
final Set<String> initialSelectedCountryIsoCodes;
21-
final Set<SourceType> initialSelectedSourceTypes;
2229

2330
@override
2431
List<Object?> get props => [
2532
initialSelectedSources,
26-
initialSelectedCountryIsoCodes,
27-
initialSelectedSourceTypes,
2833
];
2934
}
3035

lib/headlines-feed/models/headline_filter.dart

Lines changed: 12 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -9,43 +9,39 @@ class HeadlineFilter extends Equatable {
99
const HeadlineFilter({
1010
this.topics,
1111
this.sources,
12-
this.selectedSourceCountryIsoCodes,
13-
this.selectedSourceSourceTypes,
1412
this.eventCountries,
15-
this.sourceCountries,
1613
this.isFromFollowedItems = false,
1714
});
1815

1916
/// The list of selected topics to filter headlines by.
2017
final List<Topic>? topics;
2118

2219
/// The list of selected sources to filter headlines by.
20+
///
21+
/// Note: The `SourceFilterPage` uses internal UI state (country and source
22+
/// type capsules) to refine the list of sources presented to the user.
23+
/// However, only the *explicitly selected* sources from that refined list
24+
/// are passed back and stored here. The country and source type selections
25+
/// themselves are *not* part of this filter model, as they are purely for
26+
/// UI-side filtering on the `SourceFilterPage` and should not affect the
27+
/// backend query for headlines.
2328
final List<Source>? sources;
2429

25-
/// The set of ISO codes for countries selected to filter sources by their
26-
/// headquarters.
27-
final Set<String>? selectedSourceCountryIsoCodes;
28-
29-
/// The set of source types selected to filter sources by.
30-
final Set<SourceType>? selectedSourceSourceTypes;
31-
3230
/// The list of selected event countries to filter headlines by.
3331
final List<Country>? eventCountries;
3432

35-
/// The list of selected source headquarters countries to filter headlines by.
36-
final List<Country>? sourceCountries;
37-
3833
/// Whether the filter is based on the user's followed items.
34+
///
35+
/// When `true`, the `topics` and `sources` fields will be populated based
36+
/// on the user's followed items, and manual selections for these categories
37+
/// will be ignored.
3938
final bool isFromFollowedItems;
4039

4140
@override
4241
List<Object?> get props => [
4342
topics,
4443
sources,
45-
selectedSourceCountryIsoCodes,
46-
selectedSourceSourceTypes,
4744
eventCountries,
48-
sourceCountries,
4945
isFromFollowedItems,
5046
];
5147

@@ -54,21 +50,13 @@ class HeadlineFilter extends Equatable {
5450
HeadlineFilter copyWith({
5551
List<Topic>? topics,
5652
List<Source>? sources,
57-
Set<String>? selectedSourceCountryIsoCodes,
58-
Set<SourceType>? selectedSourceSourceTypes,
5953
List<Country>? eventCountries,
60-
List<Country>? sourceCountries,
6154
bool? isFromFollowedItems,
6255
}) {
6356
return HeadlineFilter(
6457
topics: topics ?? this.topics,
6558
sources: sources ?? this.sources,
66-
selectedSourceCountryIsoCodes:
67-
selectedSourceCountryIsoCodes ?? this.selectedSourceCountryIsoCodes,
68-
selectedSourceSourceTypes:
69-
selectedSourceSourceTypes ?? this.selectedSourceSourceTypes,
7059
eventCountries: eventCountries ?? this.eventCountries,
71-
sourceCountries: sourceCountries ?? this.sourceCountries,
7260
isFromFollowedItems: isFromFollowedItems ?? this.isFromFollowedItems,
7361
);
7462
}

lib/headlines-feed/view/headlines_feed_page.dart

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,6 @@ class _HeadlinesFeedPageState extends State<HeadlinesFeedPage> {
140140
(state.filter.topics?.isNotEmpty ?? false) ||
141141
(state.filter.sources?.isNotEmpty ?? false) ||
142142
(state.filter.eventCountries?.isNotEmpty ?? false) ||
143-
(state.filter.sourceCountries?.isNotEmpty ?? false) ||
144143
state.filter.isFromFollowedItems;
145144
return Stack(
146145
children: [
@@ -205,7 +204,10 @@ class _HeadlinesFeedPageState extends State<HeadlinesFeedPage> {
205204
if (state.status == HeadlinesFeedStatus.failure &&
206205
state.feedItems.isEmpty) {
207206
return FailureStateWidget(
208-
exception: state.error!,
207+
//TODO(fulleni): l10n.
208+
exception:
209+
state.error ??
210+
const UnknownException('Failed to load headlines feed.'),
209211
onRetry: () => context.read<HeadlinesFeedBloc>().add(
210212
HeadlinesFeedRefreshRequested(
211213
adThemeStyle: AdThemeStyle.fromTheme(Theme.of(context)),

lib/headlines-feed/view/headlines_filter_page.dart

Lines changed: 11 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,6 @@ import 'package:ui_kit/ui_kit.dart';
1616

1717
// Keys for passing data to/from SourceFilterPage
1818
const String keySelectedSources = 'selectedSources';
19-
const String keySelectedCountryIsoCodes = 'selectedCountryIsoCodes';
20-
const String keySelectedSourceTypes = 'selectedSourceTypes';
2119

2220
/// {@template headlines_filter_page}
2321
/// A full-screen dialog page for selecting headline filters.
@@ -42,10 +40,7 @@ class _HeadlinesFilterPageState extends State<HeadlinesFilterPage> {
4240
/// and are only applied back to the BLoC when the user taps 'Apply'.
4341
late List<Topic> _tempSelectedTopics;
4442
late List<Source> _tempSelectedSources;
45-
late Set<String> _tempSelectedSourceCountryIsoCodes;
46-
late Set<SourceType> _tempSelectedSourceSourceTypes;
4743
late List<Country> _tempSelectedEventCountries;
48-
late List<Country> _tempSelectedSourceCountries;
4944

5045
// New state variables for the "Apply my followed items" feature
5146
bool _useFollowedFilters = false;
@@ -63,16 +58,7 @@ class _HeadlinesFilterPageState extends State<HeadlinesFilterPage> {
6358
final currentFilter = headlinesFeedState.filter;
6459
_tempSelectedTopics = List.from(currentFilter.topics ?? []);
6560
_tempSelectedSources = List.from(currentFilter.sources ?? []);
66-
_tempSelectedSourceCountryIsoCodes = Set.from(
67-
currentFilter.selectedSourceCountryIsoCodes ?? {},
68-
);
69-
_tempSelectedSourceSourceTypes = Set.from(
70-
currentFilter.selectedSourceSourceTypes ?? {},
71-
);
7261
_tempSelectedEventCountries = List.from(currentFilter.eventCountries ?? []);
73-
_tempSelectedSourceCountries = List.from(
74-
currentFilter.sourceCountries ?? [],
75-
);
7662

7763
_useFollowedFilters = currentFilter.isFromFollowedItems;
7864
_isLoadingFollowedFilters = false;
@@ -129,7 +115,6 @@ class _HeadlinesFilterPageState extends State<HeadlinesFilterPage> {
129115
_tempSelectedTopics = [];
130116
_tempSelectedSources = [];
131117
_tempSelectedEventCountries = [];
132-
_tempSelectedSourceCountries = [];
133118
});
134119
if (mounted) {
135120
ScaffoldMessenger.of(context)
@@ -168,7 +153,6 @@ class _HeadlinesFilterPageState extends State<HeadlinesFilterPage> {
168153
_tempSelectedTopics = [];
169154
_tempSelectedSources = [];
170155
_tempSelectedEventCountries = [];
171-
_tempSelectedSourceCountries = [];
172156
_isLoadingFollowedFilters = false;
173157
_useFollowedFilters = false;
174158
});
@@ -208,10 +192,6 @@ class _HeadlinesFilterPageState extends State<HeadlinesFilterPage> {
208192
_tempSelectedTopics = [];
209193
_tempSelectedSources = [];
210194
_tempSelectedEventCountries = [];
211-
_tempSelectedSourceCountries = [];
212-
// Keep source country/type filters as they are not part of this quick filter
213-
// _tempSelectedSourceCountryIsoCodes = {};
214-
// _tempSelectedSourceSourceTypes = {};
215195
});
216196
}
217197

@@ -226,14 +206,13 @@ class _HeadlinesFilterPageState extends State<HeadlinesFilterPage> {
226206
/// (e.g., `SourceFilterPage`) via the `extra` parameter of `context.pushNamed`.
227207
/// Updates the temporary state via the [onResult] callback when the
228208
/// criterion page pops with a result (the user tapped 'Apply' on that page).
229-
Widget _buildFilterTile({
209+
Widget _buildFilterTile<T>({
230210
required BuildContext context,
231211
required String title,
232212
required int selectedCount,
233213
required String routeName,
234-
// For sources, currentSelection will be a Map
235-
required dynamic currentSelectionData,
236-
required void Function(dynamic)? onResult,
214+
required List<T> currentSelectionData,
215+
required void Function(List<T>)? onResult,
237216
bool enabled = true,
238217
}) {
239218
final l10n = AppLocalizationsX(context).l10n;
@@ -252,7 +231,7 @@ class _HeadlinesFilterPageState extends State<HeadlinesFilterPage> {
252231
onTap:
253232
enabled // Only allow tap if enabled
254233
? () async {
255-
final result = await context.pushNamed<dynamic>(
234+
final result = await context.pushNamed<List<T>>(
256235
routeName,
257236
extra: currentSelectionData,
258237
);
@@ -308,20 +287,9 @@ class _HeadlinesFilterPageState extends State<HeadlinesFilterPage> {
308287
sources: _tempSelectedSources.isNotEmpty
309288
? _tempSelectedSources
310289
: null,
311-
selectedSourceCountryIsoCodes:
312-
_tempSelectedSourceCountryIsoCodes.isNotEmpty
313-
? _tempSelectedSourceCountryIsoCodes
314-
: null,
315-
selectedSourceSourceTypes:
316-
_tempSelectedSourceSourceTypes.isNotEmpty
317-
? _tempSelectedSourceSourceTypes
318-
: null,
319290
eventCountries: _tempSelectedEventCountries.isNotEmpty
320291
? _tempSelectedEventCountries
321292
: null,
322-
sourceCountries: _tempSelectedSourceCountries.isNotEmpty
323-
? _tempSelectedSourceCountries
324-
: null,
325293
isFromFollowedItems: _useFollowedFilters,
326294
);
327295
context.read<HeadlinesFeedBloc>().add(
@@ -379,54 +347,37 @@ class _HeadlinesFilterPageState extends State<HeadlinesFilterPage> {
379347
),
380348
),
381349
const Divider(),
382-
_buildFilterTile(
350+
_buildFilterTile<Topic>(
383351
context: context,
384352
title: l10n.headlinesFeedFilterTopicLabel,
385353
enabled: !_useFollowedFilters && !_isLoadingFollowedFilters,
386354
selectedCount: _tempSelectedTopics.length,
387355
routeName: Routes.feedFilterTopicsName,
388356
currentSelectionData: _tempSelectedTopics,
389357
onResult: (result) {
390-
if (result is List<Topic>) {
391-
setState(() => _tempSelectedTopics = result);
392-
}
358+
setState(() => _tempSelectedTopics = result);
393359
},
394360
),
395-
_buildFilterTile(
361+
_buildFilterTile<Source>(
396362
context: context,
397363
title: l10n.headlinesFeedFilterSourceLabel,
398364
enabled: !_useFollowedFilters && !_isLoadingFollowedFilters,
399365
selectedCount: _tempSelectedSources.length,
400366
routeName: Routes.feedFilterSourcesName,
401-
currentSelectionData: {
402-
keySelectedSources: _tempSelectedSources,
403-
keySelectedCountryIsoCodes: _tempSelectedSourceCountryIsoCodes,
404-
keySelectedSourceTypes: _tempSelectedSourceSourceTypes,
405-
},
367+
currentSelectionData: _tempSelectedSources,
406368
onResult: (result) {
407-
if (result is Map<String, dynamic>) {
408-
setState(() {
409-
_tempSelectedSources =
410-
result[keySelectedSources] as List<Source>? ?? [];
411-
_tempSelectedSourceCountryIsoCodes =
412-
result[keySelectedCountryIsoCodes] as Set<String>? ?? {};
413-
_tempSelectedSourceSourceTypes =
414-
result[keySelectedSourceTypes] as Set<SourceType>? ?? {};
415-
});
416-
}
369+
setState(() => _tempSelectedSources = result);
417370
},
418371
),
419-
_buildFilterTile(
372+
_buildFilterTile<Country>(
420373
context: context,
421374
title: l10n.headlinesFeedFilterEventCountryLabel,
422375
enabled: !_useFollowedFilters && !_isLoadingFollowedFilters,
423376
selectedCount: _tempSelectedEventCountries.length,
424377
routeName: Routes.feedFilterEventCountriesName,
425378
currentSelectionData: _tempSelectedEventCountries,
426379
onResult: (result) {
427-
if (result is List<Country>) {
428-
setState(() => _tempSelectedEventCountries = result);
429-
}
380+
setState(() => _tempSelectedEventCountries = result);
430381
},
431382
),
432383
],

0 commit comments

Comments
 (0)