Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ import 'package:uuid/uuid.dart';
part 'create_headline_event.dart';
part 'create_headline_state.dart';

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

const _searchDebounceDuration = Duration(milliseconds: 300);

/// A BLoC to manage the state of creating a new headline.
Expand All @@ -20,11 +24,11 @@ class CreateHeadlineBloc
required DataRepository<Source> sourcesRepository,
required DataRepository<Topic> topicsRepository,
required DataRepository<Country> countriesRepository,
}) : _headlinesRepository = headlinesRepository,
_sourcesRepository = sourcesRepository,
_topicsRepository = topicsRepository,
_countriesRepository = countriesRepository,
super(const CreateHeadlineState()) {
}) : _headlinesRepository = headlinesRepository,
_sourcesRepository = sourcesRepository,
_topicsRepository = topicsRepository,
_countriesRepository = countriesRepository,
super(const CreateHeadlineState()) {
on<CreateHeadlineDataLoaded>(_onDataLoaded);
on<CreateHeadlineTitleChanged>(_onTitleChanged);
on<CreateHeadlineExcerptChanged>(_onExcerptChanged);
Expand All @@ -35,6 +39,7 @@ class CreateHeadlineBloc
on<CreateHeadlineCountryChanged>(_onCountryChanged);
on<CreateHeadlineStatusChanged>(_onStatusChanged);
on<CreateHeadlineSubmitted>(_onSubmitted);
on<_FetchNextCountryPage>(_onFetchNextCountryPage);
}

final DataRepository<Headline> _headlinesRepository;
Expand Down Expand Up @@ -76,19 +81,10 @@ class CreateHeadlineBloc
),
);

// Start background fetching for all countries
while (state.countriesHasMore) {
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,
),
);
// 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));
Expand Down Expand Up @@ -163,6 +159,49 @@ 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));

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
128 changes: 97 additions & 31 deletions lib/content_management/bloc/create_source/create_source_bloc.dart
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,14 @@ import 'package:uuid/uuid.dart';
part 'create_source_event.dart';
part 'create_source_state.dart';

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

final class _FetchNextLanguagePage extends CreateSourceEvent {
const _FetchNextLanguagePage();
}

const _searchDebounceDuration = Duration(milliseconds: 300);

/// A BLoC to manage the state of creating a new source.
Expand All @@ -18,10 +26,10 @@ class CreateSourceBloc extends Bloc<CreateSourceEvent, CreateSourceState> {
required DataRepository<Source> sourcesRepository,
required DataRepository<Country> countriesRepository,
required DataRepository<Language> languagesRepository,
}) : _sourcesRepository = sourcesRepository,
_countriesRepository = countriesRepository,
_languagesRepository = languagesRepository,
super(const CreateSourceState()) {
}) : _sourcesRepository = sourcesRepository,
_countriesRepository = countriesRepository,
_languagesRepository = languagesRepository,
super(const CreateSourceState()) {
on<CreateSourceDataLoaded>(_onDataLoaded);
on<CreateSourceNameChanged>(_onNameChanged);
on<CreateSourceDescriptionChanged>(_onDescriptionChanged);
Expand All @@ -31,6 +39,8 @@ class CreateSourceBloc extends Bloc<CreateSourceEvent, CreateSourceState> {
on<CreateSourceHeadquartersChanged>(_onHeadquartersChanged);
on<CreateSourceStatusChanged>(_onStatusChanged);
on<CreateSourceSubmitted>(_onSubmitted);
on<_FetchNextCountryPage>(_onFetchNextCountryPage);
on<_FetchNextLanguagePage>(_onFetchNextLanguagePage);
}

final DataRepository<Source> _sourcesRepository;
Expand Down Expand Up @@ -66,34 +76,13 @@ class CreateSourceBloc extends Bloc<CreateSourceEvent, CreateSourceState> {
),
);

// Start background fetching for all countries
while (state.countriesHasMore) {
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,
),
);
// After the initial page is loaded, start background processes to
// fetch all remaining pages for countries and languages.
if (state.countriesHasMore) {
add(const _FetchNextCountryPage());
}

// Start background fetching for all languages
while (state.languagesHasMore) {
final nextLanguages = await _languagesRepository.readAll(
pagination: PaginationOptions(cursor: state.languagesCursor),
sort: [const SortOption('name', SortOrder.asc)],
);
emit(
state.copyWith(
languages: List.of(state.languages)..addAll(nextLanguages.items),
languagesCursor: nextLanguages.cursor,
languagesHasMore: nextLanguages.hasMore,
),
);
if (state.languagesHasMore) {
add(const _FetchNextLanguagePage());
}
} on HttpException catch (e) {
emit(state.copyWith(status: CreateSourceStatus.failure, exception: e));
Expand Down Expand Up @@ -161,6 +150,83 @@ class CreateSourceBloc extends Bloc<CreateSourceEvent, CreateSourceState> {
);
}

// --- 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<CreateSourceState> emit,
) async {
if (!state.countriesHasMore || state.countriesIsLoadingMore) return;

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

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> _onFetchNextLanguagePage(
_FetchNextLanguagePage event,
Emitter<CreateSourceState> emit,
) async {
if (!state.languagesHasMore || state.languagesIsLoadingMore) return;

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

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

final nextLanguages = await _languagesRepository.readAll(
pagination: PaginationOptions(cursor: state.languagesCursor),
sort: [const SortOption('name', SortOrder.asc)],
);

emit(
state.copyWith(
languages: List.of(state.languages)..addAll(nextLanguages.items),
languagesCursor: nextLanguages.cursor,
languagesHasMore: nextLanguages.hasMore,
languagesIsLoadingMore: false,
),
);

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

Future<void> _onSubmitted(
CreateSourceSubmitted event,
Emitter<CreateSourceState> emit,
Expand Down
77 changes: 58 additions & 19 deletions lib/content_management/bloc/edit_headline/edit_headline_bloc.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ import 'package:flutter/foundation.dart';
part 'edit_headline_event.dart';
part 'edit_headline_state.dart';

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

const _searchDebounceDuration = Duration(milliseconds: 300);

/// A BLoC to manage the state of editing a single headline.
Expand All @@ -19,12 +23,12 @@ class EditHeadlineBloc extends Bloc<EditHeadlineEvent, EditHeadlineState> {
required DataRepository<Topic> topicsRepository,
required DataRepository<Country> countriesRepository,
required String headlineId,
}) : _headlinesRepository = headlinesRepository,
_sourcesRepository = sourcesRepository,
_topicsRepository = topicsRepository,
_countriesRepository = countriesRepository,
_headlineId = headlineId,
super(const EditHeadlineState()) {
}) : _headlinesRepository = headlinesRepository,
_sourcesRepository = sourcesRepository,
_topicsRepository = topicsRepository,
_countriesRepository = countriesRepository,
_headlineId = headlineId,
super(const EditHeadlineState()) {
on<EditHeadlineLoaded>(_onLoaded);
on<EditHeadlineTitleChanged>(_onTitleChanged);
on<EditHeadlineExcerptChanged>(_onExcerptChanged);
Expand All @@ -35,6 +39,7 @@ class EditHeadlineBloc extends Bloc<EditHeadlineEvent, EditHeadlineState> {
on<EditHeadlineCountryChanged>(_onCountryChanged);
on<EditHeadlineStatusChanged>(_onStatusChanged);
on<EditHeadlineSubmitted>(_onSubmitted);
on<_FetchNextCountryPage>(_onFetchNextCountryPage);
}

final DataRepository<Headline> _headlinesRepository;
Expand Down Expand Up @@ -87,19 +92,10 @@ class EditHeadlineBloc extends Bloc<EditHeadlineEvent, EditHeadlineState> {
),
);

// Start background fetching for all countries
while (state.countriesHasMore) {
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,
),
);
// 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: EditHeadlineStatus.failure, exception: e));
Expand Down Expand Up @@ -201,6 +197,49 @@ class EditHeadlineBloc extends Bloc<EditHeadlineEvent, EditHeadlineState> {
);
}

// --- 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<EditHeadlineState> emit,
) async {
if (!state.countriesHasMore || state.countriesIsLoadingMore) return;

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

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(
EditHeadlineSubmitted event,
Emitter<EditHeadlineState> emit,
Expand Down
Loading
Loading