Skip to content

Commit ca782bb

Browse files
authored
Merge pull request #73 from flutter-news-app-full-source-code/fix-app-status-architecture
Fix app status architecture
2 parents 0d90abd + 714a1c1 commit ca782bb

File tree

8 files changed

+312
-308
lines changed

8 files changed

+312
-308
lines changed

README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,12 @@ Offer users control over their app experience:
4848
* **Feed Display:** Customize how headlines are presented.
4949
> **Your Advantage:** Deliver a premium, adaptable user experience that caters to individual needs without writing any code. 🔧
5050
51+
#### 📡 **Backend-Driven Behavior**
52+
The app is built to respond to commands from your backend API, allowing for dynamic control over the user experience:
53+
* **Maintenance Mode:** Displays a full-screen "kill switch" page when the backend signals that the service is temporarily unavailable.
54+
* **Forced Updates:** Shows a non-dismissible "Update Required" screen when a new version is mandatory, guiding users to the app store.
55+
> **Your Advantage:** The client-side logic to handle critical operational states is already implemented. Your app can gracefully manage server downtime and enforce version updates without you needing to code these complex, full-screen takeover flows. 🛠️
56+
5157
#### 📱 **Adaptive UI for All Screens**
5258
Built with `flutter_adaptive_scaffold`, the app offers responsive navigation and layouts that look great on both phones and tablets.
5359
> **Your Advantage:** Deliver a consistent and optimized UX across a wide range of devices effortlessly. ↔️

lib/app/bloc/app_bloc.dart

Lines changed: 66 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,6 @@ class AppBloc extends Bloc<AppEvent, AppState> {
5959
on<AppFontFamilyChanged>(_onFontFamilyChanged);
6060
on<AppTextScaleFactorChanged>(_onAppTextScaleFactorChanged);
6161
on<AppFontWeightChanged>(_onAppFontWeightChanged);
62-
on<AppOpened>(_onAppOpened);
6362

6463
// Listen directly to the auth state changes stream
6564
_userSubscription = _authenticationRepository.authStateChanges.listen(
@@ -412,67 +411,91 @@ class AppBloc extends Bloc<AppEvent, AppState> {
412411
return;
413412
}
414413

415-
// Avoid refetching if already loaded for the current user session, unless explicitly trying to recover from a failed state.
416-
if (state.remoteConfig != null &&
417-
state.status != AppStatus.configFetchFailed) {
414+
// For background checks, we don't want to show a loading screen.
415+
// Only for the initial fetch should we set the status to configFetching.
416+
if (!event.isBackgroundCheck) {
418417
print(
419-
'[AppBloc] AppConfig already loaded for user ${state.user?.id} and not in a failed state. Skipping fetch.',
418+
'[AppBloc] Initial config fetch. Setting status to configFetching.',
420419
);
421-
return;
420+
emit(
421+
state.copyWith(
422+
status: AppStatus.configFetching,
423+
),
424+
);
425+
} else {
426+
print('[AppBloc] Background config fetch. Proceeding silently.');
422427
}
423428

424-
print(
425-
'[AppBloc] Attempting to fetch AppConfig for user: ${state.user!.id}...',
426-
);
427-
emit(
428-
state.copyWith(
429-
status: AppStatus.configFetching,
430-
remoteConfig: null,
431-
clearAppConfig: true,
432-
),
433-
);
434-
435429
try {
436430
final remoteConfig = await _appConfigRepository.read(id: kRemoteConfigId);
437431
print(
438432
'[AppBloc] Remote Config fetched successfully. ID: ${remoteConfig.id} for user: ${state.user!.id}',
439433
);
440434

441-
// Determine the correct status based on the existing user's role.
442-
// This ensures that successfully fetching config doesn't revert auth status to 'initial'.
443-
final newStatusBasedOnUser =
444-
state.user!.appRole == AppUserRole.standardUser
445-
? AppStatus.authenticated
446-
: AppStatus.anonymous;
447-
emit(
448-
state.copyWith(
449-
remoteConfig: remoteConfig,
450-
status: newStatusBasedOnUser,
451-
),
452-
);
435+
// --- CRITICAL STATUS EVALUATION ---
436+
// For both initial and background checks, if a critical status is found,
437+
// we must update the app status immediately to lock the UI.
438+
439+
// 1. Check for Maintenance Mode. This has the highest priority.
440+
if (remoteConfig.appStatus.isUnderMaintenance) {
441+
emit(
442+
state.copyWith(
443+
status: AppStatus.underMaintenance,
444+
remoteConfig: remoteConfig,
445+
),
446+
);
447+
return;
448+
}
449+
450+
// 2. Check for a Required Update.
451+
// TODO(fulleni): Compare with actual app version from package_info_plus.
452+
if (remoteConfig.appStatus.isLatestVersionOnly) {
453+
emit(
454+
state.copyWith(
455+
status: AppStatus.updateRequired,
456+
remoteConfig: remoteConfig,
457+
),
458+
);
459+
return;
460+
}
461+
462+
// --- POST-CHECK STATE RESOLUTION ---
463+
// If no critical status was found, we resolve the final state.
464+
465+
// For an initial fetch, we transition from configFetching to the correct
466+
// authenticated/anonymous state.
467+
if (!event.isBackgroundCheck) {
468+
final finalStatus = state.user!.appRole == AppUserRole.standardUser
469+
? AppStatus.authenticated
470+
: AppStatus.anonymous;
471+
emit(state.copyWith(remoteConfig: remoteConfig, status: finalStatus));
472+
} else {
473+
// For a background check, the status is already correct (e.g., authenticated).
474+
// We just need to update the remoteConfig in the state silently.
475+
// The status does not need to change, preventing a disruptive UI rebuild.
476+
emit(state.copyWith(remoteConfig: remoteConfig));
477+
}
453478
} on HttpException catch (e) {
454479
print(
455480
'[AppBloc] Failed to fetch AppConfig (HttpException) for user ${state.user?.id}: ${e.runtimeType} - ${e.message}',
456481
);
457-
emit(
458-
state.copyWith(
459-
status: AppStatus.configFetchFailed,
460-
remoteConfig: null,
461-
clearAppConfig: true,
462-
),
463-
);
482+
// Only show a failure screen on an initial fetch.
483+
// For background checks, we fail silently to avoid disruption.
484+
if (!event.isBackgroundCheck) {
485+
emit(state.copyWith(status: AppStatus.configFetchFailed));
486+
} else {
487+
print('[AppBloc] Silent failure on background config fetch.');
488+
}
464489
} catch (e, s) {
465490
print(
466491
'[AppBloc] Unexpected error fetching AppConfig for user ${state.user?.id}: $e',
467492
);
468493
print('[AppBloc] Stacktrace: $s');
469-
emit(
470-
state.copyWith(
471-
status: AppStatus.configFetchFailed,
472-
remoteConfig: null,
473-
clearAppConfig: true,
474-
),
475-
);
494+
if (!event.isBackgroundCheck) {
495+
emit(state.copyWith(status: AppStatus.configFetchFailed));
496+
} else {
497+
print('[AppBloc] Silent failure on background config fetch.');
498+
}
476499
}
477500
}
478501

@@ -524,24 +547,4 @@ class AppBloc extends Bloc<AppEvent, AppState> {
524547
);
525548
}
526549
}
527-
528-
Future<void> _onAppOpened(AppOpened event, Emitter<AppState> emit) async {
529-
if (state.remoteConfig == null) {
530-
return;
531-
}
532-
533-
final appStatus = state.remoteConfig!.appStatus;
534-
535-
if (appStatus.isUnderMaintenance) {
536-
emit(state.copyWith(status: AppStatus.underMaintenance));
537-
return;
538-
}
539-
540-
// TODO(fulleni): Get the current app version from a package like
541-
// package_info_plus and compare it with appStatus.latestAppVersion.
542-
if (appStatus.isLatestVersionOnly) {
543-
emit(state.copyWith(status: AppStatus.updateRequired));
544-
return;
545-
}
546-
}
547550
}

lib/app/bloc/app_event.dart

Lines changed: 27 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ abstract class AppEvent extends Equatable {
77
List<Object?> get props => [];
88
}
99

10+
/// Dispatched when the authentication state changes (e.g., user logs in/out).
1011
class AppUserChanged extends AppEvent {
1112
const AppUserChanged(this.user);
1213

@@ -16,122 +17,81 @@ class AppUserChanged extends AppEvent {
1617
List<Object?> get props => [user];
1718
}
1819

19-
/// {@template app_settings_refreshed}
20-
/// Internal event to trigger reloading of settings within AppBloc.
21-
/// Added when user changes or upon explicit request.
22-
/// {@endtemplate}
20+
/// Dispatched to request a refresh of the user's application settings.
2321
class AppSettingsRefreshed extends AppEvent {
24-
/// {@macro app_settings_refreshed}
2522
const AppSettingsRefreshed();
2623
}
2724

28-
/// {@template app_logout_requested}
29-
/// Event to request user logout.
30-
/// {@endtemplate}
25+
/// Dispatched to fetch the remote application configuration.
26+
class AppConfigFetchRequested extends AppEvent {
27+
const AppConfigFetchRequested({this.isBackgroundCheck = false});
28+
29+
/// Whether this fetch is a silent background check.
30+
///
31+
/// If `true`, the BLoC will not enter a visible loading state.
32+
/// If `false` (default), it's treated as an initial fetch that shows a
33+
/// loading UI.
34+
final bool isBackgroundCheck;
35+
36+
@override
37+
List<Object> get props => [isBackgroundCheck];
38+
}
39+
40+
/// Dispatched when the user logs out.
3141
class AppLogoutRequested extends AppEvent {
32-
/// {@macro app_logout_requested}
3342
const AppLogoutRequested();
3443
}
3544

36-
/// {@template app_theme_mode_changed}
37-
/// Event to change the application's theme mode.
38-
/// {@endtemplate}
45+
/// Dispatched when the theme mode (light/dark/system) changes.
3946
class AppThemeModeChanged extends AppEvent {
40-
/// {@macro app_theme_mode_changed}
4147
const AppThemeModeChanged(this.themeMode);
42-
4348
final ThemeMode themeMode;
44-
4549
@override
46-
List<Object?> get props => [themeMode];
50+
List<Object> get props => [themeMode];
4751
}
4852

49-
/// {@template app_flex_scheme_changed}
50-
/// Event to change the application's FlexColorScheme.
51-
/// {@endtemplate}
53+
/// Dispatched when the accent color theme changes.
5254
class AppFlexSchemeChanged extends AppEvent {
53-
/// {@macro app_flex_scheme_changed}
5455
const AppFlexSchemeChanged(this.flexScheme);
55-
5656
final FlexScheme flexScheme;
57-
5857
@override
59-
List<Object?> get props => [flexScheme];
58+
List<Object> get props => [flexScheme];
6059
}
6160

62-
/// {@template app_font_family_changed}
63-
/// Event to change the application's font family.
64-
/// {@endtemplate}
61+
/// Dispatched when the font family changes.
6562
class AppFontFamilyChanged extends AppEvent {
66-
/// {@macro app_font_family_changed}
6763
const AppFontFamilyChanged(this.fontFamily);
68-
6964
final String? fontFamily;
70-
7165
@override
7266
List<Object?> get props => [fontFamily];
7367
}
7468

75-
/// {@template app_text_scale_factor_changed}
76-
/// Event to change the application's text scale factor.
77-
/// {@endtemplate}
69+
/// Dispatched when the text scale factor changes.
7870
class AppTextScaleFactorChanged extends AppEvent {
79-
/// {@macro app_text_scale_factor_changed}
8071
const AppTextScaleFactorChanged(this.appTextScaleFactor);
81-
8272
final AppTextScaleFactor appTextScaleFactor;
83-
8473
@override
85-
List<Object?> get props => [appTextScaleFactor];
74+
List<Object> get props => [appTextScaleFactor];
8675
}
8776

88-
/// {@template app_font_weight_changed}
89-
/// Event to change the application's font weight.
90-
/// {@endtemplate}
77+
/// Dispatched when the font weight changes.
9178
class AppFontWeightChanged extends AppEvent {
92-
/// {@macro app_font_weight_changed}
9379
const AppFontWeightChanged(this.fontWeight);
94-
95-
/// The new font weight to apply.
9680
final AppFontWeight fontWeight;
97-
9881
@override
9982
List<Object> get props => [fontWeight];
10083
}
10184

102-
/// {@template app_config_fetch_requested}
103-
/// Event to trigger fetching of the global AppConfig.
104-
/// {@endtemplate}
105-
class AppConfigFetchRequested extends AppEvent {
106-
/// {@macro app_config_fetch_requested}
107-
const AppConfigFetchRequested();
108-
}
109-
110-
/// {@template app_opened}
111-
/// Event triggered when the application is opened.
112-
/// Used to check for required updates or maintenance mode.
113-
/// {@endtemplate}
114-
class AppOpened extends AppEvent {
115-
/// {@macro app_opened}
116-
const AppOpened();
117-
}
118-
119-
/// {@template app_user_account_action_shown}
120-
/// Event triggered when an AccountAction has been shown to the user,
121-
/// prompting an update to their `lastAccountActionShownAt` timestamp.
122-
/// {@endtemplate}
85+
/// Dispatched when a one-time user account action has been shown.
12386
class AppUserAccountActionShown extends AppEvent {
124-
/// {@macro app_user_account_action_shown}
12587
const AppUserAccountActionShown({
12688
required this.userId,
12789
required this.feedActionType,
12890
required this.isCompleted,
12991
});
130-
13192
final String userId;
13293
final FeedActionType feedActionType;
13394
final bool isCompleted;
134-
13595
@override
13696
List<Object> get props => [userId, feedActionType, isCompleted];
13797
}

0 commit comments

Comments
 (0)