Skip to content

Commit 8c44508

Browse files
authored
Merge pull request #164 from flutter-news-app-full-source-code/fix/saved-filters-demo-intial-data-not-applied
Fix/saved filters demo intial data not applied
2 parents d385834 + a19d810 commit 8c44508

File tree

6 files changed

+88
-45
lines changed

6 files changed

+88
-45
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
## 1.4.0 - 2025-10-14
44

5+
- **feat(demo)**: pre-populate saved filters and settings for new anonymous users
6+
- **fix**: ensure saved filters are immediately visible on app start
57
- **feat**: implement saved feed filters with create, rename, and delete
68
- **feat**: add horizontal filter bar to the headlines feed for quick selection
79
- **refactor**: replace monolithic "apply followed items" with granular controls

lib/app/bloc/app_bloc.dart

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -350,6 +350,11 @@ class AppBloc extends Bloc<AppEvent, AppState> {
350350
'[AppBloc] Demo mode: User-specific data initialized for '
351351
'user: ${newUser.id}.',
352352
);
353+
// After successful initialization, immediately re-fetch the user data.
354+
// This ensures that the newly created fixture data (settings and preferences)
355+
// is loaded into the AppState and propagated to the UI, making features
356+
// like the saved filters bar immediately visible.
357+
await _fetchAndSetUserData(newUser, emit);
353358
} catch (e, s) {
354359
_logger.severe(
355360
'[AppBloc] ERROR: Failed to initialize demo user data.',
@@ -408,9 +413,12 @@ class AppBloc extends Bloc<AppEvent, AppState> {
408413
}
409414
}
410415

411-
// After potential initialization and migration,
412-
// ensure user-specific data (settings and preferences) are loaded.
413-
await _fetchAndSetUserData(newUser, emit);
416+
// If not in demo mode, or if the demo initializer is not used,
417+
// perform the standard data fetch. In demo mode, this call is now
418+
// redundant as it's handled immediately after initialization.
419+
if (_environment != local_config.AppEnvironment.demo) {
420+
await _fetchAndSetUserData(newUser, emit);
421+
}
414422
} else {
415423
// If user logs out, clear user-specific data from state.
416424
emit(state.copyWith(settings: null, userContentPreferences: null));

lib/app/services/demo_data_initializer_service.dart

Lines changed: 44 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,13 @@ import 'package:logging/logging.dart';
44

55
/// {@template demo_data_initializer_service}
66
/// A service responsible for ensuring that essential user-specific data
7-
/// (like [UserAppSettings] and [UserContentPreferences]) exists in the
8-
/// data in-memory clients when a user is first encountered in the demo environment.
7+
/// (like [UserAppSettings] and [UserContentPreferences]) exists for a new user
8+
/// in the demo environment.
9+
///
10+
/// Instead of creating default empty objects, this service now acts as a
11+
/// "fixture injector". It clones rich, pre-defined data from fixture lists,
12+
/// providing new anonymous users with a full-featured initial experience,
13+
/// including pre-populated saved filters.
914
///
1015
/// This service is specifically designed for the in-memory data clients
1116
/// used in the demo environment. In production/development environments,
@@ -17,6 +22,8 @@ class DemoDataInitializerService {
1722
required DataRepository<UserAppSettings> userAppSettingsRepository,
1823
required DataRepository<UserContentPreferences>
1924
userContentPreferencesRepository,
25+
required this.userAppSettingsFixturesData,
26+
required this.userContentPreferencesFixturesData,
2027
}) : _userAppSettingsRepository = userAppSettingsRepository,
2128
_userContentPreferencesRepository = userContentPreferencesRepository,
2229
_logger = Logger('DemoDataInitializerService');
@@ -26,6 +33,16 @@ class DemoDataInitializerService {
2633
_userContentPreferencesRepository;
2734
final Logger _logger;
2835

36+
/// A list of [UserAppSettings] fixture data to be used as a template.
37+
///
38+
/// The first item in this list will be cloned for new users.
39+
final List<UserAppSettings> userAppSettingsFixturesData;
40+
41+
/// A list of [UserContentPreferences] fixture data to be used as a template.
42+
///
43+
/// The first item in this list will be cloned for new users.
44+
final List<UserContentPreferences> userContentPreferencesFixturesData;
45+
2946
/// Initializes essential user-specific data in the in-memory clients
3047
/// for the given [user].
3148
///
@@ -58,36 +75,25 @@ class DemoDataInitializerService {
5875
} on NotFoundException {
5976
_logger.info(
6077
'UserAppSettings not found for user ID: '
61-
'$userId. Creating default settings.',
78+
'$userId. Creating settings from fixture.',
6279
);
63-
final defaultSettings = UserAppSettings(
80+
// Clone the first item from the fixture data, assigning the new user's ID.
81+
// This ensures every new demo user gets a rich, pre-populated set of settings.
82+
if (userAppSettingsFixturesData.isEmpty) {
83+
throw StateError(
84+
'Cannot create settings from fixture: userAppSettingsFixturesData is empty.',
85+
);
86+
}
87+
final fixtureSettings = userAppSettingsFixturesData.first.copyWith(
6488
id: userId,
65-
displaySettings: const DisplaySettings(
66-
baseTheme: AppBaseTheme.system,
67-
accentTheme: AppAccentTheme.defaultBlue,
68-
fontFamily: 'SystemDefault',
69-
textScaleFactor: AppTextScaleFactor.medium,
70-
fontWeight: AppFontWeight.regular,
71-
),
72-
language: languagesFixturesData.firstWhere(
73-
(l) => l.code == 'en',
74-
orElse: () => throw StateError(
75-
'Default language "en" not found in language fixtures.',
76-
),
77-
),
78-
feedPreferences: const FeedDisplayPreferences(
79-
headlineDensity: HeadlineDensity.standard,
80-
headlineImageStyle: HeadlineImageStyle.smallThumbnail,
81-
showSourceInHeadlineFeed: true,
82-
showPublishDateInHeadlineFeed: true,
83-
),
8489
);
90+
8591
await _userAppSettingsRepository.create(
86-
item: defaultSettings,
92+
item: fixtureSettings,
8793
userId: userId,
8894
);
8995
_logger.info(
90-
'Default UserAppSettings created for '
96+
'UserAppSettings from fixture created for '
9197
'user ID: $userId.',
9298
);
9399
} catch (e, s) {
@@ -110,22 +116,24 @@ class DemoDataInitializerService {
110116
} on NotFoundException {
111117
_logger.info(
112118
'UserContentPreferences not found for '
113-
'user ID: $userId. Creating default preferences.',
114-
);
115-
final defaultPreferences = UserContentPreferences(
116-
id: userId,
117-
followedCountries: const [],
118-
followedSources: const [],
119-
followedTopics: const [],
120-
savedHeadlines: const [],
121-
savedFilters: const [],
119+
'user ID: $userId. Creating preferences from fixture.',
122120
);
121+
// Clone the first item from the fixture data, assigning the new user's ID.
122+
// This provides new demo users with pre-populated saved filters and other preferences.
123+
if (userContentPreferencesFixturesData.isEmpty) {
124+
throw StateError(
125+
'Cannot create preferences from fixture: userContentPreferencesFixturesData is empty.',
126+
);
127+
}
128+
final fixturePreferences = userContentPreferencesFixturesData.first
129+
.copyWith(id: userId);
130+
123131
await _userContentPreferencesRepository.create(
124-
item: defaultPreferences,
132+
item: fixturePreferences,
125133
userId: userId,
126134
);
127135
_logger.info(
128-
'Default UserContentPreferences created '
136+
'UserContentPreferences from fixture created '
129137
'for user ID: $userId.',
130138
);
131139
} catch (e, s) {

lib/bootstrap.dart

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -469,15 +469,18 @@ Future<Widget> bootstrap(
469469
);
470470

471471
// Conditionally instantiate DemoDataInitializerService
472-
// This service is responsible for ensuring that essential user-specific data
473-
// (like UserAppSettings, UserContentPreferences)
474-
// exists in the data in-memory clients when a user is first encountered
475-
// in the demo environment.
472+
// In the demo environment, this service acts as a "fixture injector".
473+
// When a new user is encountered, it clones pre-defined fixture data
474+
// (settings and preferences, including saved filters) for that user,
475+
// ensuring a rich initial experience.
476476
final demoDataInitializerService =
477477
appConfig.environment == app_config.AppEnvironment.demo
478478
? DemoDataInitializerService(
479479
userAppSettingsRepository: userAppSettingsRepository,
480480
userContentPreferencesRepository: userContentPreferencesRepository,
481+
userAppSettingsFixturesData: userAppSettingsFixturesData,
482+
userContentPreferencesFixturesData:
483+
userContentPreferencesFixturesData,
481484
)
482485
: null;
483486
logger

lib/headlines-feed/bloc/headlines_feed_bloc.dart

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,12 +30,25 @@ class HeadlinesFeedBloc extends Bloc<HeadlinesFeedEvent, HeadlinesFeedState> {
3030
required FeedDecoratorService feedDecoratorService,
3131
required AppBloc appBloc,
3232
required InlineAdCacheService inlineAdCacheService,
33+
UserContentPreferences? initialUserContentPreferences,
3334
}) : _headlinesRepository = headlinesRepository,
3435
_feedDecoratorService = feedDecoratorService,
3536
_appBloc = appBloc,
3637
_inlineAdCacheService = inlineAdCacheService,
37-
super(const HeadlinesFeedState()) {
38+
super(
39+
HeadlinesFeedState(
40+
// Initialize the state with saved filters from the AppBloc's
41+
// current state. This prevents a race condition where the
42+
// feed bloc is created after the AppBloc has already loaded
43+
// the user's preferences, ensuring the UI has the data
44+
// from the very beginning.
45+
savedFilters:
46+
initialUserContentPreferences?.savedFilters ?? const [],
47+
),
48+
) {
3849
// Subscribe to AppBloc to receive updates on user preferences.
50+
// This handles subsequent changes to preferences (e.g., adding/deleting
51+
// a filter) while the feed page is active.
3952
_appBlocSubscription = _appBloc.stream.listen((appState) {
4053
// Check if userContentPreferences has changed and is not null.
4154
if (appState.userContentPreferences != null &&

lib/router/router.dart

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -346,12 +346,21 @@ GoRouter createRouter({
346346
providers: [
347347
BlocProvider(
348348
create: (context) {
349+
// Read the AppBloc once to get the initial state.
350+
final appBloc = context.read<AppBloc>();
351+
349352
return HeadlinesFeedBloc(
350353
headlinesRepository: context
351354
.read<DataRepository<Headline>>(),
352355
feedDecoratorService: feedDecoratorService,
353-
appBloc: context.read<AppBloc>(),
356+
appBloc: appBloc,
354357
inlineAdCacheService: inlineAdCacheService,
358+
// Prime the HeadlinesFeedBloc with the initial user
359+
// preferences. This prevents a race condition where the
360+
// feed is displayed before the bloc receives the saved
361+
// filters from the AppBloc stream.
362+
initialUserContentPreferences:
363+
appBloc.state.userContentPreferences,
355364
);
356365
},
357366
),

0 commit comments

Comments
 (0)