Skip to content

Commit 04cca94

Browse files
authored
Merge pull request #55 from flutter-news-app-full-source-code/enhance-contenet-management-data-perfetching-performance
Enhance contenet management data perfetching performance
2 parents 38b74a5 + 4c093b8 commit 04cca94

31 files changed

+493
-856
lines changed

lib/app/view/app.dart

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -108,12 +108,14 @@ class App extends StatelessWidget {
108108
headlinesRepository: context.read<DataRepository<Headline>>(),
109109
topicsRepository: context.read<DataRepository<Topic>>(),
110110
sourcesRepository: context.read<DataRepository<Source>>(),
111-
),
111+
countriesRepository: context.read<DataRepository<Country>>(),
112+
languagesRepository: context.read<DataRepository<Language>>(),
113+
)..add(const SharedDataRequested()),
112114
),
113115
BlocProvider(
114116
create: (context) => DashboardBloc(
115-
dashboardSummaryRepository:
116-
context.read<DataRepository<DashboardSummary>>(),
117+
dashboardSummaryRepository: context
118+
.read<DataRepository<DashboardSummary>>(),
117119
headlinesRepository: context.read<DataRepository<Headline>>(),
118120
topicsRepository: context.read<DataRepository<Topic>>(),
119121
sourcesRepository: context.read<DataRepository<Source>>(),

lib/content_management/bloc/archived_headlines/archived_headlines_bloc.dart

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@ class ArchivedHeadlinesBloc
1212
extends Bloc<ArchivedHeadlinesEvent, ArchivedHeadlinesState> {
1313
ArchivedHeadlinesBloc({
1414
required DataRepository<Headline> headlinesRepository,
15-
}) : _headlinesRepository = headlinesRepository,
16-
super(const ArchivedHeadlinesState()) {
15+
}) : _headlinesRepository = headlinesRepository,
16+
super(const ArchivedHeadlinesState()) {
1717
on<LoadArchivedHeadlinesRequested>(_onLoadArchivedHeadlinesRequested);
1818
on<RestoreHeadlineRequested>(_onRestoreHeadlineRequested);
1919
on<DeleteHeadlineForeverRequested>(_onDeleteHeadlineForeverRequested);

lib/content_management/bloc/archived_sources/archived_sources_bloc.dart

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@ class ArchivedSourcesBloc
1010
extends Bloc<ArchivedSourcesEvent, ArchivedSourcesState> {
1111
ArchivedSourcesBloc({
1212
required DataRepository<Source> sourcesRepository,
13-
}) : _sourcesRepository = sourcesRepository,
14-
super(const ArchivedSourcesState()) {
13+
}) : _sourcesRepository = sourcesRepository,
14+
super(const ArchivedSourcesState()) {
1515
on<LoadArchivedSourcesRequested>(_onLoadArchivedSourcesRequested);
1616
on<RestoreSourceRequested>(_onRestoreSourceRequested);
1717
}

lib/content_management/bloc/archived_sources/archived_sources_state.dart

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -46,11 +46,11 @@ class ArchivedSourcesState extends Equatable {
4646

4747
@override
4848
List<Object?> get props => [
49-
status,
50-
sources,
51-
cursor,
52-
hasMore,
53-
exception,
54-
restoredSource,
55-
];
49+
status,
50+
sources,
51+
cursor,
52+
hasMore,
53+
exception,
54+
restoredSource,
55+
];
5656
}

lib/content_management/bloc/archived_topics/archived_topics_bloc.dart

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@ class ArchivedTopicsBloc
1010
extends Bloc<ArchivedTopicsEvent, ArchivedTopicsState> {
1111
ArchivedTopicsBloc({
1212
required DataRepository<Topic> topicsRepository,
13-
}) : _topicsRepository = topicsRepository,
14-
super(const ArchivedTopicsState()) {
13+
}) : _topicsRepository = topicsRepository,
14+
super(const ArchivedTopicsState()) {
1515
on<LoadArchivedTopicsRequested>(_onLoadArchivedTopicsRequested);
1616
on<RestoreTopicRequested>(_onRestoreTopicRequested);
1717
}

lib/content_management/bloc/archived_topics/archived_topics_state.dart

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -46,11 +46,11 @@ class ArchivedTopicsState extends Equatable {
4646

4747
@override
4848
List<Object?> get props => [
49-
status,
50-
topics,
51-
cursor,
52-
hasMore,
53-
exception,
54-
restoredTopic,
55-
];
49+
status,
50+
topics,
51+
cursor,
52+
hasMore,
53+
exception,
54+
restoredTopic,
55+
];
5656
}

lib/content_management/bloc/content_management_bloc.dart

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,15 @@ class ContentManagementBloc
2424
required DataRepository<Headline> headlinesRepository,
2525
required DataRepository<Topic> topicsRepository,
2626
required DataRepository<Source> sourcesRepository,
27+
required DataRepository<Country> countriesRepository,
28+
required DataRepository<Language> languagesRepository,
2729
}) : _headlinesRepository = headlinesRepository,
2830
_topicsRepository = topicsRepository,
2931
_sourcesRepository = sourcesRepository,
32+
_countriesRepository = countriesRepository,
33+
_languagesRepository = languagesRepository,
3034
super(const ContentManagementState()) {
35+
on<SharedDataRequested>(_onSharedDataRequested);
3136
on<ContentManagementTabChanged>(_onContentManagementTabChanged);
3237
on<LoadHeadlinesRequested>(_onLoadHeadlinesRequested);
3338
on<HeadlineUpdated>(_onHeadlineUpdated);
@@ -43,6 +48,99 @@ class ContentManagementBloc
4348
final DataRepository<Headline> _headlinesRepository;
4449
final DataRepository<Topic> _topicsRepository;
4550
final DataRepository<Source> _sourcesRepository;
51+
final DataRepository<Country> _countriesRepository;
52+
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.
59+
Future<void> _onSharedDataRequested(
60+
SharedDataRequested event,
61+
Emitter<ContentManagementState> emit,
62+
) 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+
86+
// Check if data is already loaded or is currently loading to prevent
87+
// redundant fetches.
88+
if (state.allCountriesStatus == ContentManagementStatus.success &&
89+
state.allLanguagesStatus == ContentManagementStatus.success) {
90+
return;
91+
}
92+
93+
// Set loading status for both lists.
94+
emit(
95+
state.copyWith(
96+
allCountriesStatus: ContentManagementStatus.loading,
97+
allLanguagesStatus: ContentManagementStatus.loading,
98+
),
99+
);
100+
101+
try {
102+
// Fetch both lists in parallel.
103+
final results = await Future.wait([
104+
fetchAll<Country>(
105+
repository: _countriesRepository,
106+
sort: [const SortOption('name', SortOrder.asc)],
107+
),
108+
fetchAll<Language>(
109+
repository: _languagesRepository,
110+
sort: [const SortOption('name', SortOrder.asc)],
111+
),
112+
]);
113+
114+
final countries = results[0] as List<Country>;
115+
final languages = results[1] as List<Language>;
116+
117+
// Update the state with the complete lists.
118+
emit(
119+
state.copyWith(
120+
allCountries: countries,
121+
allCountriesStatus: ContentManagementStatus.success,
122+
allLanguages: languages,
123+
allLanguagesStatus: ContentManagementStatus.success,
124+
),
125+
);
126+
} on HttpException catch (e) {
127+
emit(
128+
state.copyWith(
129+
allCountriesStatus: ContentManagementStatus.failure,
130+
allLanguagesStatus: ContentManagementStatus.failure,
131+
exception: e,
132+
),
133+
);
134+
} catch (e) {
135+
emit(
136+
state.copyWith(
137+
allCountriesStatus: ContentManagementStatus.failure,
138+
allLanguagesStatus: ContentManagementStatus.failure,
139+
exception: UnknownException('An unexpected error occurred: $e'),
140+
),
141+
);
142+
}
143+
}
46144

47145
void _onContentManagementTabChanged(
48146
ContentManagementTabChanged event,

lib/content_management/bloc/content_management_event.dart

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,3 +155,12 @@ final class SourceUpdated extends ContentManagementEvent {
155155
@override
156156
List<Object?> get props => [source];
157157
}
158+
159+
/// {@template shared_data_requested}
160+
/// Event to request loading of shared data like countries and languages.
161+
/// This should be dispatched once when the content management section is loaded.
162+
/// {@endtemplate}
163+
final class SharedDataRequested extends ContentManagementEvent {
164+
/// {@macro shared_data_requested}
165+
const SharedDataRequested();
166+
}

lib/content_management/bloc/content_management_state.dart

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,10 @@ class ContentManagementState extends Equatable {
3232
this.sources = const [],
3333
this.sourcesCursor,
3434
this.sourcesHasMore = false,
35+
this.allCountriesStatus = ContentManagementStatus.initial,
36+
this.allCountries = const [],
37+
this.allLanguagesStatus = ContentManagementStatus.initial,
38+
this.allLanguages = const [],
3539
this.exception,
3640
});
3741

@@ -74,6 +78,18 @@ class ContentManagementState extends Equatable {
7478
/// Indicates if there are more sources to load.
7579
final bool sourcesHasMore;
7680

81+
/// Status of all countries data operations.
82+
final ContentManagementStatus allCountriesStatus;
83+
84+
/// Cached list of all countries.
85+
final List<Country> allCountries;
86+
87+
/// Status of all languages data operations.
88+
final ContentManagementStatus allLanguagesStatus;
89+
90+
/// Cached list of all languages.
91+
final List<Language> allLanguages;
92+
7793
/// The error describing an operation failure, if any.
7894
final HttpException? exception;
7995

@@ -92,6 +108,10 @@ class ContentManagementState extends Equatable {
92108
List<Source>? sources,
93109
String? sourcesCursor,
94110
bool? sourcesHasMore,
111+
ContentManagementStatus? allCountriesStatus,
112+
List<Country>? allCountries,
113+
ContentManagementStatus? allLanguagesStatus,
114+
List<Language>? allLanguages,
95115
HttpException? exception,
96116
}) {
97117
return ContentManagementState(
@@ -108,6 +128,10 @@ class ContentManagementState extends Equatable {
108128
sources: sources ?? this.sources,
109129
sourcesCursor: sourcesCursor ?? this.sourcesCursor,
110130
sourcesHasMore: sourcesHasMore ?? this.sourcesHasMore,
131+
allCountriesStatus: allCountriesStatus ?? this.allCountriesStatus,
132+
allCountries: allCountries ?? this.allCountries,
133+
allLanguagesStatus: allLanguagesStatus ?? this.allLanguagesStatus,
134+
allLanguages: allLanguages ?? this.allLanguages,
111135
exception: exception ?? this.exception,
112136
);
113137
}
@@ -127,6 +151,10 @@ class ContentManagementState extends Equatable {
127151
sources,
128152
sourcesCursor,
129153
sourcesHasMore,
154+
allCountriesStatus,
155+
allCountries,
156+
allLanguagesStatus,
157+
allLanguages,
130158
exception,
131159
];
132160
}

lib/content_management/bloc/create_headline/create_headline_bloc.dart

Lines changed: 2 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,6 @@ import 'package:uuid/uuid.dart';
88
part 'create_headline_event.dart';
99
part 'create_headline_state.dart';
1010

11-
final class _FetchNextCountryPage extends CreateHeadlineEvent {
12-
const _FetchNextCountryPage();
13-
}
14-
1511
/// A BLoC to manage the state of creating a new headline.
1612
class CreateHeadlineBloc
1713
extends Bloc<CreateHeadlineEvent, CreateHeadlineState> {
@@ -20,12 +16,11 @@ class CreateHeadlineBloc
2016
required DataRepository<Headline> headlinesRepository,
2117
required DataRepository<Source> sourcesRepository,
2218
required DataRepository<Topic> topicsRepository,
23-
required DataRepository<Country> countriesRepository,
19+
required List<Country> countries,
2420
}) : _headlinesRepository = headlinesRepository,
2521
_sourcesRepository = sourcesRepository,
2622
_topicsRepository = topicsRepository,
27-
_countriesRepository = countriesRepository,
28-
super(const CreateHeadlineState()) {
23+
super(CreateHeadlineState(countries: countries)) {
2924
on<CreateHeadlineDataLoaded>(_onDataLoaded);
3025
on<CreateHeadlineTitleChanged>(_onTitleChanged);
3126
on<CreateHeadlineExcerptChanged>(_onExcerptChanged);
@@ -36,13 +31,11 @@ class CreateHeadlineBloc
3631
on<CreateHeadlineCountryChanged>(_onCountryChanged);
3732
on<CreateHeadlineStatusChanged>(_onStatusChanged);
3833
on<CreateHeadlineSubmitted>(_onSubmitted);
39-
on<_FetchNextCountryPage>(_onFetchNextCountryPage);
4034
}
4135

4236
final DataRepository<Headline> _headlinesRepository;
4337
final DataRepository<Source> _sourcesRepository;
4438
final DataRepository<Topic> _topicsRepository;
45-
final DataRepository<Country> _countriesRepository;
4639
final _uuid = const Uuid();
4740

4841
Future<void> _onDataLoaded(
@@ -65,26 +58,13 @@ class CreateHeadlineBloc
6558
final sources = (sourcesResponse as PaginatedResponse<Source>).items;
6659
final topics = (topicsResponse as PaginatedResponse<Topic>).items;
6760

68-
final countriesResponse = await _countriesRepository.readAll(
69-
sort: [const SortOption('name', SortOrder.asc)],
70-
);
71-
7261
emit(
7362
state.copyWith(
7463
status: CreateHeadlineStatus.initial,
7564
sources: sources,
7665
topics: topics,
77-
countries: countriesResponse.items,
78-
countriesCursor: countriesResponse.cursor,
79-
countriesHasMore: countriesResponse.hasMore,
8066
),
8167
);
82-
83-
// After the initial page of countries is loaded, start a background
84-
// process to fetch all remaining pages.
85-
if (state.countriesHasMore) {
86-
add(const _FetchNextCountryPage());
87-
}
8868
} on HttpException catch (e) {
8969
emit(state.copyWith(status: CreateHeadlineStatus.failure, exception: e));
9070
} catch (e) {
@@ -159,49 +139,6 @@ class CreateHeadlineBloc
159139
}
160140

161141
// --- Background Data Fetching for Dropdown ---
162-
// The DropdownButtonFormField widget does not natively support on-scroll
163-
// pagination. To preserve UI consistency across the application, this BLoC
164-
// employs an event-driven background fetching mechanism.
165-
//
166-
// After the first page of items is loaded, a chain of events is initiated
167-
// to progressively fetch all remaining pages. This process is throttled
168-
// and runs in the background, ensuring the UI remains responsive while the
169-
// full list of dropdown options is populated over time.
170-
Future<void> _onFetchNextCountryPage(
171-
_FetchNextCountryPage event,
172-
Emitter<CreateHeadlineState> emit,
173-
) async {
174-
if (!state.countriesHasMore || state.countriesIsLoadingMore) return;
175-
176-
try {
177-
emit(state.copyWith(countriesIsLoadingMore: true));
178-
179-
// ignore: inference_failure_on_instance_creation
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-
205142
Future<void> _onSubmitted(
206143
CreateHeadlineSubmitted event,
207144
Emitter<CreateHeadlineState> emit,

0 commit comments

Comments
 (0)