Skip to content

Commit 50e4e75

Browse files
authored
Merge pull request #56 from flutter-news-app-full-source-code/enhance-content-management-background-fetching
Enhance content management background fetching
2 parents 04cca94 + e607dbf commit 50e4e75

File tree

7 files changed

+348
-208
lines changed

7 files changed

+348
-208
lines changed

lib/app/view/app.dart

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import 'package:flutter_news_app_web_dashboard_full_source_code/content_manageme
1616
import 'package:flutter_news_app_web_dashboard_full_source_code/dashboard/bloc/dashboard_bloc.dart';
1717
import 'package:flutter_news_app_web_dashboard_full_source_code/l10n/app_localizations.dart';
1818
import 'package:flutter_news_app_web_dashboard_full_source_code/router/router.dart';
19+
import 'package:flutter_news_app_web_dashboard_full_source_code/shared/services/throttled_fetching_service.dart';
1920
import 'package:go_router/go_router.dart';
2021
import 'package:kv_storage_service/kv_storage_service.dart';
2122
import 'package:logging/logging.dart';
@@ -79,6 +80,7 @@ class App extends StatelessWidget {
7980
RepositoryProvider.value(value: _countriesRepository),
8081
RepositoryProvider.value(value: _languagesRepository),
8182
RepositoryProvider.value(value: _kvStorageService),
83+
RepositoryProvider(create: (context) => const ThrottledFetchingService()),
8284
],
8385
child: MultiBlocProvider(
8486
providers: [
@@ -110,6 +112,7 @@ class App extends StatelessWidget {
110112
sourcesRepository: context.read<DataRepository<Source>>(),
111113
countriesRepository: context.read<DataRepository<Country>>(),
112114
languagesRepository: context.read<DataRepository<Language>>(),
115+
fetchingService: context.read<ThrottledFetchingService>(),
113116
)..add(const SharedDataRequested()),
114117
),
115118
BlocProvider(

lib/content_management/bloc/content_management_bloc.dart

Lines changed: 34 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import 'package:bloc/bloc.dart';
22
import 'package:core/core.dart';
33
import 'package:data_repository/data_repository.dart';
44
import 'package:equatable/equatable.dart';
5+
import 'package:flutter_news_app_web_dashboard_full_source_code/shared/services/throttled_fetching_service.dart';
56

67
part 'content_management_event.dart';
78
part 'content_management_state.dart';
@@ -26,12 +27,14 @@ class ContentManagementBloc
2627
required DataRepository<Source> sourcesRepository,
2728
required DataRepository<Country> countriesRepository,
2829
required DataRepository<Language> languagesRepository,
29-
}) : _headlinesRepository = headlinesRepository,
30-
_topicsRepository = topicsRepository,
31-
_sourcesRepository = sourcesRepository,
32-
_countriesRepository = countriesRepository,
33-
_languagesRepository = languagesRepository,
34-
super(const ContentManagementState()) {
30+
required ThrottledFetchingService fetchingService,
31+
}) : _headlinesRepository = headlinesRepository,
32+
_topicsRepository = topicsRepository,
33+
_sourcesRepository = sourcesRepository,
34+
_countriesRepository = countriesRepository,
35+
_languagesRepository = languagesRepository,
36+
_fetchingService = fetchingService,
37+
super(const ContentManagementState()) {
3538
on<SharedDataRequested>(_onSharedDataRequested);
3639
on<ContentManagementTabChanged>(_onContentManagementTabChanged);
3740
on<LoadHeadlinesRequested>(_onLoadHeadlinesRequested);
@@ -50,39 +53,32 @@ class ContentManagementBloc
5053
final DataRepository<Source> _sourcesRepository;
5154
final DataRepository<Country> _countriesRepository;
5255
final DataRepository<Language> _languagesRepository;
53-
54-
// --- Background Data Fetching for countries/languages for the ui Dropdown ---
55-
//
56-
// The DropdownButtonFormField widget does not natively support on-scroll
57-
// pagination. To preserve UI consistency across the application, this BLoC
58-
// employs an event-driven background fetching mechanism.
56+
final ThrottledFetchingService _fetchingService;
57+
58+
/// Handles the pre-fetching of shared data required for the content
59+
/// management section.
60+
///
61+
/// **Strategy Rationale (The "Why"):**
62+
/// This pre-fetching strategy is a direct result of a UI component choice
63+
/// made to preserve visual consistency across the application. The standard
64+
/// `DropdownButtonFormField` is used for selection fields in forms.
65+
/// A key limitation of this widget is its lack of native support for
66+
/// on-scroll pagination or dynamic data loading.
67+
///
68+
/// To work around this, and to ensure a seamless user experience without
69+
/// loading delays when a form is opened, we must load the entire dataset
70+
/// for these dropdowns (e.g., all countries, all languages) into the state
71+
/// ahead of time.
72+
///
73+
/// **Implementation (The "How"):**
74+
/// To execute this pre-fetch efficiently, this handler utilizes the
75+
/// `ThrottledFetchingService`. This service fetches all pages of a given
76+
/// resource in parallel, which dramatically reduces the load time compared
77+
/// to fetching them sequentially, making the upfront data load manageable.
5978
Future<void> _onSharedDataRequested(
6079
SharedDataRequested event,
6180
Emitter<ContentManagementState> emit,
6281
) async {
63-
// Helper function to fetch all items of a given type.
64-
Future<List<T>> fetchAll<T>({
65-
required DataRepository<T> repository,
66-
required List<SortOption> sort,
67-
}) async {
68-
final allItems = <T>[];
69-
String? cursor;
70-
bool hasMore;
71-
72-
do {
73-
final response = await repository.readAll(
74-
sort: sort,
75-
pagination: PaginationOptions(cursor: cursor),
76-
filter: {'status': ContentStatus.active.name},
77-
);
78-
allItems.addAll(response.items);
79-
cursor = response.cursor;
80-
hasMore = response.hasMore;
81-
} while (hasMore);
82-
83-
return allItems;
84-
}
85-
8682
// Check if data is already loaded or is currently loading to prevent
8783
// redundant fetches.
8884
if (state.allCountriesStatus == ContentManagementStatus.success &&
@@ -99,13 +95,13 @@ class ContentManagementBloc
9995
);
10096

10197
try {
102-
// Fetch both lists in parallel.
98+
// Fetch both lists in parallel using the dedicated fetching service.
10399
final results = await Future.wait([
104-
fetchAll<Country>(
100+
_fetchingService.fetchAll<Country>(
105101
repository: _countriesRepository,
106102
sort: [const SortOption('name', SortOrder.asc)],
107103
),
108-
fetchAll<Language>(
104+
_fetchingService.fetchAll<Language>(
109105
repository: _languagesRepository,
110106
sort: [const SortOption('name', SortOrder.asc)],
111107
),

lib/content_management/view/create_headline_page.dart

Lines changed: 48 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,9 @@ class CreateHeadlinePage extends StatelessWidget {
2222
// The list of all countries is fetched once and cached in the
2323
// ContentManagementBloc. We read it here and provide it to the
2424
// CreateHeadlineBloc.
25-
final allCountries = context
26-
.read<ContentManagementBloc>()
27-
.state
28-
.allCountries;
25+
final contentManagementState = context.watch<ContentManagementBloc>().state;
26+
final allCountries = contentManagementState.allCountries;
27+
2928
return BlocProvider(
3029
create: (context) => CreateHeadlineBloc(
3130
headlinesRepository: context.read<DataRepository<Headline>>(),
@@ -221,40 +220,53 @@ class _CreateHeadlineViewState extends State<_CreateHeadlineView> {
221220
.add(CreateHeadlineTopicChanged(value)),
222221
),
223222
const SizedBox(height: AppSpacing.lg),
224-
DropdownButtonFormField<Country?>(
225-
value: state.eventCountry,
226-
decoration: InputDecoration(
227-
labelText: l10n.countryName,
228-
border: const OutlineInputBorder(),
229-
),
230-
items: [
231-
DropdownMenuItem(value: null, child: Text(l10n.none)),
232-
...state.countries.map(
233-
(country) => DropdownMenuItem(
234-
value: country,
235-
child: Row(
236-
children: [
237-
SizedBox(
238-
width: 32,
239-
height: 20,
240-
child: Image.network(
241-
country.flagUrl,
242-
fit: BoxFit.cover,
243-
errorBuilder:
244-
(context, error, stackTrace) =>
245-
const Icon(Icons.flag),
246-
),
223+
BlocBuilder<ContentManagementBloc, ContentManagementState>(
224+
builder: (context, contentState) {
225+
final isLoading = contentState.allCountriesStatus ==
226+
ContentManagementStatus.loading;
227+
return DropdownButtonFormField<Country?>(
228+
value: state.eventCountry,
229+
decoration: InputDecoration(
230+
labelText: l10n.countryName,
231+
border: const OutlineInputBorder(),
232+
helperText:
233+
isLoading ? l10n.loadingFullList : null,
234+
),
235+
items: [
236+
DropdownMenuItem(
237+
value: null,
238+
child: Text(l10n.none),
239+
),
240+
...state.countries.map(
241+
(country) => DropdownMenuItem(
242+
value: country,
243+
child: Row(
244+
children: [
245+
SizedBox(
246+
width: 32,
247+
height: 20,
248+
child: Image.network(
249+
country.flagUrl,
250+
fit: BoxFit.cover,
251+
errorBuilder:
252+
(context, error, stackTrace) =>
253+
const Icon(Icons.flag),
254+
),
255+
),
256+
const SizedBox(width: AppSpacing.md),
257+
Text(country.name),
258+
],
247259
),
248-
const SizedBox(width: AppSpacing.md),
249-
Text(country.name),
250-
],
260+
),
251261
),
252-
),
253-
),
254-
],
255-
onChanged: (value) => context
256-
.read<CreateHeadlineBloc>()
257-
.add(CreateHeadlineCountryChanged(value)),
262+
],
263+
onChanged: isLoading
264+
? null
265+
: (value) => context
266+
.read<CreateHeadlineBloc>()
267+
.add(CreateHeadlineCountryChanged(value)),
268+
);
269+
},
258270
),
259271
const SizedBox(height: AppSpacing.lg),
260272
DropdownButtonFormField<ContentStatus>(

lib/content_management/view/create_source_page.dart

Lines changed: 70 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -168,24 +168,35 @@ class _CreateSourceViewState extends State<_CreateSourceView> {
168168
.add(CreateSourceUrlChanged(value)),
169169
),
170170
const SizedBox(height: AppSpacing.lg),
171-
DropdownButtonFormField<Language?>(
172-
value: state.language,
173-
decoration: InputDecoration(
174-
labelText: l10n.language,
175-
border: const OutlineInputBorder(),
176-
),
177-
items: [
178-
DropdownMenuItem(value: null, child: Text(l10n.none)),
179-
...state.languages.map(
180-
(language) => DropdownMenuItem(
181-
value: language,
182-
child: Text(language.name),
171+
BlocBuilder<ContentManagementBloc, ContentManagementState>(
172+
builder: (context, contentState) {
173+
final isLoading = contentState.allLanguagesStatus ==
174+
ContentManagementStatus.loading;
175+
return DropdownButtonFormField<Language?>(
176+
value: state.language,
177+
decoration: InputDecoration(
178+
labelText: l10n.language,
179+
border: const OutlineInputBorder(),
180+
helperText:
181+
isLoading ? l10n.loadingFullList : null,
183182
),
184-
),
185-
],
186-
onChanged: (value) => context
187-
.read<CreateSourceBloc>()
188-
.add(CreateSourceLanguageChanged(value)),
183+
items: [
184+
DropdownMenuItem(
185+
value: null, child: Text(l10n.none)),
186+
...state.languages.map(
187+
(language) => DropdownMenuItem(
188+
value: language,
189+
child: Text(language.name),
190+
),
191+
),
192+
],
193+
onChanged: isLoading
194+
? null
195+
: (value) => context
196+
.read<CreateSourceBloc>()
197+
.add(CreateSourceLanguageChanged(value)),
198+
);
199+
},
189200
),
190201
const SizedBox(height: AppSpacing.lg),
191202
DropdownButtonFormField<SourceType?>(
@@ -208,40 +219,50 @@ class _CreateSourceViewState extends State<_CreateSourceView> {
208219
.add(CreateSourceTypeChanged(value)),
209220
),
210221
const SizedBox(height: AppSpacing.lg),
211-
DropdownButtonFormField<Country?>(
212-
value: state.headquarters,
213-
decoration: InputDecoration(
214-
labelText: l10n.headquarters,
215-
border: const OutlineInputBorder(),
216-
),
217-
items: [
218-
DropdownMenuItem(value: null, child: Text(l10n.none)),
219-
...state.countries.map(
220-
(country) => DropdownMenuItem(
221-
value: country,
222-
child: Row(
223-
children: [
224-
SizedBox(
225-
width: 32,
226-
height: 20,
227-
child: Image.network(
228-
country.flagUrl,
229-
fit: BoxFit.cover,
230-
errorBuilder:
231-
(context, error, stackTrace) =>
232-
const Icon(Icons.flag),
233-
),
222+
BlocBuilder<ContentManagementBloc, ContentManagementState>(
223+
builder: (context, contentState) {
224+
final isLoading = contentState.allCountriesStatus ==
225+
ContentManagementStatus.loading;
226+
return DropdownButtonFormField<Country?>(
227+
value: state.headquarters,
228+
decoration: InputDecoration(
229+
labelText: l10n.headquarters,
230+
border: const OutlineInputBorder(),
231+
helperText:
232+
isLoading ? l10n.loadingFullList : null,
233+
),
234+
items: [
235+
DropdownMenuItem(
236+
value: null, child: Text(l10n.none)),
237+
...state.countries.map(
238+
(country) => DropdownMenuItem(
239+
value: country,
240+
child: Row(
241+
children: [
242+
SizedBox(
243+
width: 32,
244+
height: 20,
245+
child: Image.network(
246+
country.flagUrl,
247+
fit: BoxFit.cover,
248+
errorBuilder:
249+
(context, error, stackTrace) =>
250+
const Icon(Icons.flag),
251+
),
252+
),
253+
const SizedBox(width: AppSpacing.md),
254+
Text(country.name),
255+
],
234256
),
235-
const SizedBox(width: AppSpacing.md),
236-
Text(country.name),
237-
],
257+
),
238258
),
239-
),
240-
),
241-
],
242-
onChanged: (value) => context
243-
.read<CreateSourceBloc>()
244-
.add(CreateSourceHeadquartersChanged(value)),
259+
],
260+
onChanged: isLoading
261+
? null
262+
: (value) => context.read<CreateSourceBloc>().add(
263+
CreateSourceHeadquartersChanged(value)),
264+
);
265+
},
245266
),
246267
const SizedBox(height: AppSpacing.lg),
247268
DropdownButtonFormField<ContentStatus>(

0 commit comments

Comments
 (0)