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 @@ -12,8 +12,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 @@ -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,
];
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,6 @@ import 'package:uuid/uuid.dart';
part 'create_headline_event.dart';
part 'create_headline_state.dart';

final class _FetchNextCountryPage extends CreateHeadlineEvent {
const _FetchNextCountryPage();
}

/// A BLoC to manage the state of creating a new headline.
class CreateHeadlineBloc
extends Bloc<CreateHeadlineEvent, CreateHeadlineState> {
Expand All @@ -20,12 +16,11 @@ class CreateHeadlineBloc
required DataRepository<Headline> headlinesRepository,
required DataRepository<Source> sourcesRepository,
required DataRepository<Topic> topicsRepository,
required DataRepository<Country> countriesRepository,
required List<Country> countries,
}) : _headlinesRepository = headlinesRepository,
_sourcesRepository = sourcesRepository,
_topicsRepository = topicsRepository,
_countriesRepository = countriesRepository,
super(const CreateHeadlineState()) {
super(CreateHeadlineState(countries: countries)) {
on<CreateHeadlineDataLoaded>(_onDataLoaded);
on<CreateHeadlineTitleChanged>(_onTitleChanged);
on<CreateHeadlineExcerptChanged>(_onExcerptChanged);
Expand All @@ -36,13 +31,11 @@ class CreateHeadlineBloc
on<CreateHeadlineCountryChanged>(_onCountryChanged);
on<CreateHeadlineStatusChanged>(_onStatusChanged);
on<CreateHeadlineSubmitted>(_onSubmitted);
on<_FetchNextCountryPage>(_onFetchNextCountryPage);
}

final DataRepository<Headline> _headlinesRepository;
final DataRepository<Source> _sourcesRepository;
final DataRepository<Topic> _topicsRepository;
final DataRepository<Country> _countriesRepository;
final _uuid = const Uuid();

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

final countriesResponse = await _countriesRepository.readAll(
sort: [const SortOption('name', SortOrder.asc)],
);

emit(
state.copyWith(
status: CreateHeadlineStatus.initial,
sources: sources,
topics: topics,
countries: countriesResponse.items,
countriesCursor: countriesResponse.cursor,
countriesHasMore: countriesResponse.hasMore,
),
);

// After the initial page of countries is loaded, start a background
// process to fetch all remaining pages.
if (state.countriesHasMore) {
add(const _FetchNextCountryPage());
}
} on HttpException catch (e) {
emit(state.copyWith(status: CreateHeadlineStatus.failure, exception: e));
} catch (e) {
Expand Down Expand Up @@ -159,49 +139,6 @@ class CreateHeadlineBloc
}

// --- Background Data Fetching for 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.
//
// After the first page of items is loaded, a chain of events is initiated
// to progressively fetch all remaining pages. This process is throttled
// and runs in the background, ensuring the UI remains responsive while the
// full list of dropdown options is populated over time.
Future<void> _onFetchNextCountryPage(
_FetchNextCountryPage event,
Emitter<CreateHeadlineState> emit,
) async {
if (!state.countriesHasMore || state.countriesIsLoadingMore) return;

try {
emit(state.copyWith(countriesIsLoadingMore: true));

// ignore: inference_failure_on_instance_creation
await Future.delayed(const Duration(milliseconds: 400));

final nextCountries = await _countriesRepository.readAll(
pagination: PaginationOptions(cursor: state.countriesCursor),
sort: [const SortOption('name', SortOrder.asc)],
);

emit(
state.copyWith(
countries: List.of(state.countries)..addAll(nextCountries.items),
countriesCursor: nextCountries.cursor,
countriesHasMore: nextCountries.hasMore,
countriesIsLoadingMore: false,
),
);

if (nextCountries.hasMore) {
add(const _FetchNextCountryPage());
}
} catch (e) {
emit(state.copyWith(countriesIsLoadingMore: false));
// Optionally log the error without disrupting the user
}
}

Future<void> _onSubmitted(
CreateHeadlineSubmitted event,
Emitter<CreateHeadlineState> emit,
Expand Down
Loading
Loading