Skip to content

Commit 7130cc7

Browse files
committed
refactor: Use bloc for source filtering
- Implemented SourcesFilterBloc - Handles loading, error and empty states - Implements pagination
1 parent eb17e88 commit 7130cc7

File tree

1 file changed

+72
-98
lines changed

1 file changed

+72
-98
lines changed

lib/headlines-feed/view/source_filter_page.dart

Lines changed: 72 additions & 98 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,21 @@
1-
//
21
// ignore_for_file: lines_longer_than_80_chars
32

43
import 'package:flutter/material.dart';
54
import 'package:flutter_bloc/flutter_bloc.dart';
65
import 'package:go_router/go_router.dart';
6+
import 'package:ht_main/headlines-feed/bloc/sources_filter_bloc.dart'; // Import the BLoC
77
import 'package:ht_main/l10n/l10n.dart';
88
import 'package:ht_main/shared/constants/constants.dart';
99
import 'package:ht_main/shared/widgets/widgets.dart'; // For loading/error widgets
1010
import 'package:ht_sources_client/ht_sources_client.dart';
11-
import 'package:ht_sources_repository/ht_sources_repository.dart';
11+
// Removed repository import: import 'package:ht_sources_repository/ht_sources_repository.dart';
1212

1313
/// {@template source_filter_page}
1414
/// A page dedicated to selecting news sources for filtering headlines.
1515
///
16-
/// Fetches sources paginatively, allows multiple selections, and returns
17-
/// the selected list via `context.pop` when the user applies the changes.
16+
/// Uses [SourcesFilterBloc] to fetch sources paginatively, allows multiple
17+
/// selections, and returns the selected list via `context.pop` when the user
18+
/// applies the changes.
1819
/// {@endtemplate}
1920
class SourceFilterPage extends StatefulWidget {
2021
/// {@macro source_filter_page}
@@ -26,32 +27,13 @@ class SourceFilterPage extends StatefulWidget {
2627

2728
/// State for the [SourceFilterPage].
2829
///
29-
/// Manages the local selection state ([_pageSelectedSources]), fetches
30-
/// sources from the [HtSourcesRepository], handles pagination using a
31-
/// [ScrollController], and displays loading/error/empty/loaded states.
30+
/// Manages the local selection state ([_pageSelectedSources]) and interacts
31+
/// with [SourcesFilterBloc] for data fetching and pagination.
3232
class _SourceFilterPageState extends State<SourceFilterPage> {
3333
/// Stores the sources selected by the user on this page.
3434
/// Initialized from the `extra` parameter passed during navigation.
3535
late Set<Source> _pageSelectedSources;
3636

37-
/// List of all sources fetched from the repository.
38-
List<Source> _allSources = [];
39-
40-
/// Flag indicating if the initial source list is being loaded.
41-
bool _isLoading = true;
42-
43-
/// Flag indicating if more sources are being loaded for pagination.
44-
bool _isLoadingMore = false;
45-
46-
/// Flag indicating if more sources are available to fetch.
47-
bool _hasMore = true;
48-
49-
/// Cursor for fetching the next page of sources.
50-
String? _cursor;
51-
52-
/// Stores any error message that occurred during fetching.
53-
String? _error;
54-
5537
/// Scroll controller to detect when the user reaches the end of the list
5638
/// for pagination.
5739
final _scrollController = ScrollController();
@@ -63,7 +45,8 @@ class _SourceFilterPageState extends State<SourceFilterPage> {
6345
WidgetsBinding.instance.addPostFrameCallback((_) {
6446
final initialSelection = GoRouterState.of(context).extra as List<Source>?;
6547
_pageSelectedSources = Set.from(initialSelection ?? []);
66-
_fetchSources(); // Initial fetch
48+
// Request initial sources from the BLoC
49+
context.read<SourcesFilterBloc>().add(SourcesFilterRequested());
6750
});
6851
_scrollController.addListener(_onScroll);
6952
}
@@ -76,63 +59,20 @@ class _SourceFilterPageState extends State<SourceFilterPage> {
7659
super.dispose();
7760
}
7861

79-
/// Fetches sources from the [HtSourcesRepository].
80-
///
81-
/// Handles both initial fetch and pagination (`loadMore = true`). Updates
82-
/// loading states, fetched data ([_allSources]), pagination info
83-
/// ([_cursor], [_hasMore]), and error state ([_error]).
84-
Future<void> _fetchSources({bool loadMore = false}) async {
85-
// Prevent unnecessary fetches
86-
if (!loadMore && _isLoading) return;
87-
if (loadMore && (_isLoadingMore || !_hasMore)) return;
88-
89-
setState(() {
90-
if (loadMore) {
91-
_isLoadingMore = true;
92-
} else {
93-
_isLoading = true;
94-
_error = null;
95-
}
96-
});
97-
98-
try {
99-
// Ensure HtSourcesRepository is provided higher up in the widget tree
100-
final repo = context.read<HtSourcesRepository>();
101-
final response = await repo.getSources(
102-
limit: 20, // Adjust limit as needed
103-
startAfterId: loadMore ? _cursor : null,
104-
);
105-
106-
setState(() {
107-
if (loadMore) {
108-
_allSources.addAll(response.items);
109-
} else {
110-
_allSources = response.items;
111-
}
112-
_cursor = response.cursor;
113-
_hasMore = response.hasMore;
114-
_isLoading = false;
115-
_isLoadingMore = false;
116-
});
117-
} catch (e) {
118-
setState(() {
119-
_isLoading = false;
120-
_isLoadingMore = false;
121-
_error = e.toString();
122-
});
123-
}
124-
}
125-
12662
/// Callback function for scroll events.
12763
///
12864
/// Checks if the user has scrolled near the bottom of the list and triggers
129-
/// fetching more sources if available.
65+
/// fetching more sources via the BLoC if available.
13066
void _onScroll() {
13167
if (!_scrollController.hasClients) return;
13268
final maxScroll = _scrollController.position.maxScrollExtent;
13369
final currentScroll = _scrollController.offset;
134-
if (currentScroll >= (maxScroll * 0.9) && _hasMore && !_isLoadingMore) {
135-
_fetchSources(loadMore: true);
70+
final bloc = context.read<SourcesFilterBloc>();
71+
// Fetch more when nearing the bottom, if BLoC has more and isn't already loading more
72+
if (currentScroll >= (maxScroll * 0.9) &&
73+
bloc.state.hasMore &&
74+
bloc.state.status != SourcesFilterStatus.loadingMore) {
75+
bloc.add(SourcesFilterLoadMoreRequested());
13676
}
13777
}
13878

@@ -153,61 +93,95 @@ class _SourceFilterPageState extends State<SourceFilterPage> {
15393
),
15494
],
15595
),
156-
body: _buildBody(context),
96+
// Use BlocBuilder to react to state changes from SourcesFilterBloc
97+
body: BlocBuilder<SourcesFilterBloc, SourcesFilterState>(
98+
builder: (context, state) {
99+
return _buildBody(context, state);
100+
},
101+
),
157102
);
158103
}
159104

160-
/// Builds the main content body based on the current loading/error/data state.
161-
Widget _buildBody(BuildContext context) {
105+
/// Builds the main content body based on the current [SourcesFilterState].
106+
Widget _buildBody(BuildContext context, SourcesFilterState state) {
162107
final l10n = context.l10n;
163-
if (_isLoading) {
164-
// Show initial loading indicator
108+
109+
// Handle initial loading state
110+
if (state.status == SourcesFilterStatus.loading) {
165111
return LoadingStateWidget(
166112
icon: Icons.source_outlined,
167113
headline: l10n.sourceFilterLoadingHeadline,
168114
subheadline: l10n.sourceFilterLoadingSubheadline,
169115
);
170116
}
171117

172-
if (_error != null) {
173-
return FailureStateWidget(message: _error!, onRetry: _fetchSources);
118+
// Handle failure state (show error and retry button)
119+
if (state.status == SourcesFilterStatus.failure && state.sources.isEmpty) {
120+
return FailureStateWidget(
121+
message: state.error?.toString() ?? l10n.unknownError, // Assumes unknownError exists
122+
onRetry: () =>
123+
context.read<SourcesFilterBloc>().add(SourcesFilterRequested()),
124+
);
174125
}
175126

176-
if (_allSources.isEmpty) {
127+
// Handle empty state (after successful load but no sources found)
128+
if (state.status == SourcesFilterStatus.success && state.sources.isEmpty) {
177129
return InitialStateWidget(
178130
icon: Icons.search_off,
179131
headline: l10n.sourceFilterEmptyHeadline,
180132
subheadline: l10n.sourceFilterEmptySubheadline,
181133
);
182134
}
183135

136+
// Handle loaded state (success or loading more)
184137
return ListView.builder(
185138
controller: _scrollController,
186139
padding: const EdgeInsets.only(bottom: AppSpacing.xxl),
187-
itemCount: _allSources.length + (_hasMore ? 1 : 0),
140+
itemCount: state.sources.length +
141+
((state.status == SourcesFilterStatus.loadingMore ||
142+
(state.status == SourcesFilterStatus.failure &&
143+
state.sources.isNotEmpty))
144+
? 1
145+
: 0),
188146
itemBuilder: (context, index) {
189-
if (index >= _allSources.length) {
190-
return _isLoadingMore
191-
? const Padding(
192-
padding: EdgeInsets.symmetric(vertical: AppSpacing.lg),
193-
child: Center(child: CircularProgressIndicator()),
194-
)
195-
: const SizedBox.shrink();
147+
if (index >= state.sources.length) {
148+
if (state.status == SourcesFilterStatus.loadingMore) {
149+
return const Padding(
150+
padding: EdgeInsets.symmetric(vertical: AppSpacing.lg),
151+
child: Center(child: CircularProgressIndicator()),
152+
);
153+
} else if (state.status == SourcesFilterStatus.failure) {
154+
return Padding(
155+
padding: const EdgeInsets.symmetric(
156+
vertical: AppSpacing.md,
157+
horizontal: AppSpacing.lg,
158+
),
159+
child: Center(
160+
child: Text(
161+
l10n.loadMoreError, // Assumes loadMoreError exists
162+
style: Theme.of(context).textTheme.bodySmall?.copyWith(
163+
color: Theme.of(context).colorScheme.error,
164+
),
165+
),
166+
),
167+
);
168+
} else {
169+
return const SizedBox.shrink();
170+
}
196171
}
197172

198-
final source = _allSources[index];
173+
final source = state.sources[index];
199174
final isSelected = _pageSelectedSources.contains(source);
200175

201-
// Sources don't have icons in the model, so just use text
202176
return CheckboxListTile(
203177
title: Text(source.name),
204178
subtitle:
205179
source.description != null && source.description!.isNotEmpty
206180
? Text(
207-
source.description!,
208-
maxLines: 1,
209-
overflow: TextOverflow.ellipsis,
210-
)
181+
source.description!,
182+
maxLines: 1,
183+
overflow: TextOverflow.ellipsis,
184+
)
211185
: null,
212186
value: isSelected,
213187
onChanged: (bool? value) {

0 commit comments

Comments
 (0)