Skip to content

Enhance contenet management data perfetching performance #55

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
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