Skip to content

Commit e6dc07d

Browse files
committed
fix(bloc): correct props override in HeadlinesFilterEvent
Resolves an `invalid_override` error in `FilterSourceCriteriaChanged`. The `props` getter was returning a `List<Object?>` due to nullable properties, which is incompatible with the `List<Object>` required by the `Equatable` superclass. The fix adjusts the `props` implementation to correctly handle nullable properties, ensuring it returns a `List<Object>` and satisfies the type contract.
1 parent 000ac85 commit e6dc07d

File tree

5 files changed

+141
-87
lines changed

5 files changed

+141
-87
lines changed

lib/headlines-feed/bloc/headlines_filter_bloc.dart

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ class HeadlinesFilterBloc
3939
on<FilterSourceToggled>(_onFilterSourceToggled);
4040
on<FilterCountryToggled>(_onFilterCountryToggled);
4141
on<FilterSelectionsCleared>(_onFilterSelectionsCleared);
42+
on<FilterSourceCriteriaChanged>(_onFilterSourceCriteriaChanged);
4243
}
4344

4445
final DataRepository<Topic> _topicsRepository;
@@ -158,4 +159,20 @@ class HeadlinesFilterBloc
158159
),
159160
);
160161
}
162+
163+
/// Handles the [FilterSourceCriteriaChanged] event, updating the UI-only
164+
/// filter criteria for the source list.
165+
void _onFilterSourceCriteriaChanged(
166+
FilterSourceCriteriaChanged event,
167+
Emitter<HeadlinesFilterState> emit,
168+
) {
169+
emit(
170+
state.copyWith(
171+
selectedSourceHeadquarterCountries:
172+
event.selectedCountries ?? state.selectedSourceHeadquarterCountries,
173+
selectedSourceTypes:
174+
event.selectedSourceTypes ?? state.selectedSourceTypes,
175+
),
176+
);
177+
}
161178
}

lib/headlines-feed/bloc/headlines_filter_event.dart

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,3 +99,27 @@ final class FilterSelectionsCleared extends HeadlinesFilterEvent {
9999
/// {@macro filter_selections_cleared}
100100
const FilterSelectionsCleared();
101101
}
102+
103+
/// {@template filter_source_criteria_changed}
104+
/// Event triggered when the source filtering criteria (headquarters or types)
105+
/// are updated from the `SourceListFilterPage`.
106+
/// {@endtemplate}
107+
final class FilterSourceCriteriaChanged extends HeadlinesFilterEvent {
108+
/// {@macro filter_source_criteria_changed}
109+
const FilterSourceCriteriaChanged({
110+
this.selectedCountries,
111+
this.selectedSourceTypes,
112+
});
113+
114+
/// The updated set of selected headquarters countries.
115+
final Set<Country>? selectedCountries;
116+
117+
/// The updated set of selected source types.
118+
final Set<SourceType>? selectedSourceTypes;
119+
120+
@override
121+
List<Object> get props => [
122+
if (selectedCountries != null) selectedCountries!,
123+
if (selectedSourceTypes != null) selectedSourceTypes!,
124+
];
125+
}

lib/headlines-feed/bloc/headlines_filter_state.dart

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ final class HeadlinesFilterState extends Equatable {
3131
this.selectedTopics = const {},
3232
this.selectedSources = const {},
3333
this.selectedCountries = const {},
34+
this.selectedSourceHeadquarterCountries = const {},
35+
this.selectedSourceTypes = const {},
3436
this.error,
3537
});
3638

@@ -55,6 +57,14 @@ final class HeadlinesFilterState extends Equatable {
5557
/// The set of [Country] objects currently selected by the user.
5658
final Set<Country> selectedCountries;
5759

60+
/// The set of [Country] objects selected for filtering the source list
61+
/// by headquarters.
62+
final Set<Country> selectedSourceHeadquarterCountries;
63+
64+
/// The set of [SourceType] objects selected for filtering the source list
65+
/// by type.
66+
final Set<SourceType> selectedSourceTypes;
67+
5868
/// An optional error object if the status is [HeadlinesFilterStatus.failure].
5969
final HttpException? error;
6070

@@ -67,6 +77,8 @@ final class HeadlinesFilterState extends Equatable {
6777
Set<Topic>? selectedTopics,
6878
Set<Source>? selectedSources,
6979
Set<Country>? selectedCountries,
80+
Set<Country>? selectedSourceHeadquarterCountries,
81+
Set<SourceType>? selectedSourceTypes,
7082
HttpException? error,
7183
bool clearError = false,
7284
}) {
@@ -78,6 +90,10 @@ final class HeadlinesFilterState extends Equatable {
7890
selectedTopics: selectedTopics ?? this.selectedTopics,
7991
selectedSources: selectedSources ?? this.selectedSources,
8092
selectedCountries: selectedCountries ?? this.selectedCountries,
93+
selectedSourceHeadquarterCountries:
94+
selectedSourceHeadquarterCountries ??
95+
this.selectedSourceHeadquarterCountries,
96+
selectedSourceTypes: selectedSourceTypes ?? this.selectedSourceTypes,
8197
error: clearError ? null : error ?? this.error,
8298
);
8399
}
@@ -91,6 +107,8 @@ final class HeadlinesFilterState extends Equatable {
91107
selectedTopics,
92108
selectedSources,
93109
selectedCountries,
110+
selectedSourceHeadquarterCountries,
111+
selectedSourceTypes,
94112
error,
95113
];
96114
}

lib/headlines-feed/view/source_filter_page.dart

Lines changed: 80 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -45,11 +45,73 @@ class _SourceFilterView extends StatefulWidget {
4545
}
4646

4747
class _SourceFilterViewState extends State<_SourceFilterView> {
48-
// Local state to hold the filter criteria for the source list.
49-
// These are managed by the dedicated SourceListFilterPage and are used
50-
// only for filtering the UI in this page, not for the final headline query.
51-
Set<Country> _filteredHeadquarterCountries = {};
52-
Set<SourceType> _filteredSourceTypes = {};
48+
Widget _buildSourcesList(
49+
BuildContext context,
50+
HeadlinesFilterState filterState,
51+
AppLocalizations l10n,
52+
TextTheme textTheme,
53+
List<Source> displayableSources,
54+
) {
55+
if (filterState.status == HeadlinesFilterStatus.loading &&
56+
displayableSources.isEmpty) {
57+
return const Center(child: CircularProgressIndicator());
58+
}
59+
if (filterState.status == HeadlinesFilterStatus.failure &&
60+
displayableSources.isEmpty) {
61+
return FailureStateWidget(
62+
exception:
63+
filterState.error ??
64+
const UnknownException('Failed to load displayable sources.'),
65+
onRetry: () {
66+
context.read<HeadlinesFilterBloc>().add(
67+
FilterDataLoaded(
68+
initialSelectedTopics: filterState.selectedTopics.toList(),
69+
initialSelectedSources: filterState.selectedSources.toList(),
70+
initialSelectedCountries: filterState.selectedCountries.toList(),
71+
),
72+
);
73+
},
74+
);
75+
}
76+
if (displayableSources.isEmpty &&
77+
filterState.status != HeadlinesFilterStatus.loading) {
78+
return Center(
79+
child: Padding(
80+
padding: const EdgeInsets.all(AppSpacing.paddingLarge),
81+
child: Text(
82+
l10n.headlinesFeedFilterNoSourcesMatch,
83+
style: textTheme.bodyLarge,
84+
textAlign: TextAlign.center,
85+
),
86+
),
87+
);
88+
}
89+
90+
return ListView.builder(
91+
padding: const EdgeInsets.symmetric(
92+
vertical: AppSpacing.paddingSmall,
93+
).copyWith(bottom: AppSpacing.xxl),
94+
itemCount: displayableSources.length,
95+
itemBuilder: (context, index) {
96+
final source = displayableSources[index];
97+
return CheckboxListTile(
98+
title: Text(source.name, style: textTheme.titleMedium),
99+
value: filterState.selectedSources.contains(source),
100+
onChanged: (bool? value) {
101+
if (value != null) {
102+
context.read<HeadlinesFilterBloc>().add(
103+
FilterSourceToggled(source: source, isSelected: value),
104+
);
105+
}
106+
},
107+
controlAffinity: ListTileControlAffinity.leading,
108+
contentPadding: const EdgeInsets.symmetric(
109+
horizontal: AppSpacing.paddingMedium,
110+
),
111+
);
112+
},
113+
);
114+
}
53115

54116
@override
55117
Widget build(BuildContext context) {
@@ -81,19 +143,20 @@ class _SourceFilterViewState extends State<_SourceFilterView> {
81143
.toList()
82144
..sort((a, b) => a.name.compareTo(b.name)),
83145
'initialSelectedHeadquarterCountries':
84-
_filteredHeadquarterCountries,
85-
'initialSelectedSourceTypes': _filteredSourceTypes,
146+
filterState.selectedSourceHeadquarterCountries,
147+
'initialSelectedSourceTypes': filterState.selectedSourceTypes,
86148
},
87149
);
88150

89151
// When the filter page returns with new criteria, update the
90-
// local state to re-render the list.
152+
// bloc to re-render the list.
91153
if (result != null && mounted) {
92-
setState(() {
93-
_filteredHeadquarterCountries =
94-
result['countries'] as Set<Country>;
95-
_filteredSourceTypes = result['types'] as Set<SourceType>;
96-
});
154+
context.read<HeadlinesFilterBloc>().add(
155+
FilterSourceCriteriaChanged(
156+
selectedCountries: result['countries'] as Set<Country>,
157+
selectedSourceTypes: result['types'] as Set<SourceType>,
158+
),
159+
);
97160
}
98161
},
99162
),
@@ -145,15 +208,15 @@ class _SourceFilterViewState extends State<_SourceFilterView> {
145208
final displayableSources = filterState.allSources.where((source) {
146209
// Filter by headquarters country.
147210
final matchesCountry =
148-
_filteredHeadquarterCountries.isEmpty ||
149-
_filteredHeadquarterCountries.any(
211+
filterState.selectedSourceHeadquarterCountries.isEmpty ||
212+
filterState.selectedSourceHeadquarterCountries.any(
150213
(c) => c.isoCode == source.headquarters.isoCode,
151214
);
152215

153216
// Filter by source type.
154217
final matchesType =
155-
_filteredSourceTypes.isEmpty ||
156-
_filteredSourceTypes.contains(source.sourceType);
218+
filterState.selectedSourceTypes.isEmpty ||
219+
filterState.selectedSourceTypes.contains(source.sourceType);
157220
return matchesCountry && matchesType;
158221
}).toList();
159222

@@ -183,72 +246,4 @@ class _SourceFilterViewState extends State<_SourceFilterView> {
183246
),
184247
);
185248
}
186-
187-
Widget _buildSourcesList(
188-
BuildContext context,
189-
HeadlinesFilterState filterState,
190-
AppLocalizations l10n,
191-
TextTheme textTheme,
192-
List<Source> displayableSources,
193-
) {
194-
if (filterState.status == HeadlinesFilterStatus.loading &&
195-
displayableSources.isEmpty) {
196-
return const Center(child: CircularProgressIndicator());
197-
}
198-
if (filterState.status == HeadlinesFilterStatus.failure &&
199-
displayableSources.isEmpty) {
200-
return FailureStateWidget(
201-
exception:
202-
filterState.error ??
203-
const UnknownException('Failed to load displayable sources.'),
204-
onRetry: () {
205-
context.read<HeadlinesFilterBloc>().add(
206-
FilterDataLoaded(
207-
initialSelectedTopics: filterState.selectedTopics.toList(),
208-
initialSelectedSources: filterState.selectedSources.toList(),
209-
initialSelectedCountries: filterState.selectedCountries.toList(),
210-
),
211-
);
212-
},
213-
);
214-
}
215-
if (displayableSources.isEmpty &&
216-
filterState.status != HeadlinesFilterStatus.loading) {
217-
return Center(
218-
child: Padding(
219-
padding: const EdgeInsets.all(AppSpacing.paddingLarge),
220-
child: Text(
221-
l10n.headlinesFeedFilterNoSourcesMatch,
222-
style: textTheme.bodyLarge,
223-
textAlign: TextAlign.center,
224-
),
225-
),
226-
);
227-
}
228-
229-
return ListView.builder(
230-
padding: const EdgeInsets.symmetric(
231-
vertical: AppSpacing.paddingSmall,
232-
).copyWith(bottom: AppSpacing.xxl),
233-
itemCount: displayableSources.length,
234-
itemBuilder: (context, index) {
235-
final source = displayableSources[index];
236-
return CheckboxListTile(
237-
title: Text(source.name, style: textTheme.titleMedium),
238-
value: filterState.selectedSources.contains(source),
239-
onChanged: (bool? value) {
240-
if (value != null) {
241-
context.read<HeadlinesFilterBloc>().add(
242-
FilterSourceToggled(source: source, isSelected: value),
243-
);
244-
}
245-
},
246-
controlAffinity: ListTileControlAffinity.leading,
247-
contentPadding: const EdgeInsets.symmetric(
248-
horizontal: AppSpacing.paddingMedium,
249-
),
250-
);
251-
},
252-
);
253-
}
254249
}

lib/headlines-feed/view/source_list_filter_page.dart

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,8 @@ class SourceListFilterPage extends StatefulWidget {
4141
}
4242

4343
class _SourceListFilterPageState extends State<SourceListFilterPage> {
44-
late final Set<Country> _selectedHeadquarterCountries;
45-
late final Set<SourceType> _selectedSourceTypes;
44+
late Set<Country> _selectedHeadquarterCountries;
45+
late Set<SourceType> _selectedSourceTypes;
4646

4747
@override
4848
void initState() {

0 commit comments

Comments
 (0)