Skip to content

Commit 0bc1b80

Browse files
committed
feat: implement countries filter bloc
- Fetches countries for filtering - Implements pagination - Handles loading and error states
1 parent 43b213d commit 0bc1b80

File tree

3 files changed

+232
-0
lines changed

3 files changed

+232
-0
lines changed
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
import 'dart:async';
2+
3+
import 'package:bloc/bloc.dart';
4+
import 'package:bloc_concurrency/bloc_concurrency.dart'; // For transformers
5+
import 'package:equatable/equatable.dart';
6+
import 'package:ht_countries_client/ht_countries_client.dart'; // For Country model and exceptions
7+
import 'package:ht_countries_repository/ht_countries_repository.dart';
8+
import 'package:ht_shared/ht_shared.dart'; // For PaginatedResponse
9+
10+
part 'countries_filter_event.dart';
11+
part 'countries_filter_state.dart';
12+
13+
/// {@template countries_filter_bloc}
14+
/// Manages the state for fetching and displaying countries for filtering.
15+
///
16+
/// Handles initial fetching and pagination of countries using the
17+
/// provided [HtCountriesRepository].
18+
/// {@endtemplate}
19+
class CountriesFilterBloc extends Bloc<CountriesFilterEvent, CountriesFilterState> {
20+
/// {@macro countries_filter_bloc}
21+
///
22+
/// Requires a [HtCountriesRepository] to interact with the data layer.
23+
CountriesFilterBloc({required HtCountriesRepository countriesRepository})
24+
: _countriesRepository = countriesRepository,
25+
super(const CountriesFilterState()) {
26+
on<CountriesFilterRequested>(
27+
_onCountriesFilterRequested,
28+
transformer: restartable(), // Only process the latest request
29+
);
30+
on<CountriesFilterLoadMoreRequested>(
31+
_onCountriesFilterLoadMoreRequested,
32+
transformer: droppable(), // Ignore new requests while one is processing
33+
);
34+
}
35+
36+
final HtCountriesRepository _countriesRepository;
37+
38+
/// Number of countries to fetch per page.
39+
static const _countriesLimit = 20;
40+
41+
/// Handles the initial request to fetch countries.
42+
Future<void> _onCountriesFilterRequested(
43+
CountriesFilterRequested event,
44+
Emitter<CountriesFilterState> emit,
45+
) async {
46+
// Prevent fetching if already loading or successful
47+
if (state.status == CountriesFilterStatus.loading ||
48+
state.status == CountriesFilterStatus.success) {
49+
return;
50+
}
51+
52+
emit(state.copyWith(status: CountriesFilterStatus.loading));
53+
54+
try {
55+
// Note: Repository uses 'cursor' parameter name here
56+
final response = await _countriesRepository.fetchCountries(
57+
limit: _countriesLimit,
58+
cursor: null, // Explicitly null for initial fetch
59+
);
60+
emit(
61+
state.copyWith(
62+
status: CountriesFilterStatus.success,
63+
countries: response.items,
64+
hasMore: response.hasMore,
65+
cursor: response.cursor,
66+
clearError: true, // Clear any previous error
67+
),
68+
);
69+
} on CountryFetchFailure catch (e) {
70+
emit(
71+
state.copyWith(
72+
status: CountriesFilterStatus.failure,
73+
error: e,
74+
),
75+
);
76+
} catch (e) {
77+
// Catch unexpected errors
78+
emit(
79+
state.copyWith(
80+
status: CountriesFilterStatus.failure,
81+
error: e,
82+
),
83+
);
84+
}
85+
}
86+
87+
/// Handles the request to load more countries for pagination.
88+
Future<void> _onCountriesFilterLoadMoreRequested(
89+
CountriesFilterLoadMoreRequested event,
90+
Emitter<CountriesFilterState> emit,
91+
) async {
92+
// Only proceed if currently successful and has more items
93+
if (state.status != CountriesFilterStatus.success || !state.hasMore) {
94+
return;
95+
}
96+
97+
emit(state.copyWith(status: CountriesFilterStatus.loadingMore));
98+
99+
try {
100+
// Note: Repository uses 'cursor' parameter name here
101+
final response = await _countriesRepository.fetchCountries(
102+
limit: _countriesLimit,
103+
cursor: state.cursor, // Use the cursor from the current state
104+
);
105+
emit(
106+
state.copyWith(
107+
status: CountriesFilterStatus.success,
108+
// Append new countries to the existing list
109+
countries: List.of(state.countries)..addAll(response.items),
110+
hasMore: response.hasMore,
111+
cursor: response.cursor,
112+
),
113+
);
114+
} on CountryFetchFailure catch (e) {
115+
// Keep existing data but indicate failure
116+
emit(
117+
state.copyWith(
118+
status: CountriesFilterStatus.failure,
119+
error: e,
120+
),
121+
);
122+
} catch (e) {
123+
// Catch unexpected errors
124+
emit(
125+
state.copyWith(
126+
status: CountriesFilterStatus.failure,
127+
error: e,
128+
),
129+
);
130+
}
131+
}
132+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
part of 'countries_filter_bloc.dart';
2+
3+
/// {@template countries_filter_event}
4+
/// Base class for events related to fetching and managing country filters.
5+
/// {@endtemplate}
6+
sealed class CountriesFilterEvent extends Equatable {
7+
/// {@macro countries_filter_event}
8+
const CountriesFilterEvent();
9+
10+
@override
11+
List<Object> get props => [];
12+
}
13+
14+
/// {@template countries_filter_requested}
15+
/// Event triggered to request the initial list of countries.
16+
/// {@endtemplate}
17+
final class CountriesFilterRequested extends CountriesFilterEvent {}
18+
19+
/// {@template countries_filter_load_more_requested}
20+
/// Event triggered to request the next page of countries for pagination.
21+
/// {@endtemplate}
22+
final class CountriesFilterLoadMoreRequested extends CountriesFilterEvent {}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
part of 'countries_filter_bloc.dart';
2+
3+
// Import removed, will be added to the main bloc file.
4+
5+
/// Enum representing the different statuses of the country filter data fetching.
6+
enum CountriesFilterStatus {
7+
/// Initial state, no data loaded yet.
8+
initial,
9+
10+
/// Currently fetching the first page of countries.
11+
loading,
12+
13+
/// Successfully loaded countries. May be loading more in the background.
14+
success,
15+
16+
/// An error occurred while fetching countries.
17+
failure,
18+
19+
/// Loading more countries for pagination (infinity scroll).
20+
loadingMore,
21+
}
22+
23+
/// {@template countries_filter_state}
24+
/// Represents the state for the country filter feature.
25+
///
26+
/// Contains the list of fetched countries, pagination information,
27+
/// loading/error status.
28+
/// {@endtemplate}
29+
final class CountriesFilterState extends Equatable {
30+
/// {@macro countries_filter_state}
31+
const CountriesFilterState({
32+
this.status = CountriesFilterStatus.initial,
33+
this.countries = const [],
34+
this.hasMore = true,
35+
this.cursor,
36+
this.error,
37+
});
38+
39+
/// The current status of fetching countries.
40+
final CountriesFilterStatus status;
41+
42+
/// The list of [Country] objects fetched so far.
43+
final List<Country> countries;
44+
45+
/// Flag indicating if there are more countries available to fetch.
46+
final bool hasMore;
47+
48+
/// The cursor string to fetch the next page of countries.
49+
/// This is typically the ID of the last fetched country.
50+
final String? cursor;
51+
52+
/// An optional error object if the status is [CountriesFilterStatus.failure].
53+
final Object? error;
54+
55+
/// Creates a copy of this state with the given fields replaced.
56+
CountriesFilterState copyWith({
57+
CountriesFilterStatus? status,
58+
List<Country>? countries,
59+
bool? hasMore,
60+
String? cursor,
61+
Object? error,
62+
bool clearError = false, // Flag to explicitly clear the error
63+
bool clearCursor = false, // Flag to explicitly clear the cursor
64+
}) {
65+
return CountriesFilterState(
66+
status: status ?? this.status,
67+
countries: countries ?? this.countries,
68+
hasMore: hasMore ?? this.hasMore,
69+
// Allow explicitly setting cursor to null or clearing it
70+
cursor: clearCursor ? null : (cursor ?? this.cursor),
71+
// Clear error if requested, otherwise keep existing or use new one
72+
error: clearError ? null : error ?? this.error,
73+
);
74+
}
75+
76+
@override
77+
List<Object?> get props => [status, countries, hasMore, cursor, error];
78+
}

0 commit comments

Comments
 (0)