Skip to content

Commit 997c93b

Browse files
authored
Merge pull request #69 from flutter-news-app-full-source-code/refactor-content-management
Refactor content management
2 parents 893fe7f + 9d59e15 commit 997c93b

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

41 files changed

+1854
-1553
lines changed

lib/app/view/app.dart

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -112,10 +112,7 @@ class App extends StatelessWidget {
112112
headlinesRepository: context.read<DataRepository<Headline>>(),
113113
topicsRepository: context.read<DataRepository<Topic>>(),
114114
sourcesRepository: context.read<DataRepository<Source>>(),
115-
countriesRepository: context.read<DataRepository<Country>>(),
116-
languagesRepository: context.read<DataRepository<Language>>(),
117-
fetchingService: context.read<ThrottledFetchingService>(),
118-
)..add(const SharedDataRequested()),
115+
),
119116
),
120117
BlocProvider(
121118
create: (context) => OverviewBloc(
@@ -233,8 +230,9 @@ class _AppViewState extends State<_AppView> {
233230
return Scaffold(
234231
// Use a distinct background color from the theme for the
235232
// areas outside the main constrained content.
236-
backgroundColor:
237-
Theme.of(context).colorScheme.surfaceContainerHighest,
233+
backgroundColor: Theme.of(
234+
context,
235+
).colorScheme.surfaceContainerHighest,
238236
body: Center(
239237
child: Card(
240238
// Remove default card margin to allow it to fill the

lib/content_management/bloc/content_management_bloc.dart

Lines changed: 30 additions & 130 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
1+
import 'dart:async';
2+
13
import 'package:bloc/bloc.dart';
24
import 'package:core/core.dart';
35
import 'package:data_repository/data_repository.dart';
46
import 'package:equatable/equatable.dart';
5-
import 'package:flutter_news_app_web_dashboard_full_source_code/shared/services/throttled_fetching_service.dart';
7+
import 'package:ui_kit/ui_kit.dart';
68

79
part 'content_management_event.dart';
810
part 'content_management_state.dart';
@@ -25,117 +27,51 @@ class ContentManagementBloc
2527
required DataRepository<Headline> headlinesRepository,
2628
required DataRepository<Topic> topicsRepository,
2729
required DataRepository<Source> sourcesRepository,
28-
required DataRepository<Country> countriesRepository,
29-
required DataRepository<Language> languagesRepository,
30-
required ThrottledFetchingService fetchingService,
3130
}) : _headlinesRepository = headlinesRepository,
3231
_topicsRepository = topicsRepository,
3332
_sourcesRepository = sourcesRepository,
34-
_countriesRepository = countriesRepository,
35-
_languagesRepository = languagesRepository,
36-
_fetchingService = fetchingService,
3733
super(const ContentManagementState()) {
38-
on<SharedDataRequested>(_onSharedDataRequested);
3934
on<ContentManagementTabChanged>(_onContentManagementTabChanged);
4035
on<LoadHeadlinesRequested>(_onLoadHeadlinesRequested);
41-
on<HeadlineUpdated>(_onHeadlineUpdated);
4236
on<ArchiveHeadlineRequested>(_onArchiveHeadlineRequested);
4337
on<LoadTopicsRequested>(_onLoadTopicsRequested);
44-
on<TopicUpdated>(_onTopicUpdated);
4538
on<ArchiveTopicRequested>(_onArchiveTopicRequested);
4639
on<LoadSourcesRequested>(_onLoadSourcesRequested);
47-
on<SourceUpdated>(_onSourceUpdated);
4840
on<ArchiveSourceRequested>(_onArchiveSourceRequested);
41+
42+
_headlineUpdateSubscription = _headlinesRepository.entityUpdated
43+
.where((type) => type == Headline)
44+
.listen((_) {
45+
add(const LoadHeadlinesRequested(limit: kDefaultRowsPerPage));
46+
});
47+
48+
_topicUpdateSubscription = _topicsRepository.entityUpdated
49+
.where((type) => type == Topic)
50+
.listen((_) {
51+
add(const LoadTopicsRequested(limit: kDefaultRowsPerPage));
52+
});
53+
54+
_sourceUpdateSubscription = _sourcesRepository.entityUpdated
55+
.where((type) => type == Source)
56+
.listen((_) {
57+
add(const LoadSourcesRequested(limit: kDefaultRowsPerPage));
58+
});
4959
}
5060

5161
final DataRepository<Headline> _headlinesRepository;
5262
final DataRepository<Topic> _topicsRepository;
5363
final DataRepository<Source> _sourcesRepository;
54-
final DataRepository<Country> _countriesRepository;
55-
final DataRepository<Language> _languagesRepository;
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.
78-
Future<void> _onSharedDataRequested(
79-
SharedDataRequested event,
80-
Emitter<ContentManagementState> emit,
81-
) async {
82-
// Check if data is already loaded or is currently loading to prevent
83-
// redundant fetches.
84-
if (state.allCountriesStatus == ContentManagementStatus.success &&
85-
state.allLanguagesStatus == ContentManagementStatus.success) {
86-
return;
87-
}
88-
89-
// Set loading status for both lists.
90-
emit(
91-
state.copyWith(
92-
allCountriesStatus: ContentManagementStatus.loading,
93-
allLanguagesStatus: ContentManagementStatus.loading,
94-
),
95-
);
96-
97-
try {
98-
// Fetch both lists in parallel using the dedicated fetching service.
99-
final results = await Future.wait([
100-
_fetchingService.fetchAll<Country>(
101-
repository: _countriesRepository,
102-
sort: [const SortOption('name', SortOrder.asc)],
103-
),
104-
_fetchingService.fetchAll<Language>(
105-
repository: _languagesRepository,
106-
sort: [const SortOption('name', SortOrder.asc)],
107-
),
108-
]);
10964

110-
final countries = results[0] as List<Country>;
111-
final languages = results[1] as List<Language>;
65+
late final StreamSubscription<Type> _headlineUpdateSubscription;
66+
late final StreamSubscription<Type> _topicUpdateSubscription;
67+
late final StreamSubscription<Type> _sourceUpdateSubscription;
11268

113-
// Update the state with the complete lists.
114-
emit(
115-
state.copyWith(
116-
allCountries: countries,
117-
allCountriesStatus: ContentManagementStatus.success,
118-
allLanguages: languages,
119-
allLanguagesStatus: ContentManagementStatus.success,
120-
),
121-
);
122-
} on HttpException catch (e) {
123-
emit(
124-
state.copyWith(
125-
allCountriesStatus: ContentManagementStatus.failure,
126-
allLanguagesStatus: ContentManagementStatus.failure,
127-
exception: e,
128-
),
129-
);
130-
} catch (e) {
131-
emit(
132-
state.copyWith(
133-
allCountriesStatus: ContentManagementStatus.failure,
134-
allLanguagesStatus: ContentManagementStatus.failure,
135-
exception: UnknownException('An unexpected error occurred: $e'),
136-
),
137-
);
138-
}
69+
@override
70+
Future<void> close() {
71+
_headlineUpdateSubscription.cancel();
72+
_topicUpdateSubscription.cancel();
73+
_sourceUpdateSubscription.cancel();
74+
return super.close();
13975
}
14076

14177
void _onContentManagementTabChanged(
@@ -226,18 +162,6 @@ class ContentManagementBloc
226162
}
227163
}
228164

229-
void _onHeadlineUpdated(
230-
HeadlineUpdated event,
231-
Emitter<ContentManagementState> emit,
232-
) {
233-
final updatedHeadlines = List<Headline>.from(state.headlines);
234-
final index = updatedHeadlines.indexWhere((h) => h.id == event.headline.id);
235-
if (index != -1) {
236-
updatedHeadlines[index] = event.headline;
237-
emit(state.copyWith(headlines: updatedHeadlines));
238-
}
239-
}
240-
241165
Future<void> _onLoadTopicsRequested(
242166
LoadTopicsRequested event,
243167
Emitter<ContentManagementState> emit,
@@ -319,18 +243,6 @@ class ContentManagementBloc
319243
}
320244
}
321245

322-
void _onTopicUpdated(
323-
TopicUpdated event,
324-
Emitter<ContentManagementState> emit,
325-
) {
326-
final updatedTopics = List<Topic>.from(state.topics);
327-
final index = updatedTopics.indexWhere((t) => t.id == event.topic.id);
328-
if (index != -1) {
329-
updatedTopics[index] = event.topic;
330-
emit(state.copyWith(topics: updatedTopics));
331-
}
332-
}
333-
334246
Future<void> _onLoadSourcesRequested(
335247
LoadSourcesRequested event,
336248
Emitter<ContentManagementState> emit,
@@ -411,16 +323,4 @@ class ContentManagementBloc
411323
);
412324
}
413325
}
414-
415-
void _onSourceUpdated(
416-
SourceUpdated event,
417-
Emitter<ContentManagementState> emit,
418-
) {
419-
final updatedSources = List<Source>.from(state.sources);
420-
final index = updatedSources.indexWhere((s) => s.id == event.source.id);
421-
if (index != -1) {
422-
updatedSources[index] = event.source;
423-
emit(state.copyWith(sources: updatedSources));
424-
}
425-
}
426326
}

lib/content_management/bloc/content_management_event.dart

Lines changed: 0 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -52,20 +52,6 @@ final class ArchiveHeadlineRequested extends ContentManagementEvent {
5252
List<Object?> get props => [id];
5353
}
5454

55-
/// {@template headline_updated}
56-
/// Event to update an existing headline in the local state.
57-
/// {@endtemplate}
58-
final class HeadlineUpdated extends ContentManagementEvent {
59-
/// {@macro headline_updated}
60-
const HeadlineUpdated(this.headline);
61-
62-
/// The headline that was updated.
63-
final Headline headline;
64-
65-
@override
66-
List<Object?> get props => [headline];
67-
}
68-
6955
/// {@template load_topics_requested}
7056
/// Event to request loading of topics.
7157
/// {@endtemplate}
@@ -97,20 +83,6 @@ final class ArchiveTopicRequested extends ContentManagementEvent {
9783
List<Object?> get props => [id];
9884
}
9985

100-
/// {@template topic_updated}
101-
/// Event to update an existing topic in the local state.
102-
/// {@endtemplate}
103-
final class TopicUpdated extends ContentManagementEvent {
104-
/// {@macro topic_updated}
105-
const TopicUpdated(this.topic);
106-
107-
/// The topic that was updated.
108-
final Topic topic;
109-
110-
@override
111-
List<Object?> get props => [topic];
112-
}
113-
11486
/// {@template load_sources_requested}
11587
/// Event to request loading of sources.
11688
/// {@endtemplate}
@@ -141,26 +113,3 @@ final class ArchiveSourceRequested extends ContentManagementEvent {
141113
@override
142114
List<Object?> get props => [id];
143115
}
144-
145-
/// {@template source_updated}
146-
/// Event to update an existing source in the local state.
147-
/// {@endtemplate}
148-
final class SourceUpdated extends ContentManagementEvent {
149-
/// {@macro source_updated}
150-
const SourceUpdated(this.source);
151-
152-
/// The source that was updated.
153-
final Source source;
154-
155-
@override
156-
List<Object?> get props => [source];
157-
}
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: 1 addition & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -32,10 +32,6 @@ 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 [],
3935
this.exception,
4036
});
4137

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

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-
9377
/// The error describing an operation failure, if any.
9478
final HttpException? exception;
9579

@@ -108,10 +92,6 @@ class ContentManagementState extends Equatable {
10892
List<Source>? sources,
10993
String? sourcesCursor,
11094
bool? sourcesHasMore,
111-
ContentManagementStatus? allCountriesStatus,
112-
List<Country>? allCountries,
113-
ContentManagementStatus? allLanguagesStatus,
114-
List<Language>? allLanguages,
11595
HttpException? exception,
11696
}) {
11797
return ContentManagementState(
@@ -128,10 +108,7 @@ class ContentManagementState extends Equatable {
128108
sources: sources ?? this.sources,
129109
sourcesCursor: sourcesCursor ?? this.sourcesCursor,
130110
sourcesHasMore: sourcesHasMore ?? this.sourcesHasMore,
131-
allCountriesStatus: allCountriesStatus ?? this.allCountriesStatus,
132-
allCountries: allCountries ?? this.allCountries,
133-
allLanguagesStatus: allLanguagesStatus ?? this.allLanguagesStatus,
134-
allLanguages: allLanguages ?? this.allLanguages,
111+
135112
exception: exception ?? this.exception,
136113
);
137114
}
@@ -151,10 +128,5 @@ class ContentManagementState extends Equatable {
151128
sources,
152129
sourcesCursor,
153130
sourcesHasMore,
154-
allCountriesStatus,
155-
allCountries,
156-
allLanguagesStatus,
157-
allLanguages,
158-
exception,
159131
];
160132
}

0 commit comments

Comments
 (0)