Skip to content
Merged
8 changes: 5 additions & 3 deletions lib/app/view/app.dart
Original file line number Diff line number Diff line change
Expand Up @@ -108,12 +108,14 @@ class App extends StatelessWidget {
headlinesRepository: context.read<DataRepository<Headline>>(),
topicsRepository: context.read<DataRepository<Topic>>(),
sourcesRepository: context.read<DataRepository<Source>>(),
),
countriesRepository: context.read<DataRepository<Country>>(),
languagesRepository: context.read<DataRepository<Language>>(),
)..add(const SharedDataRequested()),
),
BlocProvider(
create: (context) => DashboardBloc(
dashboardSummaryRepository:
context.read<DataRepository<DashboardSummary>>(),
dashboardSummaryRepository: context
.read<DataRepository<DashboardSummary>>(),
headlinesRepository: context.read<DataRepository<Headline>>(),
topicsRepository: context.read<DataRepository<Topic>>(),
sourcesRepository: context.read<DataRepository<Source>>(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ class ArchivedHeadlinesBloc
extends Bloc<ArchivedHeadlinesEvent, ArchivedHeadlinesState> {
ArchivedHeadlinesBloc({
required DataRepository<Headline> headlinesRepository,
}) : _headlinesRepository = headlinesRepository,
super(const ArchivedHeadlinesState()) {
}) : _headlinesRepository = headlinesRepository,
super(const ArchivedHeadlinesState()) {
on<LoadArchivedHeadlinesRequested>(_onLoadArchivedHeadlinesRequested);
on<RestoreHeadlineRequested>(_onRestoreHeadlineRequested);
on<DeleteHeadlineForeverRequested>(_onDeleteHeadlineForeverRequested);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,11 +46,11 @@ class ArchivedHeadlinesState extends Equatable {

@override
List<Object?> get props => [
status,
headlines,
cursor,
hasMore,
exception,
restoredHeadline,
];
status,
headlines,
cursor,
hasMore,
exception,
restoredHeadline,
];
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ class ArchivedSourcesBloc
extends Bloc<ArchivedSourcesEvent, ArchivedSourcesState> {
ArchivedSourcesBloc({
required DataRepository<Source> sourcesRepository,
}) : _sourcesRepository = sourcesRepository,
super(const ArchivedSourcesState()) {
}) : _sourcesRepository = sourcesRepository,
super(const ArchivedSourcesState()) {
on<LoadArchivedSourcesRequested>(_onLoadArchivedSourcesRequested);
on<RestoreSourceRequested>(_onRestoreSourceRequested);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,11 +46,11 @@ class ArchivedSourcesState extends Equatable {

@override
List<Object?> get props => [
status,
sources,
cursor,
hasMore,
exception,
restoredSource,
];
status,
sources,
cursor,
hasMore,
exception,
restoredSource,
];
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ class ArchivedTopicsBloc
extends Bloc<ArchivedTopicsEvent, ArchivedTopicsState> {
ArchivedTopicsBloc({
required DataRepository<Topic> topicsRepository,
}) : _topicsRepository = topicsRepository,
super(const ArchivedTopicsState()) {
}) : _topicsRepository = topicsRepository,
super(const ArchivedTopicsState()) {
on<LoadArchivedTopicsRequested>(_onLoadArchivedTopicsRequested);
on<RestoreTopicRequested>(_onRestoreTopicRequested);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,11 +46,11 @@ class ArchivedTopicsState extends Equatable {

@override
List<Object?> get props => [
status,
topics,
cursor,
hasMore,
exception,
restoredTopic,
];
status,
topics,
cursor,
hasMore,
exception,
restoredTopic,
];
}
98 changes: 98 additions & 0 deletions lib/content_management/bloc/content_management_bloc.dart
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,15 @@ class ContentManagementBloc
required DataRepository<Headline> headlinesRepository,
required DataRepository<Topic> topicsRepository,
required DataRepository<Source> sourcesRepository,
required DataRepository<Country> countriesRepository,
required DataRepository<Language> languagesRepository,
}) : _headlinesRepository = headlinesRepository,
_topicsRepository = topicsRepository,
_sourcesRepository = sourcesRepository,
_countriesRepository = countriesRepository,
_languagesRepository = languagesRepository,
super(const ContentManagementState()) {
on<SharedDataRequested>(_onSharedDataRequested);
on<ContentManagementTabChanged>(_onContentManagementTabChanged);
on<LoadHeadlinesRequested>(_onLoadHeadlinesRequested);
on<HeadlineUpdated>(_onHeadlineUpdated);
Expand All @@ -43,6 +48,99 @@ class ContentManagementBloc
final DataRepository<Headline> _headlinesRepository;
final DataRepository<Topic> _topicsRepository;
final DataRepository<Source> _sourcesRepository;
final DataRepository<Country> _countriesRepository;
final DataRepository<Language> _languagesRepository;

// --- Background Data Fetching for countries/languages for the ui Dropdown ---
//
// The DropdownButtonFormField widget does not natively support on-scroll
// pagination. To preserve UI consistency across the application, this BLoC
// employs an event-driven background fetching mechanism.
Future<void> _onSharedDataRequested(
SharedDataRequested event,
Emitter<ContentManagementState> emit,
) async {
// Helper function to fetch all items of a given type.
Future<List<T>> fetchAll<T>({
required DataRepository<T> repository,
required List<SortOption> sort,
}) async {
final allItems = <T>[];
String? cursor;
bool hasMore;

do {
final response = await repository.readAll(
sort: sort,
pagination: PaginationOptions(cursor: cursor),
filter: {'status': ContentStatus.active.name},
);
allItems.addAll(response.items);
cursor = response.cursor;
hasMore = response.hasMore;
} while (hasMore);

return allItems;
}

// Check if data is already loaded or is currently loading to prevent
// redundant fetches.
if (state.allCountriesStatus == ContentManagementStatus.success &&
state.allLanguagesStatus == ContentManagementStatus.success) {
return;
}

// Set loading status for both lists.
emit(
state.copyWith(
allCountriesStatus: ContentManagementStatus.loading,
allLanguagesStatus: ContentManagementStatus.loading,
),
);

try {
// Fetch both lists in parallel.
final results = await Future.wait([
fetchAll<Country>(
repository: _countriesRepository,
sort: [const SortOption('name', SortOrder.asc)],
),
fetchAll<Language>(
repository: _languagesRepository,
sort: [const SortOption('name', SortOrder.asc)],
),
]);

final countries = results[0] as List<Country>;
final languages = results[1] as List<Language>;

// Update the state with the complete lists.
emit(
state.copyWith(
allCountries: countries,
allCountriesStatus: ContentManagementStatus.success,
allLanguages: languages,
allLanguagesStatus: ContentManagementStatus.success,
),
);
} on HttpException catch (e) {
emit(
state.copyWith(
allCountriesStatus: ContentManagementStatus.failure,
allLanguagesStatus: ContentManagementStatus.failure,
exception: e,
),
);
} catch (e) {
emit(
state.copyWith(
allCountriesStatus: ContentManagementStatus.failure,
allLanguagesStatus: ContentManagementStatus.failure,
exception: UnknownException('An unexpected error occurred: $e'),
),
);
}
}

void _onContentManagementTabChanged(
ContentManagementTabChanged event,
Expand Down
9 changes: 9 additions & 0 deletions lib/content_management/bloc/content_management_event.dart
Original file line number Diff line number Diff line change
Expand Up @@ -155,3 +155,12 @@ final class SourceUpdated extends ContentManagementEvent {
@override
List<Object?> get props => [source];
}

/// {@template shared_data_requested}
/// Event to request loading of shared data like countries and languages.
/// This should be dispatched once when the content management section is loaded.
/// {@endtemplate}
final class SharedDataRequested extends ContentManagementEvent {
/// {@macro shared_data_requested}
const SharedDataRequested();
}
28 changes: 28 additions & 0 deletions lib/content_management/bloc/content_management_state.dart
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ class ContentManagementState extends Equatable {
this.sources = const [],
this.sourcesCursor,
this.sourcesHasMore = false,
this.allCountriesStatus = ContentManagementStatus.initial,
this.allCountries = const [],
this.allLanguagesStatus = ContentManagementStatus.initial,
this.allLanguages = const [],
this.exception,
});

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

/// Status of all countries data operations.
final ContentManagementStatus allCountriesStatus;

/// Cached list of all countries.
final List<Country> allCountries;

/// Status of all languages data operations.
final ContentManagementStatus allLanguagesStatus;

/// Cached list of all languages.
final List<Language> allLanguages;

/// The error describing an operation failure, if any.
final HttpException? exception;

Expand All @@ -92,6 +108,10 @@ class ContentManagementState extends Equatable {
List<Source>? sources,
String? sourcesCursor,
bool? sourcesHasMore,
ContentManagementStatus? allCountriesStatus,
List<Country>? allCountries,
ContentManagementStatus? allLanguagesStatus,
List<Language>? allLanguages,
HttpException? exception,
}) {
return ContentManagementState(
Expand All @@ -108,6 +128,10 @@ class ContentManagementState extends Equatable {
sources: sources ?? this.sources,
sourcesCursor: sourcesCursor ?? this.sourcesCursor,
sourcesHasMore: sourcesHasMore ?? this.sourcesHasMore,
allCountriesStatus: allCountriesStatus ?? this.allCountriesStatus,
allCountries: allCountries ?? this.allCountries,
allLanguagesStatus: allLanguagesStatus ?? this.allLanguagesStatus,
allLanguages: allLanguages ?? this.allLanguages,
exception: exception ?? this.exception,
);
}
Expand All @@ -127,6 +151,10 @@ class ContentManagementState extends Equatable {
sources,
sourcesCursor,
sourcesHasMore,
allCountriesStatus,
allCountries,
allLanguagesStatus,
allLanguages,
exception,
];
}
Loading