From 8a745acee79e5f1ef42f23b4e5fdef3290f161b7 Mon Sep 17 00:00:00 2001 From: fulleni Date: Fri, 1 Aug 2025 09:53:06 +0100 Subject: [PATCH 1/7] refactor(content_management): add comments explaining background fetching process - Add detailed comments in both create_source_bloc.dart and edit_source_bloc.dart - Explain the reason for using background fetching for countries and languages - Highlight UI consistency and technical limitations as motivations - Describe how the UI updates progressively in the background --- .../bloc/create_source/create_source_bloc.dart | 13 +++++++++++-- .../bloc/edit_source/edit_source_bloc.dart | 13 +++++++++++-- 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/lib/content_management/bloc/create_source/create_source_bloc.dart b/lib/content_management/bloc/create_source/create_source_bloc.dart index 3f534a68..fbaf4681 100644 --- a/lib/content_management/bloc/create_source/create_source_bloc.dart +++ b/lib/content_management/bloc/create_source/create_source_bloc.dart @@ -66,7 +66,17 @@ class CreateSourceBloc extends Bloc { ), ); - // Start background fetching for all countries + // After the initial page is loaded, start a background process to + // fetch all remaining pages for countries and languages. + // + // This approach is used for the following reasons: + // 1. UI Consistency: It allows us to use the standard + // `DropdownButtonFormField`, which is used elsewhere in the app. + // 2. Technical Limitation: The standard dropdown does not expose a + // scroll controller, making on-scroll pagination impossible. + // + // The UI will update progressively and silently in the background as + // more data arrives. while (state.countriesHasMore) { final nextCountries = await _countriesRepository.readAll( pagination: PaginationOptions(cursor: state.countriesCursor), @@ -81,7 +91,6 @@ class CreateSourceBloc extends Bloc { ); } - // Start background fetching for all languages while (state.languagesHasMore) { final nextLanguages = await _languagesRepository.readAll( pagination: PaginationOptions(cursor: state.languagesCursor), diff --git a/lib/content_management/bloc/edit_source/edit_source_bloc.dart b/lib/content_management/bloc/edit_source/edit_source_bloc.dart index 1c98adcd..489491d7 100644 --- a/lib/content_management/bloc/edit_source/edit_source_bloc.dart +++ b/lib/content_management/bloc/edit_source/edit_source_bloc.dart @@ -90,7 +90,17 @@ class EditSourceBloc extends Bloc { ), ); - // Start background fetching for all countries + // After the initial page is loaded, start a background process to + // fetch all remaining pages for countries and languages. + // + // This approach is used for the following reasons: + // 1. UI Consistency: It allows us to use the standard + // `DropdownButtonFormField`, which is used elsewhere in the app. + // 2. Technical Limitation: The standard dropdown does not expose a + // scroll controller, making on-scroll pagination impossible. + // + // The UI will update progressively and silently in the background as + // more data arrives. while (state.countriesHasMore) { final nextCountries = await _countriesRepository.readAll( pagination: PaginationOptions(cursor: state.countriesCursor), @@ -105,7 +115,6 @@ class EditSourceBloc extends Bloc { ); } - // Start background fetching for all languages while (state.languagesHasMore) { final nextLanguages = await _languagesRepository.readAll( pagination: PaginationOptions(cursor: state.languagesCursor), From 80a32edff6d8bbfcc9c1b42bec02505de46c8ef5 Mon Sep 17 00:00:00 2001 From: fulleni Date: Fri, 1 Aug 2025 09:54:33 +0100 Subject: [PATCH 2/7] docs(content_management): explain background fetching for countries - Add detailed comments in both create_headline_bloc.dart and edit_headline_bloc.dart - Explain the reason for using background fetching for countries - Highlight UI consistency and technical limitations as motivations - Describe how the UI updates progressively in the background --- .../bloc/create_headline/create_headline_bloc.dart | 11 ++++++++++- .../bloc/edit_headline/edit_headline_bloc.dart | 11 ++++++++++- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/lib/content_management/bloc/create_headline/create_headline_bloc.dart b/lib/content_management/bloc/create_headline/create_headline_bloc.dart index 5b392cf6..5ba54896 100644 --- a/lib/content_management/bloc/create_headline/create_headline_bloc.dart +++ b/lib/content_management/bloc/create_headline/create_headline_bloc.dart @@ -76,7 +76,16 @@ class CreateHeadlineBloc ), ); - // Start background fetching for all countries + // After the initial page of countries is loaded, start a background + // process to fetch all remaining pages. + // + // This approach is used for the following reasons: + // 1. UI Consistency: It allows us to use the standard + // `DropdownButtonFormField`, which is used elsewhere in the app. + // 2. Technical Limitation: The standard dropdown does not expose a + // + // The UI will update progressively and silently in the background as + // more data arrives. while (state.countriesHasMore) { final nextCountries = await _countriesRepository.readAll( pagination: PaginationOptions(cursor: state.countriesCursor), diff --git a/lib/content_management/bloc/edit_headline/edit_headline_bloc.dart b/lib/content_management/bloc/edit_headline/edit_headline_bloc.dart index 8f12e490..d12bb67e 100644 --- a/lib/content_management/bloc/edit_headline/edit_headline_bloc.dart +++ b/lib/content_management/bloc/edit_headline/edit_headline_bloc.dart @@ -87,7 +87,16 @@ class EditHeadlineBloc extends Bloc { ), ); - // Start background fetching for all countries + // After the initial page of countries is loaded, start a background + // process to fetch all remaining pages. + // + // This approach is used for the following reasons: + // 1. UI Consistency: It allows us to use the standard + // `DropdownButtonFormField`, which is used elsewhere in the app. + // 2. Technical Limitation: The standard dropdown does not expose a + // + // The UI will update progressively and silently in the background as + // more data arrives. while (state.countriesHasMore) { final nextCountries = await _countriesRepository.readAll( pagination: PaginationOptions(cursor: state.countriesCursor), From c3fbeb6ff89bcb0dfc76bb595e4409ce438645d7 Mon Sep 17 00:00:00 2001 From: fulleni Date: Fri, 1 Aug 2025 10:15:24 +0100 Subject: [PATCH 3/7] refactor(content_management): implement background pagination for countries dropdown - Add _FetchNextCountryPage event to handle asynchronous data fetching - Modify _onDataLoaded to trigger background fetching - Implement throttling and state management for ongoing requests - Update documentation to explain the new approach --- .../create_headline/create_headline_bloc.dart | 80 +++++++++++++------ 1 file changed, 55 insertions(+), 25 deletions(-) diff --git a/lib/content_management/bloc/create_headline/create_headline_bloc.dart b/lib/content_management/bloc/create_headline/create_headline_bloc.dart index 5ba54896..43118c1a 100644 --- a/lib/content_management/bloc/create_headline/create_headline_bloc.dart +++ b/lib/content_management/bloc/create_headline/create_headline_bloc.dart @@ -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. @@ -20,11 +24,11 @@ class CreateHeadlineBloc required DataRepository sourcesRepository, required DataRepository topicsRepository, required DataRepository countriesRepository, - }) : _headlinesRepository = headlinesRepository, - _sourcesRepository = sourcesRepository, - _topicsRepository = topicsRepository, - _countriesRepository = countriesRepository, - super(const CreateHeadlineState()) { + }) : _headlinesRepository = headlinesRepository, + _sourcesRepository = sourcesRepository, + _topicsRepository = topicsRepository, + _countriesRepository = countriesRepository, + super(const CreateHeadlineState()) { on(_onDataLoaded); on(_onTitleChanged); on(_onExcerptChanged); @@ -35,6 +39,7 @@ class CreateHeadlineBloc on(_onCountryChanged); on(_onStatusChanged); on(_onSubmitted); + on<_FetchNextCountryPage>(_onFetchNextCountryPage); } final DataRepository _headlinesRepository; @@ -78,26 +83,8 @@ class CreateHeadlineBloc // After the initial page of countries is loaded, start a background // process to fetch all remaining pages. - // - // This approach is used for the following reasons: - // 1. UI Consistency: It allows us to use the standard - // `DropdownButtonFormField`, which is used elsewhere in the app. - // 2. Technical Limitation: The standard dropdown does not expose a - // - // The UI will update progressively and silently in the background as - // more data arrives. - 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, - ), - ); + if (state.countriesHasMore) { + add(const _FetchNextCountryPage()); } } on HttpException catch (e) { emit(state.copyWith(status: CreateHeadlineStatus.failure, exception: e)); @@ -172,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 _onFetchNextCountryPage( + _FetchNextCountryPage event, + Emitter 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 _onSubmitted( CreateHeadlineSubmitted event, Emitter emit, From 19e022326d7c08cf0eeb20087052f6221fde15ec Mon Sep 17 00:00:00 2001 From: fulleni Date: Fri, 1 Aug 2025 10:15:41 +0100 Subject: [PATCH 4/7] refactor(content_management): implement efficient background fetching for countries dropdown - Introduce _FetchNextCountryPage event for paginated data fetching - Replace while loop with event-driven approach for better performance - Add loading state for countries to improve user experience - Optimize code structure and formatting for better readability --- .../edit_headline/edit_headline_bloc.dart | 82 +++++++++++++------ 1 file changed, 56 insertions(+), 26 deletions(-) diff --git a/lib/content_management/bloc/edit_headline/edit_headline_bloc.dart b/lib/content_management/bloc/edit_headline/edit_headline_bloc.dart index d12bb67e..da71832a 100644 --- a/lib/content_management/bloc/edit_headline/edit_headline_bloc.dart +++ b/lib/content_management/bloc/edit_headline/edit_headline_bloc.dart @@ -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. @@ -19,12 +23,12 @@ class EditHeadlineBloc extends Bloc { required DataRepository topicsRepository, required DataRepository 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(_onLoaded); on(_onTitleChanged); on(_onExcerptChanged); @@ -35,6 +39,7 @@ class EditHeadlineBloc extends Bloc { on(_onCountryChanged); on(_onStatusChanged); on(_onSubmitted); + on<_FetchNextCountryPage>(_onFetchNextCountryPage); } final DataRepository _headlinesRepository; @@ -89,26 +94,8 @@ class EditHeadlineBloc extends Bloc { // After the initial page of countries is loaded, start a background // process to fetch all remaining pages. - // - // This approach is used for the following reasons: - // 1. UI Consistency: It allows us to use the standard - // `DropdownButtonFormField`, which is used elsewhere in the app. - // 2. Technical Limitation: The standard dropdown does not expose a - // - // The UI will update progressively and silently in the background as - // more data arrives. - 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, - ), - ); + if (state.countriesHasMore) { + add(const _FetchNextCountryPage()); } } on HttpException catch (e) { emit(state.copyWith(status: EditHeadlineStatus.failure, exception: e)); @@ -210,6 +197,49 @@ class EditHeadlineBloc extends Bloc { ); } + // --- 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 _onFetchNextCountryPage( + _FetchNextCountryPage event, + Emitter 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 _onSubmitted( EditHeadlineSubmitted event, Emitter emit, From 8163f86f479284e73077ac2fe4fe7262494b8a7d Mon Sep 17 00:00:00 2001 From: fulleni Date: Fri, 1 Aug 2025 10:16:27 +0100 Subject: [PATCH 5/7] refactor(content_management): implement background pagination for dropdowns - Add _FetchNextCountryPage and _FetchNextLanguagePage events - Implement background fetching for countries and languages - Update UI to reflect loading state for dropdown pagination - Remove inline pagination logic from CreateSourceDataLoaded event --- .../create_source/create_source_bloc.dart | 135 +++++++++++++----- 1 file changed, 96 insertions(+), 39 deletions(-) diff --git a/lib/content_management/bloc/create_source/create_source_bloc.dart b/lib/content_management/bloc/create_source/create_source_bloc.dart index fbaf4681..62a1150d 100644 --- a/lib/content_management/bloc/create_source/create_source_bloc.dart +++ b/lib/content_management/bloc/create_source/create_source_bloc.dart @@ -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. @@ -18,10 +26,10 @@ class CreateSourceBloc extends Bloc { required DataRepository sourcesRepository, required DataRepository countriesRepository, required DataRepository languagesRepository, - }) : _sourcesRepository = sourcesRepository, - _countriesRepository = countriesRepository, - _languagesRepository = languagesRepository, - super(const CreateSourceState()) { + }) : _sourcesRepository = sourcesRepository, + _countriesRepository = countriesRepository, + _languagesRepository = languagesRepository, + super(const CreateSourceState()) { on(_onDataLoaded); on(_onNameChanged); on(_onDescriptionChanged); @@ -31,6 +39,8 @@ class CreateSourceBloc extends Bloc { on(_onHeadquartersChanged); on(_onStatusChanged); on(_onSubmitted); + on<_FetchNextCountryPage>(_onFetchNextCountryPage); + on<_FetchNextLanguagePage>(_onFetchNextLanguagePage); } final DataRepository _sourcesRepository; @@ -66,43 +76,13 @@ class CreateSourceBloc extends Bloc { ), ); - // After the initial page is loaded, start a background process to + // After the initial page is loaded, start background processes to // fetch all remaining pages for countries and languages. - // - // This approach is used for the following reasons: - // 1. UI Consistency: It allows us to use the standard - // `DropdownButtonFormField`, which is used elsewhere in the app. - // 2. Technical Limitation: The standard dropdown does not expose a - // scroll controller, making on-scroll pagination impossible. - // - // The UI will update progressively and silently in the background as - // more data arrives. - 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, - ), - ); + if (state.countriesHasMore) { + add(const _FetchNextCountryPage()); } - - 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)); @@ -170,6 +150,83 @@ class CreateSourceBloc extends Bloc { ); } + // --- 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 _onFetchNextCountryPage( + _FetchNextCountryPage event, + Emitter 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 _onFetchNextLanguagePage( + _FetchNextLanguagePage event, + Emitter 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 _onSubmitted( CreateSourceSubmitted event, Emitter emit, From fd7f540c93434c1d7ca598bca888578ba2502fda Mon Sep 17 00:00:00 2001 From: fulleni Date: Fri, 1 Aug 2025 10:17:12 +0100 Subject: [PATCH 6/7] refactor(content_management): implement background pagination for dropdowns - Add _FetchNextCountryPage and _FetchNextLanguagePage events - Implement background fetching for countries and languages - Update UI to reflect loading state for dropdown pagination - Remove inline pagination logic from EditSourceLoaded event handler --- .../bloc/edit_source/edit_source_bloc.dart | 137 +++++++++++++----- 1 file changed, 97 insertions(+), 40 deletions(-) diff --git a/lib/content_management/bloc/edit_source/edit_source_bloc.dart b/lib/content_management/bloc/edit_source/edit_source_bloc.dart index 489491d7..faeabe87 100644 --- a/lib/content_management/bloc/edit_source/edit_source_bloc.dart +++ b/lib/content_management/bloc/edit_source/edit_source_bloc.dart @@ -8,6 +8,14 @@ import 'package:flutter/foundation.dart'; part 'edit_source_event.dart'; part 'edit_source_state.dart'; +final class _FetchNextCountryPage extends EditSourceEvent { + const _FetchNextCountryPage(); +} + +final class _FetchNextLanguagePage extends EditSourceEvent { + const _FetchNextLanguagePage(); +} + const _searchDebounceDuration = Duration(milliseconds: 300); /// A BLoC to manage the state of editing a single source. @@ -18,11 +26,11 @@ class EditSourceBloc extends Bloc { required DataRepository countriesRepository, required DataRepository languagesRepository, required String sourceId, - }) : _sourcesRepository = sourcesRepository, - _countriesRepository = countriesRepository, - _languagesRepository = languagesRepository, - _sourceId = sourceId, - super(const EditSourceState()) { + }) : _sourcesRepository = sourcesRepository, + _countriesRepository = countriesRepository, + _languagesRepository = languagesRepository, + _sourceId = sourceId, + super(const EditSourceState()) { on(_onLoaded); on(_onNameChanged); on(_onDescriptionChanged); @@ -32,6 +40,8 @@ class EditSourceBloc extends Bloc { on(_onHeadquartersChanged); on(_onStatusChanged); on(_onSubmitted); + on<_FetchNextCountryPage>(_onFetchNextCountryPage); + on<_FetchNextLanguagePage>(_onFetchNextLanguagePage); } final DataRepository _sourcesRepository; @@ -90,43 +100,13 @@ class EditSourceBloc extends Bloc { ), ); - // After the initial page is loaded, start a background process to + // After the initial page is loaded, start background processes to // fetch all remaining pages for countries and languages. - // - // This approach is used for the following reasons: - // 1. UI Consistency: It allows us to use the standard - // `DropdownButtonFormField`, which is used elsewhere in the app. - // 2. Technical Limitation: The standard dropdown does not expose a - // scroll controller, making on-scroll pagination impossible. - // - // The UI will update progressively and silently in the background as - // more data arrives. - 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, - ), - ); + if (state.countriesHasMore) { + add(const _FetchNextCountryPage()); } - - 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: EditSourceStatus.failure, exception: e)); @@ -214,6 +194,83 @@ class EditSourceBloc extends Bloc { ); } + // --- 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 _onFetchNextCountryPage( + _FetchNextCountryPage event, + Emitter 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 _onFetchNextLanguagePage( + _FetchNextLanguagePage event, + Emitter 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 _onSubmitted( EditSourceSubmitted event, Emitter emit, From 1cfc3e20f8978fd5748c0999d225e080c5a0bf82 Mon Sep 17 00:00:00 2001 From: fulleni Date: Fri, 1 Aug 2025 10:17:57 +0100 Subject: [PATCH 7/7] feat(content_management): disable country selection when loading more - Add helper text to indicate loading state in country dropdown - Disable country selection when more countries are being loaded --- lib/content_management/view/create_headline_page.dart | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/lib/content_management/view/create_headline_page.dart b/lib/content_management/view/create_headline_page.dart index 57a50874..d6408e11 100644 --- a/lib/content_management/view/create_headline_page.dart +++ b/lib/content_management/view/create_headline_page.dart @@ -219,6 +219,9 @@ class _CreateHeadlineViewState extends State<_CreateHeadlineView> { decoration: InputDecoration( labelText: l10n.countryName, border: const OutlineInputBorder(), + helperText: state.countriesIsLoadingMore + ? l10n.loadingFullList + : null, ), items: [ DropdownMenuItem(value: null, child: Text(l10n.none)), @@ -245,9 +248,11 @@ class _CreateHeadlineViewState extends State<_CreateHeadlineView> { ), ), ], - onChanged: (value) => context - .read() - .add(CreateHeadlineCountryChanged(value)), + onChanged: state.countriesIsLoadingMore + ? null + : (value) => context + .read() + .add(CreateHeadlineCountryChanged(value)), ), const SizedBox(height: AppSpacing.lg), DropdownButtonFormField(