Skip to content

Commit f8cca99

Browse files
authored
Merge pull request #48 from flutter-news-app-full-source-code/enhance-content-managment-datat-fetching-mechanism
Enhance content managment datat fetching mechanism
2 parents 08e7b8b + 1cfc3e2 commit f8cca99

File tree

5 files changed

+318
-103
lines changed

5 files changed

+318
-103
lines changed

lib/content_management/bloc/create_headline/create_headline_bloc.dart

Lines changed: 57 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@ import 'package:uuid/uuid.dart';
99
part 'create_headline_event.dart';
1010
part 'create_headline_state.dart';
1111

12+
final class _FetchNextCountryPage extends CreateHeadlineEvent {
13+
const _FetchNextCountryPage();
14+
}
15+
1216
const _searchDebounceDuration = Duration(milliseconds: 300);
1317

1418
/// A BLoC to manage the state of creating a new headline.
@@ -20,11 +24,11 @@ class CreateHeadlineBloc
2024
required DataRepository<Source> sourcesRepository,
2125
required DataRepository<Topic> topicsRepository,
2226
required DataRepository<Country> countriesRepository,
23-
}) : _headlinesRepository = headlinesRepository,
24-
_sourcesRepository = sourcesRepository,
25-
_topicsRepository = topicsRepository,
26-
_countriesRepository = countriesRepository,
27-
super(const CreateHeadlineState()) {
27+
}) : _headlinesRepository = headlinesRepository,
28+
_sourcesRepository = sourcesRepository,
29+
_topicsRepository = topicsRepository,
30+
_countriesRepository = countriesRepository,
31+
super(const CreateHeadlineState()) {
2832
on<CreateHeadlineDataLoaded>(_onDataLoaded);
2933
on<CreateHeadlineTitleChanged>(_onTitleChanged);
3034
on<CreateHeadlineExcerptChanged>(_onExcerptChanged);
@@ -35,6 +39,7 @@ class CreateHeadlineBloc
3539
on<CreateHeadlineCountryChanged>(_onCountryChanged);
3640
on<CreateHeadlineStatusChanged>(_onStatusChanged);
3741
on<CreateHeadlineSubmitted>(_onSubmitted);
42+
on<_FetchNextCountryPage>(_onFetchNextCountryPage);
3843
}
3944

4045
final DataRepository<Headline> _headlinesRepository;
@@ -76,19 +81,10 @@ class CreateHeadlineBloc
7681
),
7782
);
7883

79-
// Start background fetching for all countries
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-
);
84+
// After the initial page of countries is loaded, start a background
85+
// process to fetch all remaining pages.
86+
if (state.countriesHasMore) {
87+
add(const _FetchNextCountryPage());
9288
}
9389
} on HttpException catch (e) {
9490
emit(state.copyWith(status: CreateHeadlineStatus.failure, exception: e));
@@ -163,6 +159,49 @@ class CreateHeadlineBloc
163159
);
164160
}
165161

162+
// --- Background Data Fetching for Dropdown ---
163+
// The DropdownButtonFormField widget does not natively support on-scroll
164+
// pagination. To preserve UI consistency across the application, this BLoC
165+
// employs an event-driven background fetching mechanism.
166+
//
167+
// After the first page of items is loaded, a chain of events is initiated
168+
// to progressively fetch all remaining pages. This process is throttled
169+
// and runs in the background, ensuring the UI remains responsive while the
170+
// full list of dropdown options is populated over time.
171+
Future<void> _onFetchNextCountryPage(
172+
_FetchNextCountryPage event,
173+
Emitter<CreateHeadlineState> emit,
174+
) async {
175+
if (!state.countriesHasMore || state.countriesIsLoadingMore) return;
176+
177+
try {
178+
emit(state.copyWith(countriesIsLoadingMore: true));
179+
180+
await Future.delayed(const Duration(milliseconds: 400));
181+
182+
final nextCountries = await _countriesRepository.readAll(
183+
pagination: PaginationOptions(cursor: state.countriesCursor),
184+
sort: [const SortOption('name', SortOrder.asc)],
185+
);
186+
187+
emit(
188+
state.copyWith(
189+
countries: List.of(state.countries)..addAll(nextCountries.items),
190+
countriesCursor: nextCountries.cursor,
191+
countriesHasMore: nextCountries.hasMore,
192+
countriesIsLoadingMore: false,
193+
),
194+
);
195+
196+
if (nextCountries.hasMore) {
197+
add(const _FetchNextCountryPage());
198+
}
199+
} catch (e) {
200+
emit(state.copyWith(countriesIsLoadingMore: false));
201+
// Optionally log the error without disrupting the user
202+
}
203+
}
204+
166205
Future<void> _onSubmitted(
167206
CreateHeadlineSubmitted event,
168207
Emitter<CreateHeadlineState> emit,

lib/content_management/bloc/create_source/create_source_bloc.dart

Lines changed: 97 additions & 31 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,34 +76,13 @@ class CreateSourceBloc extends Bloc<CreateSourceEvent, CreateSourceState> {
6676
),
6777
);
6878

69-
// Start background fetching for all countries
70-
while (state.countriesHasMore) {
71-
final nextCountries = await _countriesRepository.readAll(
72-
pagination: PaginationOptions(cursor: state.countriesCursor),
73-
sort: [const SortOption('name', SortOrder.asc)],
74-
);
75-
emit(
76-
state.copyWith(
77-
countries: List.of(state.countries)..addAll(nextCountries.items),
78-
countriesCursor: nextCountries.cursor,
79-
countriesHasMore: nextCountries.hasMore,
80-
),
81-
);
79+
// After the initial page is loaded, start background processes to
80+
// fetch all remaining pages for countries and languages.
81+
if (state.countriesHasMore) {
82+
add(const _FetchNextCountryPage());
8283
}
83-
84-
// Start background fetching for all languages
85-
while (state.languagesHasMore) {
86-
final nextLanguages = await _languagesRepository.readAll(
87-
pagination: PaginationOptions(cursor: state.languagesCursor),
88-
sort: [const SortOption('name', SortOrder.asc)],
89-
);
90-
emit(
91-
state.copyWith(
92-
languages: List.of(state.languages)..addAll(nextLanguages.items),
93-
languagesCursor: nextLanguages.cursor,
94-
languagesHasMore: nextLanguages.hasMore,
95-
),
96-
);
84+
if (state.languagesHasMore) {
85+
add(const _FetchNextLanguagePage());
9786
}
9887
} on HttpException catch (e) {
9988
emit(state.copyWith(status: CreateSourceStatus.failure, exception: e));
@@ -161,6 +150,83 @@ class CreateSourceBloc extends Bloc<CreateSourceEvent, CreateSourceState> {
161150
);
162151
}
163152

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+
164230
Future<void> _onSubmitted(
165231
CreateSourceSubmitted event,
166232
Emitter<CreateSourceState> emit,

lib/content_management/bloc/edit_headline/edit_headline_bloc.dart

Lines changed: 58 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@ import 'package:flutter/foundation.dart';
88
part 'edit_headline_event.dart';
99
part 'edit_headline_state.dart';
1010

11+
final class _FetchNextCountryPage extends EditHeadlineEvent {
12+
const _FetchNextCountryPage();
13+
}
14+
1115
const _searchDebounceDuration = Duration(milliseconds: 300);
1216

1317
/// A BLoC to manage the state of editing a single headline.
@@ -19,12 +23,12 @@ class EditHeadlineBloc extends Bloc<EditHeadlineEvent, EditHeadlineState> {
1923
required DataRepository<Topic> topicsRepository,
2024
required DataRepository<Country> countriesRepository,
2125
required String headlineId,
22-
}) : _headlinesRepository = headlinesRepository,
23-
_sourcesRepository = sourcesRepository,
24-
_topicsRepository = topicsRepository,
25-
_countriesRepository = countriesRepository,
26-
_headlineId = headlineId,
27-
super(const EditHeadlineState()) {
26+
}) : _headlinesRepository = headlinesRepository,
27+
_sourcesRepository = sourcesRepository,
28+
_topicsRepository = topicsRepository,
29+
_countriesRepository = countriesRepository,
30+
_headlineId = headlineId,
31+
super(const EditHeadlineState()) {
2832
on<EditHeadlineLoaded>(_onLoaded);
2933
on<EditHeadlineTitleChanged>(_onTitleChanged);
3034
on<EditHeadlineExcerptChanged>(_onExcerptChanged);
@@ -35,6 +39,7 @@ class EditHeadlineBloc extends Bloc<EditHeadlineEvent, EditHeadlineState> {
3539
on<EditHeadlineCountryChanged>(_onCountryChanged);
3640
on<EditHeadlineStatusChanged>(_onStatusChanged);
3741
on<EditHeadlineSubmitted>(_onSubmitted);
42+
on<_FetchNextCountryPage>(_onFetchNextCountryPage);
3843
}
3944

4045
final DataRepository<Headline> _headlinesRepository;
@@ -87,19 +92,10 @@ class EditHeadlineBloc extends Bloc<EditHeadlineEvent, EditHeadlineState> {
8792
),
8893
);
8994

90-
// Start background fetching for all countries
91-
while (state.countriesHasMore) {
92-
final nextCountries = await _countriesRepository.readAll(
93-
pagination: PaginationOptions(cursor: state.countriesCursor),
94-
sort: [const SortOption('name', SortOrder.asc)],
95-
);
96-
emit(
97-
state.copyWith(
98-
countries: List.of(state.countries)..addAll(nextCountries.items),
99-
countriesCursor: nextCountries.cursor,
100-
countriesHasMore: nextCountries.hasMore,
101-
),
102-
);
95+
// After the initial page of countries is loaded, start a background
96+
// process to fetch all remaining pages.
97+
if (state.countriesHasMore) {
98+
add(const _FetchNextCountryPage());
10399
}
104100
} on HttpException catch (e) {
105101
emit(state.copyWith(status: EditHeadlineStatus.failure, exception: e));
@@ -201,6 +197,49 @@ class EditHeadlineBloc extends Bloc<EditHeadlineEvent, EditHeadlineState> {
201197
);
202198
}
203199

200+
// --- Background Data Fetching for Dropdown ---
201+
// The DropdownButtonFormField widget does not natively support on-scroll
202+
// pagination. To preserve UI consistency across the application, this BLoC
203+
// employs an event-driven background fetching mechanism.
204+
//
205+
// After the first page of items is loaded, a chain of events is initiated
206+
// to progressively fetch all remaining pages. This process is throttled
207+
// and runs in the background, ensuring the UI remains responsive while the
208+
// full list of dropdown options is populated over time.
209+
Future<void> _onFetchNextCountryPage(
210+
_FetchNextCountryPage event,
211+
Emitter<EditHeadlineState> emit,
212+
) async {
213+
if (!state.countriesHasMore || state.countriesIsLoadingMore) return;
214+
215+
try {
216+
emit(state.copyWith(countriesIsLoadingMore: true));
217+
218+
await Future.delayed(const Duration(milliseconds: 400));
219+
220+
final nextCountries = await _countriesRepository.readAll(
221+
pagination: PaginationOptions(cursor: state.countriesCursor),
222+
sort: [const SortOption('name', SortOrder.asc)],
223+
);
224+
225+
emit(
226+
state.copyWith(
227+
countries: List.of(state.countries)..addAll(nextCountries.items),
228+
countriesCursor: nextCountries.cursor,
229+
countriesHasMore: nextCountries.hasMore,
230+
countriesIsLoadingMore: false,
231+
),
232+
);
233+
234+
if (nextCountries.hasMore) {
235+
add(const _FetchNextCountryPage());
236+
}
237+
} catch (e) {
238+
emit(state.copyWith(countriesIsLoadingMore: false));
239+
// Optionally log the error without disrupting the user
240+
}
241+
}
242+
204243
Future<void> _onSubmitted(
205244
EditHeadlineSubmitted event,
206245
Emitter<EditHeadlineState> emit,

0 commit comments

Comments
 (0)