Skip to content

Commit 4e0aa71

Browse files
authored
Merge pull request #80 from flutter-news-app-full-source-code/Implementing-a-Robust-Drafting-Feature-for-Headline-Content-Management
Implementing a robust drafting feature for headline content management
2 parents 065b4e5 + c515c91 commit 4e0aa71

Some content is hidden

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

50 files changed

+3327
-379
lines changed

README.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,10 @@ Click on any category to explore.
2727

2828
### 📰 Comprehensive Content Management
2929
Manage the entire lifecycle of your news content with full CRUD (Create, Read, Update, Delete) capabilities, complemented by advanced archiving and restoration features:
30-
- **Headlines:** Create, edit, publish, archive, restore, and permanently delete news articles.
31-
- **Topics:** Organize, define, archive, and restore news topics.
32-
- **Sources:** Maintain, update, archive, and restore news sources.
33-
> **💡 Your Advantage:** Gain detailed control over your content. This centralized system ensures accuracy and consistency, allowing you to manage active content and easily retrieve or remove archived items.
30+
- **Headlines:** Draft, Create, edit, publish, archive, restore, and permanently delete news articles.
31+
- **Topics:** Draft, Create, edit, archive, and restore news topics.
32+
- **Sources:** Draft, Create, edit, archive, and restore news sources.
33+
> **💡 Your Advantage:** Gain detailed control over your content. This centralized system ensures accuracy and consistency, allowing you to manage draft/active content and easily retrieve or remove archived items.
3434
3535
---
3636

lib/bloc_observer.dart

Lines changed: 5 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -15,39 +15,16 @@ class AppBlocObserver extends BlocObserver {
1515

1616
// Initialize state information strings.
1717
// By default, truncate the full string representation of the state
18-
// to the first 250 characters to prevent excessively long logs.
19-
var oldStateInfo = oldState.toString().substring(
18+
// to the first 150 characters to prevent excessively long logs.
19+
final oldStateInfo = oldState.toString().substring(
2020
0,
21-
oldState.toString().length > 250 ? 250 : oldState.toString().length,
21+
oldState.toString().length > 150 ? 150 : oldState.toString().length,
2222
);
23-
var newStateInfo = newState.toString().substring(
23+
final newStateInfo = newState.toString().substring(
2424
0,
25-
newState.toString().length > 250 ? 250 : newState.toString().length,
25+
newState.toString().length > 150 ? 150 : newState.toString().length,
2626
);
2727

28-
try {
29-
// Attempt to access a 'status' property on the state objects.
30-
// Many BLoC states use a 'status' property (e.g., Loading, Success, Failure)
31-
// to represent their current lifecycle phase. If this property exists
32-
// and is not null, prioritize logging its value for conciseness.
33-
if (oldState.status != null) {
34-
oldStateInfo = 'status: ${oldState.status}';
35-
}
36-
if (newState.status != null) {
37-
newStateInfo = 'status: ${newState.status}';
38-
}
39-
} catch (e) {
40-
// This catch block handles cases where:
41-
// 1. The 'status' property does not exist on the state object (NoSuchMethodError).
42-
// 2. Accessing 'status' throws any other runtime error.
43-
// In such scenarios, the `oldStateInfo` and `newStateInfo` variables
44-
// will retain their initially truncated string representations,
45-
// providing a fallback for states without a 'status' property.
46-
// Log the error for debugging purposes, but do not rethrow to avoid
47-
// crashing the observer.
48-
log('Error accessing status property for ${bloc.runtimeType}: $e');
49-
}
50-
5128
// Log the state change, including the BLoC type and the old and new state information.
5229
log('onChange(${bloc.runtimeType}, $oldStateInfo -> $newStateInfo)');
5330
}

lib/content_management/bloc/content_management_state.dart

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,6 @@ class ContentManagementState extends Equatable {
108108
sources: sources ?? this.sources,
109109
sourcesCursor: sourcesCursor ?? this.sourcesCursor,
110110
sourcesHasMore: sourcesHasMore ?? this.sourcesHasMore,
111-
112111
exception: exception ?? this.exception,
113112
);
114113
}

lib/content_management/bloc/create_headline/create_headline_bloc.dart

Lines changed: 44 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ class CreateHeadlineBloc
1515
CreateHeadlineBloc({
1616
required DataRepository<Headline> headlinesRepository,
1717
}) : _headlinesRepository = headlinesRepository,
18-
1918
super(const CreateHeadlineState()) {
2019
on<CreateHeadlineTitleChanged>(_onTitleChanged);
2120
on<CreateHeadlineExcerptChanged>(_onExcerptChanged);
@@ -24,8 +23,8 @@ class CreateHeadlineBloc
2423
on<CreateHeadlineSourceChanged>(_onSourceChanged);
2524
on<CreateHeadlineTopicChanged>(_onTopicChanged);
2625
on<CreateHeadlineCountryChanged>(_onCountryChanged);
27-
on<CreateHeadlineStatusChanged>(_onStatusChanged);
28-
on<CreateHeadlineSubmitted>(_onSubmitted);
26+
on<CreateHeadlineSavedAsDraft>(_onSavedAsDraft);
27+
on<CreateHeadlinePublished>(_onPublished);
2928
}
3029

3130
final DataRepository<Headline> _headlinesRepository;
@@ -81,24 +80,52 @@ class CreateHeadlineBloc
8180
emit(state.copyWith(eventCountry: () => event.country));
8281
}
8382

84-
void _onStatusChanged(
85-
CreateHeadlineStatusChanged event,
83+
/// Handles saving the headline as a draft.
84+
Future<void> _onSavedAsDraft(
85+
CreateHeadlineSavedAsDraft event,
8686
Emitter<CreateHeadlineState> emit,
87-
) {
88-
emit(
89-
state.copyWith(
90-
contentStatus: event.status,
91-
status: CreateHeadlineStatus.initial,
92-
),
93-
);
87+
) async {
88+
emit(state.copyWith(status: CreateHeadlineStatus.submitting));
89+
try {
90+
final now = DateTime.now();
91+
final newHeadline = Headline(
92+
id: _uuid.v4(),
93+
title: state.title,
94+
excerpt: state.excerpt,
95+
url: state.url,
96+
imageUrl: state.imageUrl,
97+
source: state.source!,
98+
eventCountry: state.eventCountry!,
99+
topic: state.topic!,
100+
createdAt: now,
101+
updatedAt: now,
102+
status: ContentStatus.draft,
103+
);
104+
105+
await _headlinesRepository.create(item: newHeadline);
106+
emit(
107+
state.copyWith(
108+
status: CreateHeadlineStatus.success,
109+
createdHeadline: newHeadline,
110+
),
111+
);
112+
} on HttpException catch (e) {
113+
emit(state.copyWith(status: CreateHeadlineStatus.failure, exception: e));
114+
} catch (e) {
115+
emit(
116+
state.copyWith(
117+
status: CreateHeadlineStatus.failure,
118+
exception: UnknownException('An unexpected error occurred: $e'),
119+
),
120+
);
121+
}
94122
}
95123

96-
Future<void> _onSubmitted(
97-
CreateHeadlineSubmitted event,
124+
/// Handles publishing the headline.
125+
Future<void> _onPublished(
126+
CreateHeadlinePublished event,
98127
Emitter<CreateHeadlineState> emit,
99128
) async {
100-
if (!state.isFormValid) return;
101-
102129
emit(state.copyWith(status: CreateHeadlineStatus.submitting));
103130
try {
104131
final now = DateTime.now();
@@ -113,7 +140,7 @@ class CreateHeadlineBloc
113140
topic: state.topic!,
114141
createdAt: now,
115142
updatedAt: now,
116-
status: state.contentStatus,
143+
status: ContentStatus.active,
117144
);
118145

119146
await _headlinesRepository.create(item: newHeadline);

lib/content_management/bloc/create_headline/create_headline_event.dart

Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -64,17 +64,12 @@ final class CreateHeadlineCountryChanged extends CreateHeadlineEvent {
6464
List<Object?> get props => [country];
6565
}
6666

67-
/// Event for when the headline's status is changed.
68-
final class CreateHeadlineStatusChanged extends CreateHeadlineEvent {
69-
const CreateHeadlineStatusChanged(this.status);
70-
71-
final ContentStatus status;
72-
73-
@override
74-
List<Object?> get props => [status];
67+
/// Event to save the headline as a draft.
68+
final class CreateHeadlineSavedAsDraft extends CreateHeadlineEvent {
69+
const CreateHeadlineSavedAsDraft();
7570
}
7671

77-
/// Event to signal that the form should be submitted.
78-
final class CreateHeadlineSubmitted extends CreateHeadlineEvent {
79-
const CreateHeadlineSubmitted();
72+
/// Event to publish the headline.
73+
final class CreateHeadlinePublished extends CreateHeadlineEvent {
74+
const CreateHeadlinePublished();
8075
}

lib/content_management/bloc/create_headline/create_headline_state.dart

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@ final class CreateHeadlineState extends Equatable {
2929
this.source,
3030
this.topic,
3131
this.eventCountry,
32-
this.contentStatus = ContentStatus.active,
3332
this.exception,
3433
this.createdHeadline,
3534
});
@@ -42,7 +41,6 @@ final class CreateHeadlineState extends Equatable {
4241
final Source? source;
4342
final Topic? topic;
4443
final Country? eventCountry;
45-
final ContentStatus contentStatus;
4644
final HttpException? exception;
4745
final Headline? createdHeadline;
4846

@@ -65,7 +63,6 @@ final class CreateHeadlineState extends Equatable {
6563
ValueGetter<Source?>? source,
6664
ValueGetter<Topic?>? topic,
6765
ValueGetter<Country?>? eventCountry,
68-
ContentStatus? contentStatus,
6966
HttpException? exception,
7067
Headline? createdHeadline,
7168
}) {
@@ -78,7 +75,6 @@ final class CreateHeadlineState extends Equatable {
7875
source: source != null ? source() : this.source,
7976
topic: topic != null ? topic() : this.topic,
8077
eventCountry: eventCountry != null ? eventCountry() : this.eventCountry,
81-
contentStatus: contentStatus ?? this.contentStatus,
8278
exception: exception,
8379
createdHeadline: createdHeadline ?? this.createdHeadline,
8480
);
@@ -94,7 +90,6 @@ final class CreateHeadlineState extends Equatable {
9490
source,
9591
topic,
9692
eventCountry,
97-
contentStatus,
9893
exception,
9994
createdHeadline,
10095
];

lib/content_management/bloc/create_source/create_source_bloc.dart

Lines changed: 43 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,8 @@ class CreateSourceBloc extends Bloc<CreateSourceEvent, CreateSourceState> {
2121
on<CreateSourceTypeChanged>(_onSourceTypeChanged);
2222
on<CreateSourceLanguageChanged>(_onLanguageChanged);
2323
on<CreateSourceHeadquartersChanged>(_onHeadquartersChanged);
24-
on<CreateSourceStatusChanged>(_onStatusChanged);
25-
on<CreateSourceSubmitted>(_onSubmitted);
24+
on<CreateSourceSavedAsDraft>(_onSavedAsDraft);
25+
on<CreateSourcePublished>(_onPublished);
2626
}
2727

2828
final DataRepository<Source> _sourcesRepository;
@@ -70,24 +70,51 @@ class CreateSourceBloc extends Bloc<CreateSourceEvent, CreateSourceState> {
7070
emit(state.copyWith(headquarters: () => event.headquarters));
7171
}
7272

73-
void _onStatusChanged(
74-
CreateSourceStatusChanged event,
73+
/// Handles saving the source as a draft.
74+
Future<void> _onSavedAsDraft(
75+
CreateSourceSavedAsDraft event,
7576
Emitter<CreateSourceState> emit,
76-
) {
77-
emit(
78-
state.copyWith(
79-
contentStatus: event.status,
80-
status: CreateSourceStatus.initial,
81-
),
82-
);
77+
) async {
78+
emit(state.copyWith(status: CreateSourceStatus.submitting));
79+
try {
80+
final now = DateTime.now();
81+
final newSource = Source(
82+
id: _uuid.v4(),
83+
name: state.name,
84+
description: state.description,
85+
url: state.url,
86+
sourceType: state.sourceType!,
87+
language: state.language!,
88+
createdAt: now,
89+
updatedAt: now,
90+
headquarters: state.headquarters!,
91+
status: ContentStatus.draft,
92+
);
93+
94+
await _sourcesRepository.create(item: newSource);
95+
emit(
96+
state.copyWith(
97+
status: CreateSourceStatus.success,
98+
createdSource: newSource,
99+
),
100+
);
101+
} on HttpException catch (e) {
102+
emit(state.copyWith(status: CreateSourceStatus.failure, exception: e));
103+
} catch (e) {
104+
emit(
105+
state.copyWith(
106+
status: CreateSourceStatus.failure,
107+
exception: UnknownException('An unexpected error occurred: $e'),
108+
),
109+
);
110+
}
83111
}
84112

85-
Future<void> _onSubmitted(
86-
CreateSourceSubmitted event,
113+
/// Handles publishing the source.
114+
Future<void> _onPublished(
115+
CreateSourcePublished event,
87116
Emitter<CreateSourceState> emit,
88117
) async {
89-
if (!state.isFormValid) return;
90-
91118
emit(state.copyWith(status: CreateSourceStatus.submitting));
92119
try {
93120
final now = DateTime.now();
@@ -101,7 +128,7 @@ class CreateSourceBloc extends Bloc<CreateSourceEvent, CreateSourceState> {
101128
createdAt: now,
102129
updatedAt: now,
103130
headquarters: state.headquarters!,
104-
status: state.contentStatus,
131+
status: ContentStatus.active,
105132
);
106133

107134
await _sourcesRepository.create(item: newSource);

lib/content_management/bloc/create_source/create_source_event.dart

Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -56,17 +56,12 @@ final class CreateSourceHeadquartersChanged extends CreateSourceEvent {
5656
List<Object?> get props => [headquarters];
5757
}
5858

59-
/// Event for when the source's status is changed.
60-
final class CreateSourceStatusChanged extends CreateSourceEvent {
61-
const CreateSourceStatusChanged(this.status);
62-
63-
final ContentStatus status;
64-
65-
@override
66-
List<Object?> get props => [status];
59+
/// Event to save the source as a draft.
60+
final class CreateSourceSavedAsDraft extends CreateSourceEvent {
61+
const CreateSourceSavedAsDraft();
6762
}
6863

69-
/// Event to signal that the form should be submitted.
70-
final class CreateSourceSubmitted extends CreateSourceEvent {
71-
const CreateSourceSubmitted();
64+
/// Event to publish the source.
65+
final class CreateSourcePublished extends CreateSourceEvent {
66+
const CreateSourcePublished();
7267
}

lib/content_management/bloc/create_source/create_source_state.dart

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@ final class CreateSourceState extends Equatable {
2929
this.sourceType,
3030
this.language,
3131
this.headquarters,
32-
this.contentStatus = ContentStatus.active,
3332
this.exception,
3433
this.createdSource,
3534
});
@@ -41,7 +40,6 @@ final class CreateSourceState extends Equatable {
4140
final SourceType? sourceType;
4241
final Language? language;
4342
final Country? headquarters;
44-
final ContentStatus contentStatus;
4543
final HttpException? exception;
4644
final Source? createdSource;
4745

@@ -62,7 +60,6 @@ final class CreateSourceState extends Equatable {
6260
ValueGetter<SourceType?>? sourceType,
6361
ValueGetter<Language?>? language,
6462
ValueGetter<Country?>? headquarters,
65-
ContentStatus? contentStatus,
6663
HttpException? exception,
6764
Source? createdSource,
6865
}) {
@@ -74,7 +71,6 @@ final class CreateSourceState extends Equatable {
7471
sourceType: sourceType != null ? sourceType() : this.sourceType,
7572
language: language != null ? language() : this.language,
7673
headquarters: headquarters != null ? headquarters() : this.headquarters,
77-
contentStatus: contentStatus ?? this.contentStatus,
7874
exception: exception,
7975
createdSource: createdSource ?? this.createdSource,
8076
);
@@ -89,7 +85,6 @@ final class CreateSourceState extends Equatable {
8985
sourceType,
9086
language,
9187
headquarters,
92-
contentStatus,
9388
exception,
9489
createdSource,
9590
];

0 commit comments

Comments
 (0)