Skip to content

Commit fdb3b10

Browse files
authored
Merge pull request #96 from flutter-news-app-full-source-code/sync-country-data-route
Sync country data route
2 parents 934e501 + b1f338a commit fdb3b10

File tree

12 files changed

+117
-40
lines changed

12 files changed

+117
-40
lines changed

lib/headlines-feed/bloc/countries_filter_bloc.dart

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@ import 'package:bloc_concurrency/bloc_concurrency.dart';
55
import 'package:core/core.dart';
66
import 'package:data_repository/data_repository.dart';
77
import 'package:equatable/equatable.dart';
8-
import 'package:flutter_news_app_mobile_client_full_source_code/app/bloc/app_bloc.dart'; // Import AppBloc
8+
import 'package:flutter_news_app_mobile_client_full_source_code/app/bloc/app_bloc.dart';
9+
import 'package:flutter_news_app_mobile_client_full_source_code/headlines-feed/view/country_filter_page.dart'; // Import AppBloc
910

1011
part 'countries_filter_event.dart';
1112
part 'countries_filter_state.dart';
@@ -48,7 +49,7 @@ class CountriesFilterBloc
4849
/// Handles the request to fetch countries based on a specific usage.
4950
///
5051
/// This method fetches a non-paginated list of countries, filtered by
51-
/// the provided [usage] (e.g., 'eventCountry', 'headquarters').
52+
/// the provided [usage] (e.g., 'hasActiveSources', 'hasActiveHeadlines').
5253
Future<void> _onCountriesFilterRequested(
5354
CountriesFilterRequested event,
5455
Emitter<CountriesFilterState> emit,
@@ -65,7 +66,7 @@ class CountriesFilterBloc
6566
try {
6667
// Build the filter map based on the provided usage.
6768
final filter = event.usage != null
68-
? <String, dynamic>{'usage': event.usage}
69+
? <String, dynamic>{event.usage!.name: true}
6970
: null;
7071

7172
// Fetch countries. The API for 'usage' filters is not paginated,

lib/headlines-feed/bloc/countries_filter_event.dart

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,11 @@ final class CountriesFilterRequested extends CountriesFilterEvent {
1818
/// {@macro countries_filter_requested}
1919
///
2020
/// Optionally includes a [usage] context to filter countries by their
21-
/// relevance to headlines (e.g., 'eventCountry' or 'headquarters').
21+
/// relevance to headlines (e.g., 'hasActiveSources' or 'hasActiveHeadlines').
2222
const CountriesFilterRequested({this.usage});
2323

24-
/// The usage context for filtering countries (e.g., 'eventCountry', 'headquarters').
25-
final String? usage;
24+
/// The usage context for filtering countries (e.g., 'hasActiveSources', 'hasActiveHeadlines').
25+
final CountryFilterUsage? usage;
2626

2727
@override
2828
List<Object?> get props => [usage];

lib/headlines-feed/bloc/sources_filter_bloc.dart

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,8 @@ class SourcesFilterBloc extends Bloc<SourcesFilterEvent, SourcesFilterState> {
4545
state.copyWith(dataLoadingStatus: SourceFilterDataLoadingStatus.loading),
4646
);
4747
try {
48-
final availableCountries = (await _countriesRepository.readAll(
49-
filter: {'usage': 'headquarters'},
48+
final countriesWithActiveSources = (await _countriesRepository.readAll(
49+
filter: {'hasActiveSources': true},
5050
)).items;
5151
final initialSelectedSourceIds = event.initialSelectedSources
5252
.map((s) => s.id)
@@ -71,7 +71,7 @@ class SourcesFilterBloc extends Bloc<SourcesFilterEvent, SourcesFilterState> {
7171

7272
emit(
7373
state.copyWith(
74-
availableCountries: availableCountries,
74+
countriesWithActiveSources: countriesWithActiveSources,
7575
allAvailableSources: allAvailableSources,
7676
displayableSources: displayableSources,
7777
finallySelectedSourceIds: initialSelectedSourceIds,

lib/headlines-feed/bloc/sources_filter_state.dart

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ enum SourceFilterDataLoadingStatus { initial, loading, success, failure }
66

77
class SourcesFilterState extends Equatable {
88
const SourcesFilterState({
9-
this.availableCountries = const [],
9+
this.countriesWithActiveSources = const [],
1010
this.selectedCountryIsoCodes = const {},
1111
this.availableSourceTypes = SourceType.values,
1212
this.selectedSourceTypes = const {},
@@ -19,7 +19,7 @@ class SourcesFilterState extends Equatable {
1919
this.followedSources = const [],
2020
});
2121

22-
final List<Country> availableCountries;
22+
final List<Country> countriesWithActiveSources;
2323
final Set<String> selectedCountryIsoCodes;
2424
final List<SourceType> availableSourceTypes;
2525
final Set<SourceType> selectedSourceTypes;
@@ -36,7 +36,7 @@ class SourcesFilterState extends Equatable {
3636
final List<Source> followedSources;
3737

3838
SourcesFilterState copyWith({
39-
List<Country>? availableCountries,
39+
List<Country>? countriesWithActiveSources,
4040
Set<String>? selectedCountryIsoCodes,
4141
List<SourceType>? availableSourceTypes,
4242
Set<SourceType>? selectedSourceTypes,
@@ -51,7 +51,8 @@ class SourcesFilterState extends Equatable {
5151
bool clearFollowedSourcesError = false,
5252
}) {
5353
return SourcesFilterState(
54-
availableCountries: availableCountries ?? this.availableCountries,
54+
countriesWithActiveSources:
55+
countriesWithActiveSources ?? this.countriesWithActiveSources,
5556
selectedCountryIsoCodes:
5657
selectedCountryIsoCodes ?? this.selectedCountryIsoCodes,
5758
availableSourceTypes: availableSourceTypes ?? this.availableSourceTypes,
@@ -70,7 +71,7 @@ class SourcesFilterState extends Equatable {
7071

7172
@override
7273
List<Object?> get props => [
73-
availableCountries,
74+
countriesWithActiveSources,
7475
selectedCountryIsoCodes,
7576
availableSourceTypes,
7677
selectedSourceTypes,

lib/headlines-feed/view/country_filter_page.dart

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,14 @@ import 'package:flutter_news_app_mobile_client_full_source_code/l10n/l10n.dart';
66
import 'package:go_router/go_router.dart';
77
import 'package:ui_kit/ui_kit.dart';
88

9+
enum CountryFilterUsage {
10+
// countries with active sources
11+
hasActiveSources,
12+
13+
// countries with active headlines
14+
hasActiveHeadlines,
15+
}
16+
917
/// {@template country_filter_page}
1018
/// A page dedicated to selecting event countries for filtering headlines.
1119
///
@@ -15,14 +23,14 @@ import 'package:ui_kit/ui_kit.dart';
1523
/// {@endtemplate}
1624
class CountryFilterPage extends StatefulWidget {
1725
/// {@macro country_filter_page}
18-
const CountryFilterPage({required this.title, this.usage, super.key});
26+
const CountryFilterPage({required this.title, this.filter, super.key});
1927

2028
/// The title to display in the app bar for this filter page.
2129
final String title;
2230

23-
/// The usage context for filtering countries (e.g., 'eventCountry', 'headquarters').
31+
/// The usage context for filtering countries (e.g., 'hasActiveSources', 'hasActiveHeadlines').
2432
/// If null, fetches all countries (though this is not the primary use case for this page).
25-
final String? usage;
33+
final CountryFilterUsage? filter;
2634

2735
@override
2836
State<CountryFilterPage> createState() => _CountryFilterPageState();
@@ -65,7 +73,7 @@ class _CountryFilterPageState extends State<CountryFilterPage> {
6573
// 3. Trigger the page-specific BLoC (CountriesFilterBloc) to start
6674
// fetching the list of *all available* countries that the user can
6775
// potentially select from, using the specified usage filter.
68-
_countriesFilterBloc.add(CountriesFilterRequested(usage: widget.usage));
76+
_countriesFilterBloc.add(CountriesFilterRequested(usage: widget.filter));
6977
});
7078
}
7179

@@ -193,7 +201,7 @@ class _CountryFilterPageState extends State<CountryFilterPage> {
193201
exception:
194202
state.error ?? const UnknownException('Unknown error'),
195203
onRetry: () => _countriesFilterBloc.add(
196-
CountriesFilterRequested(usage: widget.usage),
204+
CountriesFilterRequested(usage: widget.filter),
197205
),
198206
);
199207
}

lib/headlines-feed/view/source_filter_page.dart

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -253,7 +253,7 @@ class _SourceFilterView extends StatelessWidget {
253253
height: AppSpacing.xl + AppSpacing.md,
254254
child: ListView.separated(
255255
scrollDirection: Axis.horizontal,
256-
itemCount: state.availableCountries.length + 1,
256+
itemCount: state.countriesWithActiveSources.length + 1,
257257
separatorBuilder: (context, index) =>
258258
const SizedBox(width: AppSpacing.sm),
259259
itemBuilder: (context, index) {
@@ -269,7 +269,7 @@ class _SourceFilterView extends StatelessWidget {
269269
},
270270
);
271271
}
272-
final country = state.availableCountries[index - 1];
272+
final country = state.countriesWithActiveSources[index - 1];
273273
return ChoiceChip(
274274
avatar: country.flagUrl.isNotEmpty
275275
? CircleAvatar(

lib/headlines-search/bloc/headlines_search_bloc.dart

Lines changed: 22 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,13 @@ class HeadlinesSearchBloc
1919
required DataRepository<Headline> headlinesRepository,
2020
required DataRepository<Topic> topicRepository,
2121
required DataRepository<Source> sourceRepository,
22+
required DataRepository<Country> countryRepository,
2223
required AppBloc appBloc,
2324
required FeedDecoratorService feedDecoratorService,
2425
}) : _headlinesRepository = headlinesRepository,
2526
_topicRepository = topicRepository,
2627
_sourceRepository = sourceRepository,
28+
_countryRepository = countryRepository,
2729
_appBloc = appBloc,
2830
_feedDecoratorService = feedDecoratorService,
2931
super(const HeadlinesSearchInitial()) {
@@ -37,6 +39,7 @@ class HeadlinesSearchBloc
3739
final DataRepository<Headline> _headlinesRepository;
3840
final DataRepository<Topic> _topicRepository;
3941
final DataRepository<Source> _sourceRepository;
42+
final DataRepository<Country> _countryRepository;
4043
final AppBloc _appBloc;
4144
final FeedDecoratorService _feedDecoratorService;
4245
static const _limit = 10;
@@ -170,12 +173,19 @@ class HeadlinesSearchBloc
170173
cursor: response.cursor,
171174
),
172175
);
173-
// Added break
174-
default:
175-
response = const PaginatedResponse(
176-
items: [],
177-
cursor: null,
178-
hasMore: false,
176+
case ContentType.country:
177+
response = await _countryRepository.readAll(
178+
filter: {'q': searchTerm, 'hasActiveHeadlines': true},
179+
pagination: const PaginationOptions(limit: _limit),
180+
sort: [const SortOption('name', SortOrder.asc)],
181+
);
182+
emit(
183+
successState.copyWith(
184+
items: List.of(successState.items)
185+
..addAll(response.items.cast<FeedItem>()),
186+
hasMore: response.hasMore,
187+
cursor: response.cursor,
188+
),
179189
);
180190
}
181191
} on HttpException catch (e) {
@@ -249,14 +259,13 @@ class HeadlinesSearchBloc
249259
sort: [const SortOption('name', SortOrder.asc)],
250260
);
251261
processedItems = rawResponse.items.cast<FeedItem>();
252-
default:
253-
// Handle unexpected content types if necessary
254-
rawResponse = const PaginatedResponse(
255-
items: [],
256-
cursor: null,
257-
hasMore: false,
262+
case ContentType.country:
263+
rawResponse = await _countryRepository.readAll(
264+
filter: {'q': searchTerm, 'hasActiveHeadlines': true},
265+
pagination: const PaginationOptions(limit: _limit),
266+
sort: [const SortOption('name', SortOrder.asc)],
258267
);
259-
processedItems = [];
268+
processedItems = rawResponse.items.cast<FeedItem>();
260269
}
261270
emit(
262271
HeadlinesSearchSuccess(

lib/headlines-search/view/headlines_search_page.dart

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import 'package:flutter_news_app_mobile_client_full_source_code/ads/widgets/ad_l
1212
import 'package:flutter_news_app_mobile_client_full_source_code/app/bloc/app_bloc.dart';
1313
// HeadlineItemWidget import removed
1414
import 'package:flutter_news_app_mobile_client_full_source_code/headlines-search/bloc/headlines_search_bloc.dart';
15-
// import 'package:flutter_news_app_mobile_client_full_source_code/headlines-search/widgets/country_item_widget.dart';
15+
import 'package:flutter_news_app_mobile_client_full_source_code/headlines-search/widgets/country_item_widget.dart';
1616
import 'package:flutter_news_app_mobile_client_full_source_code/headlines-search/widgets/source_item_widget.dart';
1717
import 'package:flutter_news_app_mobile_client_full_source_code/headlines-search/widgets/topic_item_widget.dart';
1818
import 'package:flutter_news_app_mobile_client_full_source_code/l10n/l10n.dart';
@@ -62,12 +62,11 @@ class _HeadlinesSearchViewState extends State<_HeadlinesSearchView> {
6262
_showClearButton = _textController.text.isNotEmpty;
6363
});
6464
});
65-
// TODO(fulleni): This logic might need adjustment if not all ContentType values are searchable.
66-
// For now, we default to headline if the current selection is not in the allowed list.
6765
final searchableTypes = [
6866
ContentType.headline,
6967
ContentType.topic,
7068
ContentType.source,
69+
ContentType.country,
7170
];
7271
if (!searchableTypes.contains(_selectedModelType)) {
7372
_selectedModelType = ContentType.headline;
@@ -122,11 +121,11 @@ class _HeadlinesSearchViewState extends State<_HeadlinesSearchView> {
122121
final textTheme = theme.textTheme;
123122
final appBarTheme = theme.appBarTheme;
124123

125-
// TODO(fulleni): Replace this with a filtered list of searchable content types.
126124
final availableSearchModelTypes = [
127125
ContentType.headline,
128126
ContentType.topic,
129127
ContentType.source,
128+
ContentType.country,
130129
];
131130

132131
if (!availableSearchModelTypes.contains(_selectedModelType)) {
@@ -344,6 +343,8 @@ class _HeadlinesSearchViewState extends State<_HeadlinesSearchView> {
344343
return TopicItemWidget(topic: feedItem);
345344
} else if (feedItem is Source) {
346345
return SourceItemWidget(source: feedItem);
346+
} else if (feedItem is Country) {
347+
return CountryItemWidget(country: feedItem);
347348
} else if (feedItem is AdPlaceholder) {
348349
// Retrieve the user's preferred headline image style from the AppBloc.
349350
// This is the single source of truth for this setting.
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import 'package:core/core.dart';
2+
import 'package:flutter/material.dart';
3+
import 'package:flutter_news_app_mobile_client_full_source_code/entity_details/view/entity_details_page.dart';
4+
import 'package:flutter_news_app_mobile_client_full_source_code/router/routes.dart';
5+
import 'package:go_router/go_router.dart';
6+
7+
/// A simple widget to display a Country search result.
8+
class CountryItemWidget extends StatelessWidget {
9+
/// Creates a [CountryItemWidget].
10+
const CountryItemWidget({required this.country, super.key});
11+
12+
/// The country to display.
13+
final Country country;
14+
15+
@override
16+
Widget build(BuildContext context) {
17+
return ListTile(
18+
leading: CircleAvatar(
19+
backgroundImage: NetworkImage(country.flagUrl),
20+
),
21+
title: Text(country.name),
22+
subtitle: country.isoCode.isNotEmpty
23+
? Text(
24+
country.isoCode,
25+
maxLines: 1,
26+
overflow: TextOverflow.ellipsis,
27+
)
28+
: null,
29+
onTap: () {
30+
context.push(
31+
Routes.countryDetails,
32+
extra: EntityDetailsPageArguments(entity: country),
33+
);
34+
},
35+
);
36+
}
37+
}

lib/router/router.dart

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -298,6 +298,24 @@ GoRouter createRouter({
298298
);
299299
},
300300
),
301+
GoRoute(
302+
path: Routes.countryDetails,
303+
name: Routes.countryDetailsName,
304+
builder: (context, state) {
305+
final args = state.extra as EntityDetailsPageArguments?;
306+
if (args == null) {
307+
return const Scaffold(
308+
body: Center(
309+
child: Text('Error: Missing country details arguments'),
310+
),
311+
);
312+
}
313+
return BlocProvider.value(
314+
value: accountBloc,
315+
child: EntityDetailsPage(args: args),
316+
);
317+
},
318+
),
301319
// --- Global Article Details Route (Top Level) ---
302320
// This GoRoute provides a top-level, globally accessible way to view the
303321
// HeadlineDetailsPage.
@@ -382,6 +400,7 @@ GoRouter createRouter({
382400
.read<DataRepository<Headline>>(),
383401
topicRepository: context.read<DataRepository<Topic>>(),
384402
sourceRepository: context.read<DataRepository<Source>>(),
403+
countryRepository: context.read<DataRepository<Country>>(),
385404
appBloc: context.read<AppBloc>(),
386405
feedDecoratorService: feedDecoratorService,
387406
);
@@ -527,7 +546,7 @@ GoRouter createRouter({
527546
child: CountryFilterPage(
528547
title:
529548
l10n.headlinesFeedFilterEventCountryLabel,
530-
usage: 'eventCountry',
549+
filter: CountryFilterUsage.hasActiveHeadlines,
531550
key: ValueKey(initialSelection.hashCode),
532551
),
533552
),

0 commit comments

Comments
 (0)