Skip to content

Enhance content managment datat fetching mechanism #48

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Aug 1, 2025
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