Skip to content

Commit 8163f86

Browse files
committed
refactor(content_management): implement background pagination for dropdowns
- Add _FetchNextCountryPage and _FetchNextLanguagePage events - Implement background fetching for countries and languages - Update UI to reflect loading state for dropdown pagination - Remove inline pagination logic from CreateSourceDataLoaded event
1 parent 19e0223 commit 8163f86

File tree

1 file changed

+96
-39
lines changed

1 file changed

+96
-39
lines changed

lib/content_management/bloc/create_source/create_source_bloc.dart

Lines changed: 96 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,14 @@ import 'package:uuid/uuid.dart';
99
part 'create_source_event.dart';
1010
part 'create_source_state.dart';
1111

12+
final class _FetchNextCountryPage extends CreateSourceEvent {
13+
const _FetchNextCountryPage();
14+
}
15+
16+
final class _FetchNextLanguagePage extends CreateSourceEvent {
17+
const _FetchNextLanguagePage();
18+
}
19+
1220
const _searchDebounceDuration = Duration(milliseconds: 300);
1321

1422
/// A BLoC to manage the state of creating a new source.
@@ -18,10 +26,10 @@ class CreateSourceBloc extends Bloc<CreateSourceEvent, CreateSourceState> {
1826
required DataRepository<Source> sourcesRepository,
1927
required DataRepository<Country> countriesRepository,
2028
required DataRepository<Language> languagesRepository,
21-
}) : _sourcesRepository = sourcesRepository,
22-
_countriesRepository = countriesRepository,
23-
_languagesRepository = languagesRepository,
24-
super(const CreateSourceState()) {
29+
}) : _sourcesRepository = sourcesRepository,
30+
_countriesRepository = countriesRepository,
31+
_languagesRepository = languagesRepository,
32+
super(const CreateSourceState()) {
2533
on<CreateSourceDataLoaded>(_onDataLoaded);
2634
on<CreateSourceNameChanged>(_onNameChanged);
2735
on<CreateSourceDescriptionChanged>(_onDescriptionChanged);
@@ -31,6 +39,8 @@ class CreateSourceBloc extends Bloc<CreateSourceEvent, CreateSourceState> {
3139
on<CreateSourceHeadquartersChanged>(_onHeadquartersChanged);
3240
on<CreateSourceStatusChanged>(_onStatusChanged);
3341
on<CreateSourceSubmitted>(_onSubmitted);
42+
on<_FetchNextCountryPage>(_onFetchNextCountryPage);
43+
on<_FetchNextLanguagePage>(_onFetchNextLanguagePage);
3444
}
3545

3646
final DataRepository<Source> _sourcesRepository;
@@ -66,43 +76,13 @@ class CreateSourceBloc extends Bloc<CreateSourceEvent, CreateSourceState> {
6676
),
6777
);
6878

69-
// After the initial page is loaded, start a background process to
79+
// After the initial page is loaded, start background processes to
7080
// fetch all remaining pages for countries and languages.
71-
//
72-
// This approach is used for the following reasons:
73-
// 1. UI Consistency: It allows us to use the standard
74-
// `DropdownButtonFormField`, which is used elsewhere in the app.
75-
// 2. Technical Limitation: The standard dropdown does not expose a
76-
// scroll controller, making on-scroll pagination impossible.
77-
//
78-
// The UI will update progressively and silently in the background as
79-
// more data arrives.
80-
while (state.countriesHasMore) {
81-
final nextCountries = await _countriesRepository.readAll(
82-
pagination: PaginationOptions(cursor: state.countriesCursor),
83-
sort: [const SortOption('name', SortOrder.asc)],
84-
);
85-
emit(
86-
state.copyWith(
87-
countries: List.of(state.countries)..addAll(nextCountries.items),
88-
countriesCursor: nextCountries.cursor,
89-
countriesHasMore: nextCountries.hasMore,
90-
),
91-
);
81+
if (state.countriesHasMore) {
82+
add(const _FetchNextCountryPage());
9283
}
93-
94-
while (state.languagesHasMore) {
95-
final nextLanguages = await _languagesRepository.readAll(
96-
pagination: PaginationOptions(cursor: state.languagesCursor),
97-
sort: [const SortOption('name', SortOrder.asc)],
98-
);
99-
emit(
100-
state.copyWith(
101-
languages: List.of(state.languages)..addAll(nextLanguages.items),
102-
languagesCursor: nextLanguages.cursor,
103-
languagesHasMore: nextLanguages.hasMore,
104-
),
105-
);
84+
if (state.languagesHasMore) {
85+
add(const _FetchNextLanguagePage());
10686
}
10787
} on HttpException catch (e) {
10888
emit(state.copyWith(status: CreateSourceStatus.failure, exception: e));
@@ -170,6 +150,83 @@ class CreateSourceBloc extends Bloc<CreateSourceEvent, CreateSourceState> {
170150
);
171151
}
172152

153+
// --- Background Data Fetching for Dropdown ---
154+
// The DropdownButtonFormField widget does not natively support on-scroll
155+
// pagination. To preserve UI consistency across the application, this BLoC
156+
// employs an event-driven background fetching mechanism.
157+
//
158+
// After the first page of items is loaded, a chain of events is initiated
159+
// to progressively fetch all remaining pages. This process is throttled
160+
// and runs in the background, ensuring the UI remains responsive while the
161+
// full list of dropdown options is populated over time.
162+
Future<void> _onFetchNextCountryPage(
163+
_FetchNextCountryPage event,
164+
Emitter<CreateSourceState> emit,
165+
) async {
166+
if (!state.countriesHasMore || state.countriesIsLoadingMore) return;
167+
168+
try {
169+
emit(state.copyWith(countriesIsLoadingMore: true));
170+
171+
await Future.delayed(const Duration(milliseconds: 400));
172+
173+
final nextCountries = await _countriesRepository.readAll(
174+
pagination: PaginationOptions(cursor: state.countriesCursor),
175+
sort: [const SortOption('name', SortOrder.asc)],
176+
);
177+
178+
emit(
179+
state.copyWith(
180+
countries: List.of(state.countries)..addAll(nextCountries.items),
181+
countriesCursor: nextCountries.cursor,
182+
countriesHasMore: nextCountries.hasMore,
183+
countriesIsLoadingMore: false,
184+
),
185+
);
186+
187+
if (nextCountries.hasMore) {
188+
add(const _FetchNextCountryPage());
189+
}
190+
} catch (e) {
191+
emit(state.copyWith(countriesIsLoadingMore: false));
192+
// Optionally log the error without disrupting the user
193+
}
194+
}
195+
196+
Future<void> _onFetchNextLanguagePage(
197+
_FetchNextLanguagePage event,
198+
Emitter<CreateSourceState> emit,
199+
) async {
200+
if (!state.languagesHasMore || state.languagesIsLoadingMore) return;
201+
202+
try {
203+
emit(state.copyWith(languagesIsLoadingMore: true));
204+
205+
await Future.delayed(const Duration(milliseconds: 400));
206+
207+
final nextLanguages = await _languagesRepository.readAll(
208+
pagination: PaginationOptions(cursor: state.languagesCursor),
209+
sort: [const SortOption('name', SortOrder.asc)],
210+
);
211+
212+
emit(
213+
state.copyWith(
214+
languages: List.of(state.languages)..addAll(nextLanguages.items),
215+
languagesCursor: nextLanguages.cursor,
216+
languagesHasMore: nextLanguages.hasMore,
217+
languagesIsLoadingMore: false,
218+
),
219+
);
220+
221+
if (nextLanguages.hasMore) {
222+
add(const _FetchNextLanguagePage());
223+
}
224+
} catch (e) {
225+
emit(state.copyWith(languagesIsLoadingMore: false));
226+
// Optionally log the error without disrupting the user
227+
}
228+
}
229+
173230
Future<void> _onSubmitted(
174231
CreateSourceSubmitted event,
175232
Emitter<CreateSourceState> emit,

0 commit comments

Comments
 (0)