Skip to content

Commit c1f7697

Browse files
committed
refactor(countries): use BLoC for country filter
- Implemented CountriesFilterBloc - Handles loading, error, and data states - Enables pagination via BLoC
1 parent 0bc1b80 commit c1f7697

File tree

1 file changed

+80
-108
lines changed

1 file changed

+80
-108
lines changed

lib/headlines-feed/view/country_filter_page.dart

Lines changed: 80 additions & 108 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,21 @@
1-
//
21
// ignore_for_file: lines_longer_than_80_chars
32

43
import 'package:flutter/material.dart';
54
import 'package:flutter_bloc/flutter_bloc.dart';
65
import 'package:go_router/go_router.dart';
76
import 'package:ht_countries_client/ht_countries_client.dart';
8-
import 'package:ht_countries_repository/ht_countries_repository.dart';
7+
// Removed repository import: import 'package:ht_countries_repository/ht_countries_repository.dart';
8+
import 'package:ht_main/headlines-feed/bloc/countries_filter_bloc.dart'; // Import the BLoC
99
import 'package:ht_main/l10n/l10n.dart';
1010
import 'package:ht_main/shared/constants/constants.dart';
1111
import 'package:ht_main/shared/widgets/widgets.dart'; // For loading/error widgets
1212

1313
/// {@template country_filter_page}
1414
/// A page dedicated to selecting event countries for filtering headlines.
1515
///
16-
/// Fetches countries paginatively, allows multiple selections, and returns
17-
/// the selected list via `context.pop` when the user applies the changes.
16+
/// Uses [CountriesFilterBloc] to fetch countries paginatively, allows multiple
17+
/// selections, and returns the selected list via `context.pop` when the user
18+
/// applies the changes.
1819
/// {@endtemplate}
1920
class CountryFilterPage extends StatefulWidget {
2021
/// {@macro country_filter_page}
@@ -26,32 +27,13 @@ class CountryFilterPage extends StatefulWidget {
2627

2728
/// State for the [CountryFilterPage].
2829
///
29-
/// Manages the local selection state ([_pageSelectedCountries]), fetches
30-
/// countries from the [HtCountriesRepository], handles pagination using a
31-
/// [ScrollController], and displays loading/error/empty/loaded states.
30+
/// Manages the local selection state ([_pageSelectedCountries]) and interacts
31+
/// with [CountriesFilterBloc] for data fetching and pagination.
3232
class _CountryFilterPageState extends State<CountryFilterPage> {
3333
/// Stores the countries selected by the user on this page.
3434
/// Initialized from the `extra` parameter passed during navigation.
3535
late Set<Country> _pageSelectedCountries;
3636

37-
/// List of all countries fetched from the repository.
38-
List<Country> _allCountries = [];
39-
40-
/// Flag indicating if the initial country list is being loaded.
41-
bool _isLoading = true;
42-
43-
/// Flag indicating if more countries are being loaded for pagination.
44-
bool _isLoadingMore = false;
45-
46-
/// Flag indicating if more countries are available to fetch.
47-
bool _hasMore = true;
48-
49-
/// Cursor for fetching the next page of countries.
50-
String? _cursor;
51-
52-
/// Stores any error message that occurred during fetching.
53-
String? _error;
54-
5537
/// Scroll controller to detect when the user reaches the end of the list
5638
/// for pagination.
5739
final _scrollController = ScrollController();
@@ -64,7 +46,8 @@ class _CountryFilterPageState extends State<CountryFilterPage> {
6446
final initialSelection =
6547
GoRouterState.of(context).extra as List<Country>?;
6648
_pageSelectedCountries = Set.from(initialSelection ?? []);
67-
_fetchCountries(); // Initial fetch
49+
// Request initial countries from the BLoC
50+
context.read<CountriesFilterBloc>().add(CountriesFilterRequested());
6851
});
6952
_scrollController.addListener(_onScroll);
7053
}
@@ -77,64 +60,20 @@ class _CountryFilterPageState extends State<CountryFilterPage> {
7760
super.dispose();
7861
}
7962

80-
/// Fetches countries from the [HtCountriesRepository].
81-
///
82-
/// Handles both initial fetch and pagination (`loadMore = true`). Updates
83-
/// loading states, fetched data ([_allCountries]), pagination info
84-
/// ([_cursor], [_hasMore]), and error state ([_error]).
85-
Future<void> _fetchCountries({bool loadMore = false}) async {
86-
// Prevent unnecessary fetches
87-
if (!loadMore && _isLoading) return;
88-
if (loadMore && (_isLoadingMore || !_hasMore)) return;
89-
90-
setState(() {
91-
if (loadMore) {
92-
_isLoadingMore = true;
93-
} else {
94-
_isLoading = true;
95-
_error = null;
96-
}
97-
});
98-
99-
try {
100-
// Ensure HtCountriesRepository is provided higher up
101-
final repo = context.read<HtCountriesRepository>();
102-
// Note: fetchCountries uses 'cursor' which corresponds to 'startAfterId'
103-
final response = await repo.fetchCountries(
104-
limit: 20, // Adjust limit as needed
105-
cursor: loadMore ? _cursor : null,
106-
);
107-
108-
setState(() {
109-
if (loadMore) {
110-
_allCountries.addAll(response.items);
111-
} else {
112-
_allCountries = response.items;
113-
}
114-
_cursor = response.cursor;
115-
_hasMore = response.hasMore;
116-
_isLoading = false;
117-
_isLoadingMore = false;
118-
});
119-
} catch (e) {
120-
setState(() {
121-
_isLoading = false;
122-
_isLoadingMore = false;
123-
_error = e.toString();
124-
});
125-
}
126-
}
127-
12863
/// Callback function for scroll events.
12964
///
13065
/// Checks if the user has scrolled near the bottom of the list and triggers
131-
/// fetching more countries if available.
66+
/// fetching more countries via the BLoC if available.
13267
void _onScroll() {
13368
if (!_scrollController.hasClients) return;
13469
final maxScroll = _scrollController.position.maxScrollExtent;
13570
final currentScroll = _scrollController.offset;
136-
if (currentScroll >= (maxScroll * 0.9) && _hasMore && !_isLoadingMore) {
137-
_fetchCountries(loadMore: true);
71+
final bloc = context.read<CountriesFilterBloc>();
72+
// Fetch more when nearing the bottom, if BLoC has more and isn't already loading more
73+
if (currentScroll >= (maxScroll * 0.9) &&
74+
bloc.state.hasMore &&
75+
bloc.state.status != CountriesFilterStatus.loadingMore) {
76+
bloc.add(CountriesFilterLoadMoreRequested());
13877
}
13978
}
14079

@@ -155,65 +94,98 @@ class _CountryFilterPageState extends State<CountryFilterPage> {
15594
),
15695
],
15796
),
158-
body: _buildBody(context),
97+
// Use BlocBuilder to react to state changes from CountriesFilterBloc
98+
body: BlocBuilder<CountriesFilterBloc, CountriesFilterState>(
99+
builder: (context, state) {
100+
return _buildBody(context, state);
101+
},
102+
),
159103
);
160104
}
161105

162-
/// Builds the main content body based on the current loading/error/data state.
163-
Widget _buildBody(BuildContext context) {
106+
/// Builds the main content body based on the current [CountriesFilterState].
107+
Widget _buildBody(BuildContext context, CountriesFilterState state) {
164108
final l10n = context.l10n;
165-
if (_isLoading) {
166-
// Show initial loading indicator
109+
110+
// Handle initial loading state
111+
if (state.status == CountriesFilterStatus.loading) {
167112
return LoadingStateWidget(
168-
icon: Icons.public_outlined,
169-
headline: l10n.countryFilterLoadingHeadline,
170-
subheadline: l10n.countryFilterLoadingSubheadline,
113+
icon: Icons.public_outlined, // Changed icon
114+
headline: l10n.countryFilterLoadingHeadline, // Assumes this exists
115+
subheadline: l10n.countryFilterLoadingSubheadline, // Assumes this exists
171116
);
172117
}
173118

174-
if (_error != null) {
175-
return FailureStateWidget(message: _error!, onRetry: _fetchCountries);
119+
// Handle failure state (show error and retry button)
120+
if (state.status == CountriesFilterStatus.failure &&
121+
state.countries.isEmpty) {
122+
return FailureStateWidget(
123+
message: state.error?.toString() ?? l10n.unknownError, // Assumes this exists
124+
onRetry: () =>
125+
context.read<CountriesFilterBloc>().add(CountriesFilterRequested()),
126+
);
176127
}
177128

178-
if (_allCountries.isEmpty) {
129+
// Handle empty state (after successful load but no countries found)
130+
if (state.status == CountriesFilterStatus.success &&
131+
state.countries.isEmpty) {
179132
return InitialStateWidget(
180133
icon: Icons.search_off,
181-
headline: l10n.countryFilterEmptyHeadline,
182-
subheadline: l10n.countryFilterEmptySubheadline,
134+
headline: l10n.countryFilterEmptyHeadline, // Assumes this exists
135+
subheadline: l10n.countryFilterEmptySubheadline, // Assumes this exists
183136
);
184137
}
185138

139+
// Handle loaded state (success or loading more)
186140
return ListView.builder(
187141
controller: _scrollController,
188142
padding: const EdgeInsets.only(bottom: AppSpacing.xxl),
189-
itemCount: _allCountries.length + (_hasMore ? 1 : 0),
143+
itemCount: state.countries.length +
144+
((state.status == CountriesFilterStatus.loadingMore ||
145+
(state.status == CountriesFilterStatus.failure &&
146+
state.countries.isNotEmpty))
147+
? 1
148+
: 0),
190149
itemBuilder: (context, index) {
191-
if (index >= _allCountries.length) {
192-
return _isLoadingMore
193-
? const Padding(
194-
padding: EdgeInsets.symmetric(vertical: AppSpacing.lg),
195-
child: Center(child: CircularProgressIndicator()),
196-
)
197-
: const SizedBox.shrink();
150+
if (index >= state.countries.length) {
151+
if (state.status == CountriesFilterStatus.loadingMore) {
152+
return const Padding(
153+
padding: EdgeInsets.symmetric(vertical: AppSpacing.lg),
154+
child: Center(child: CircularProgressIndicator()),
155+
);
156+
} else if (state.status == CountriesFilterStatus.failure) {
157+
return Padding(
158+
padding: const EdgeInsets.symmetric(
159+
vertical: AppSpacing.md,
160+
horizontal: AppSpacing.lg,
161+
),
162+
child: Center(
163+
child: Text(
164+
l10n.loadMoreError, // Assumes this exists
165+
style: Theme.of(context).textTheme.bodySmall?.copyWith(
166+
color: Theme.of(context).colorScheme.error,
167+
),
168+
),
169+
),
170+
);
171+
} else {
172+
return const SizedBox.shrink();
173+
}
198174
}
199175

200-
final country = _allCountries[index];
176+
final country = state.countries[index];
201177
final isSelected = _pageSelectedCountries.contains(country);
202178

203-
// Show country flag
204179
return CheckboxListTile(
205180
title: Text(country.name),
206-
secondary: SizedBox(
207-
width: 40, // Consistent size
208-
height: 40,
181+
secondary: SizedBox( // Use SizedBox for consistent flag size
182+
width: 40,
183+
height: 30, // Adjust height for flag aspect ratio if needed
209184
child: Image.network(
210185
country.flagUrl,
211186
fit: BoxFit.contain,
212-
// Add error builder for network images
213-
errorBuilder:
214-
(context, error, stackTrace) => const Icon(
215-
Icons.flag_circle_outlined,
216-
), // Placeholder icon
187+
errorBuilder: (context, error, stackTrace) =>
188+
const Icon(Icons.flag_outlined), // Placeholder icon
217189
),
218190
),
219191
value: isSelected,

0 commit comments

Comments
 (0)