Skip to content

Commit e87db78

Browse files
authored
Merge pull request #187 from flutter-news-app-full-source-code/feat/dicover
Feat/dicover
2 parents b876d6c + 8d6b826 commit e87db78

28 files changed

+2267
-18
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
## Upcoming Release
44

5+
- **feat**: implement discover feature for browsing news sources by category with filtering and follow/unfollow functionality.
56
- **refactor!**: Overhauled the application startup and authentication lifecycle to be robust and free of race conditions. This was a major architectural change that introduced a new `AppInitializationPage` and `AppInitializationBloc` to act as a "gatekeeper," ensuring all critical data is fetched *before* the main UI is built. This fixes a class of bugs related to indefinite loading screens, data migration on account linking, and inconsistent state during startup.
67

78
## 1.4.0 - 2025-10-17

README.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ Click on any category to explore.
3838
- **Advanced Filter Creation:** A dedicated, full-screen interface allows users to build complex filters by combining multiple `Topics`, `Sources`, and `Countries`.
3939
- **Saved Filters:** Users can name and save their custom filter combinations. These filters appear in the quick-access bar and can be reordered for a fully customized experience.
4040
- **Integrated Headline Search:** A sleek search bar in the main feed's scrolling app bar provides a focused, full-screen search experience for headlines. A user avatar within the search bar offers instant modal access to account settings.
41+
- **Intuitive Source Discovery:** A dedicated "Discover" tab provides a rich, multi-layered exploration experience. Users can browse sources by category in horizontally scrolling carousels, dive deeper into full lists with infinite scrolling and country-based filtering, and find specific sources using a familiar, contextual search bar.
4142
> **🎯 Your Advantage:** Give your users powerful content discovery tools that keep them engaged and coming back for more.
4243
4344
</details>
@@ -122,8 +123,9 @@ Gain complete command over your application's operational state and user experie
122123
---
123124

124125
### 🛠️ Flexible Environment Configuration
125-
- Easily switch between development (in-memory data or local API) and production environments with a simple code change. This empowers rapid prototyping, robust testing, and seamless deployment.
126-
> **🚀 Your Advantage:** A flexible setup that speeds up your development cycle and makes deployment simple.
126+
- The app utilizes compile-time variables (`--dart-define`) to seamlessly switch between `production`, `development`, and `demo` environments. This production-ready approach ensures that environment-specific configurations, like API endpoints, are set at build time, eliminating the risk of shipping a production app with development settings.
127+
- Includes pre-configured VS Code `launch.json` profiles for all environments, enabling one-click running and debugging for any target configuration.
128+
> **🚀 Your Advantage:** A robust, professional environment setup that streamlines the development-to-production workflow and prevents common configuration errors.
127129
128130
---
129131

lib/account/view/saved_filters_page.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ class SavedFiltersPage extends StatelessWidget {
7070
onSelected: (value) async {
7171
switch (value) {
7272
case 'rename':
73-
showDialog<void>(
73+
await showDialog<void>(
7474
context: context,
7575
builder: (_) => SaveFilterDialog(
7676
initialValue: filter.name,
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import 'package:bloc/bloc.dart';
2+
import 'package:collection/collection.dart';
3+
import 'package:core/core.dart';
4+
import 'package:data_repository/data_repository.dart';
5+
import 'package:equatable/equatable.dart';
6+
import 'package:logging/logging.dart';
7+
8+
part 'discover_event.dart';
9+
part 'discover_state.dart';
10+
11+
/// {@template discover_bloc}
12+
/// A BLoC that manages the state of the discover feature.
13+
///
14+
/// This BLoC is responsible for fetching all available news sources and
15+
/// grouping them by their respective [SourceType] for display on the
16+
/// discover page.
17+
/// {@endtemplate}
18+
class DiscoverBloc extends Bloc<DiscoverEvent, DiscoverState> {
19+
/// {@macro discover_bloc}
20+
DiscoverBloc({
21+
required DataRepository<Source> sourcesRepository,
22+
required Logger logger,
23+
}) : _sourcesRepository = sourcesRepository,
24+
_logger = logger,
25+
super(const DiscoverState()) {
26+
on<DiscoverStarted>(_onDiscoverStarted);
27+
}
28+
29+
final DataRepository<Source> _sourcesRepository;
30+
final Logger _logger;
31+
32+
/// Handles the initial fetching and grouping of all sources.
33+
///
34+
/// When [DiscoverStarted] is added, this method fetches all sources from
35+
/// the repository, groups them into a map by [SourceType], and emits
36+
/// a success or failure state.
37+
Future<void> _onDiscoverStarted(
38+
DiscoverStarted event,
39+
Emitter<DiscoverState> emit,
40+
) async {
41+
_logger.fine('[DiscoverBloc] DiscoverStarted event received.');
42+
emit(state.copyWith(status: DiscoverStatus.loading));
43+
44+
try {
45+
// Fetch all available sources from the repository.
46+
final sourcesResponse = await _sourcesRepository.readAll();
47+
_logger.info(
48+
'[DiscoverBloc] Successfully fetched ${sourcesResponse.items.length} sources.',
49+
);
50+
51+
// Group the fetched sources by their sourceType.
52+
final groupedSources = groupBy<Source, SourceType>(
53+
sourcesResponse.items,
54+
(source) => source.sourceType,
55+
);
56+
57+
emit(
58+
state.copyWith(
59+
status: DiscoverStatus.success,
60+
groupedSources: groupedSources,
61+
),
62+
);
63+
} on HttpException catch (e, s) {
64+
_logger.severe('[DiscoverBloc] Failed to fetch sources.', e, s);
65+
emit(state.copyWith(status: DiscoverStatus.failure, error: e));
66+
} catch (e, s) {
67+
_logger.severe('[DiscoverBloc] Failed to fetch sources.', e, s);
68+
emit(
69+
state.copyWith(
70+
status: DiscoverStatus.failure,
71+
error: UnknownException('An unexpected error occurred: $e'),
72+
),
73+
);
74+
}
75+
}
76+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
part of 'discover_bloc.dart';
2+
3+
/// Base class for all events related to the [DiscoverBloc].
4+
sealed class DiscoverEvent extends Equatable {
5+
/// {@macro discover_event}
6+
const DiscoverEvent();
7+
8+
@override
9+
List<Object> get props => [];
10+
}
11+
12+
/// {@template discover_started}
13+
/// Event added when the discover feature is first started.
14+
/// This triggers the initial fetch of all available sources.
15+
/// {@endtemplate}
16+
final class DiscoverStarted extends DiscoverEvent {}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
part of 'discover_bloc.dart';
2+
3+
/// The status of the [DiscoverBloc].
4+
enum DiscoverStatus {
5+
/// The initial state.
6+
initial,
7+
8+
/// The state when loading data.
9+
loading,
10+
11+
/// The state when data has been successfully loaded.
12+
success,
13+
14+
/// The state when an error has occurred.
15+
failure,
16+
}
17+
18+
/// {@template discover_state}
19+
/// The state of the discover feature, which holds the grouped sources.
20+
/// {@endtemplate}
21+
final class DiscoverState extends Equatable {
22+
/// {@macro discover_state}
23+
const DiscoverState({
24+
this.status = DiscoverStatus.initial,
25+
this.groupedSources = const {},
26+
this.error,
27+
});
28+
29+
/// The current status of the discover feature.
30+
final DiscoverStatus status;
31+
32+
/// A map of sources grouped by their [SourceType].
33+
final Map<SourceType, List<Source>> groupedSources;
34+
35+
/// The error that occurred, if any.
36+
final Exception? error;
37+
38+
/// Creates a copy of the current [DiscoverState] with the given fields
39+
/// replaced with the new values.
40+
DiscoverState copyWith({
41+
DiscoverStatus? status,
42+
Map<SourceType, List<Source>>? groupedSources,
43+
Exception? error,
44+
bool clearError = false,
45+
}) {
46+
return DiscoverState(
47+
status: status ?? this.status,
48+
groupedSources: groupedSources ?? this.groupedSources,
49+
error: clearError ? null : error ?? this.error,
50+
);
51+
}
52+
53+
@override
54+
List<Object?> get props => [status, groupedSources, error];
55+
}

0 commit comments

Comments
 (0)