Skip to content

Commit 0d90abd

Browse files
authored
Merge pull request #71 from flutter-news-app-full-source-code/feature-maintainance-mode
Feature maintainance mode
2 parents 749a452 + 29224a5 commit 0d90abd

File tree

10 files changed

+355
-0
lines changed

10 files changed

+355
-0
lines changed
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import 'dart:async';
2+
3+
import 'package:flutter/material.dart';
4+
import 'package:flutter_bloc/flutter_bloc.dart';
5+
import 'package:flutter_news_app_mobile_client_full_source_code/app/bloc/app_bloc.dart';
6+
7+
/// {@template app_status_service}
8+
/// A service dedicated to monitoring the application's lifecycle and
9+
/// proactively triggering status checks.
10+
///
11+
/// This service ensures that the application can react to server-side
12+
/// status changes (like maintenance mode or forced updates) in real-time,
13+
/// both when the app is resumed from the background and during extended
14+
/// foreground sessions.
15+
///
16+
/// It works by:
17+
/// 1. Implementing [WidgetsBindingObserver] to listen for app lifecycle events.
18+
/// 2. Triggering a remote configuration fetch via the [AppBloc] whenever the
19+
/// app is resumed (`AppLifecycleState.resumed`).
20+
/// 3. Using a periodic [Timer] to trigger fetches at a regular interval,
21+
/// catching status changes even if the app remains in the foreground.
22+
/// {@endtemplate}
23+
class AppStatusService with WidgetsBindingObserver {
24+
/// {@macro app_status_service}
25+
///
26+
/// Requires a [BuildContext] to access the [AppBloc] and a [Duration]
27+
/// for the periodic check interval.
28+
AppStatusService({
29+
required BuildContext context,
30+
required Duration checkInterval,
31+
}) : _context = context,
32+
_checkInterval = checkInterval {
33+
// Immediately register this service as a lifecycle observer.
34+
WidgetsBinding.instance.addObserver(this);
35+
// Start the periodic checks.
36+
_startPeriodicChecks();
37+
}
38+
39+
/// The build context used to look up the AppBloc.
40+
final BuildContext _context;
41+
42+
/// The interval at which to perform periodic status checks.
43+
final Duration _checkInterval;
44+
45+
/// The timer responsible for periodic checks.
46+
Timer? _timer;
47+
48+
/// Starts the periodic timer to trigger config fetches.
49+
///
50+
/// This ensures that even if the app stays in the foreground, it will
51+
/// eventually learn about new server-side status changes.
52+
void _startPeriodicChecks() {
53+
// Cancel any existing timer to prevent duplicates.
54+
_timer?.cancel();
55+
// Create a new periodic timer.
56+
_timer = Timer.periodic(_checkInterval, (_) {
57+
print(
58+
'[AppStatusService] Periodic check triggered. Requesting AppConfig fetch.',
59+
);
60+
// Add the event to the AppBloc to fetch the latest config.
61+
_context.read<AppBloc>().add(const AppConfigFetchRequested());
62+
});
63+
}
64+
65+
/// Overridden from [WidgetsBindingObserver].
66+
///
67+
/// This method is called whenever the application's lifecycle state changes.
68+
@override
69+
void didChangeAppLifecycleState(AppLifecycleState state) {
70+
// We are only interested in the 'resumed' state.
71+
if (state == AppLifecycleState.resumed) {
72+
print(
73+
'[AppStatusService] App resumed. Requesting AppConfig fetch.',
74+
);
75+
// When the app comes to the foreground, immediately trigger a check.
76+
// This is crucial for catching maintenance mode that was enabled
77+
// while the app was in the background.
78+
_context.read<AppBloc>().add(const AppConfigFetchRequested());
79+
}
80+
}
81+
82+
/// Cleans up resources used by the service.
83+
///
84+
/// This must be called when the service is no longer needed (e.g., when
85+
/// the main app widget is disposed) to prevent memory leaks from the
86+
/// timer and the observer registration.
87+
void dispose() {
88+
print('[AppStatusService] Disposing service.');
89+
// Stop the periodic timer.
90+
_timer?.cancel();
91+
// Remove this object from the list of lifecycle observers.
92+
WidgetsBinding.instance.removeObserver(this);
93+
}
94+
}

lib/app/view/app.dart

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,12 @@ import 'package:flutter/material.dart';
66
import 'package:flutter_bloc/flutter_bloc.dart';
77
import 'package:flutter_news_app_mobile_client_full_source_code/app/bloc/app_bloc.dart';
88
import 'package:flutter_news_app_mobile_client_full_source_code/app/config/app_environment.dart';
9+
import 'package:flutter_news_app_mobile_client_full_source_code/app/services/app_status_service.dart';
910
import 'package:flutter_news_app_mobile_client_full_source_code/app/services/demo_data_migration_service.dart';
1011
import 'package:flutter_news_app_mobile_client_full_source_code/authentication/bloc/authentication_bloc.dart';
1112
import 'package:flutter_news_app_mobile_client_full_source_code/l10n/app_localizations.dart';
1213
import 'package:flutter_news_app_mobile_client_full_source_code/router/router.dart';
14+
import 'package:flutter_news_app_mobile_client_full_source_code/status/view/view.dart';
1315
import 'package:go_router/go_router.dart';
1416
import 'package:kv_storage_service/kv_storage_service.dart';
1517
import 'package:ui_kit/ui_kit.dart';
@@ -132,6 +134,8 @@ class _AppViewState extends State<_AppView> {
132134
late final GoRouter _router;
133135
// Standard notifier that GoRouter listens to.
134136
late final ValueNotifier<AppStatus> _statusNotifier;
137+
// The service responsible for automated status checks.
138+
AppStatusService? _appStatusService;
135139
// Removed Dynamic Links subscription
136140

137141
@override
@@ -140,6 +144,15 @@ class _AppViewState extends State<_AppView> {
140144
final appBloc = context.read<AppBloc>();
141145
// Initialize the notifier with the BLoC's current state
142146
_statusNotifier = ValueNotifier<AppStatus>(appBloc.state.status);
147+
148+
// Instantiate and initialize the AppStatusService.
149+
// This service will automatically trigger checks when the app is resumed
150+
// or at periodic intervals, ensuring the app status is always fresh.
151+
_appStatusService = AppStatusService(
152+
context: context,
153+
checkInterval: const Duration(minutes: 15),
154+
);
155+
143156
_router = createRouter(
144157
authStatusNotifier: _statusNotifier,
145158
authenticationRepository: widget.authenticationRepository,
@@ -159,6 +172,8 @@ class _AppViewState extends State<_AppView> {
159172
@override
160173
void dispose() {
161174
_statusNotifier.dispose();
175+
// Dispose the AppStatusService to cancel timers and remove observers.
176+
_appStatusService?.dispose();
162177
// Removed Dynamic Links subscription cancellation
163178
super.dispose();
164179
}
@@ -182,6 +197,25 @@ class _AppViewState extends State<_AppView> {
182197
// Defer l10n access until inside a MaterialApp context
183198

184199
// Handle critical RemoteConfig loading states globally
200+
// These checks have the highest priority and will lock the entire UI.
201+
//
202+
// Check for Maintenance Mode.
203+
if (state.status == AppStatus.underMaintenance) {
204+
return const MaterialApp(
205+
debugShowCheckedModeBanner: false,
206+
home: MaintenancePage(),
207+
);
208+
}
209+
210+
// Check for a Required Update.
211+
if (state.status == AppStatus.updateRequired) {
212+
return const MaterialApp(
213+
debugShowCheckedModeBanner: false,
214+
home: UpdateRequiredPage(),
215+
);
216+
}
217+
218+
// Check for Config Fetching state.
185219
if (state.status == AppStatus.configFetching) {
186220
return MaterialApp(
187221
debugShowCheckedModeBanner: false,

lib/l10n/app_localizations.dart

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1549,6 +1549,36 @@ abstract class AppLocalizations {
15491549
/// In en, this message translates to:
15501550
/// **'Bold'**
15511551
String get settingsAppearanceFontWeightBold;
1552+
1553+
/// Headline for the maintenance page
1554+
///
1555+
/// In en, this message translates to:
1556+
/// **'Under Maintenance'**
1557+
String get maintenanceHeadline;
1558+
1559+
/// Subheadline for the maintenance page
1560+
///
1561+
/// In en, this message translates to:
1562+
/// **'We are currently performing maintenance. Please check back later.'**
1563+
String get maintenanceSubheadline;
1564+
1565+
/// Headline for the force update page
1566+
///
1567+
/// In en, this message translates to:
1568+
/// **'Update Required'**
1569+
String get updateRequiredHeadline;
1570+
1571+
/// Subheadline for the force update page
1572+
///
1573+
/// In en, this message translates to:
1574+
/// **'A new version of the app is available. Please update to continue using the app.'**
1575+
String get updateRequiredSubheadline;
1576+
1577+
/// Button text for the force update page
1578+
///
1579+
/// In en, this message translates to:
1580+
/// **'Update Now'**
1581+
String get updateRequiredButton;
15521582
}
15531583

15541584
class _AppLocalizationsDelegate

lib/l10n/app_localizations_ar.dart

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -804,4 +804,21 @@ class AppLocalizationsAr extends AppLocalizations {
804804

805805
@override
806806
String get settingsAppearanceFontWeightBold => 'عريض';
807+
808+
@override
809+
String get maintenanceHeadline => 'تحت الصيانة';
810+
811+
@override
812+
String get maintenanceSubheadline =>
813+
'نقوم حاليًا بإجراء صيانة. يرجى التحقق مرة أخرى لاحقًا.';
814+
815+
@override
816+
String get updateRequiredHeadline => 'التحديث مطلوب';
817+
818+
@override
819+
String get updateRequiredSubheadline =>
820+
'يتوفر إصدار جديد من التطبيق. يرجى التحديث لمتابعة استخدام التطبيق.';
821+
822+
@override
823+
String get updateRequiredButton => 'التحديث الآن';
807824
}

lib/l10n/app_localizations_en.dart

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -804,4 +804,21 @@ class AppLocalizationsEn extends AppLocalizations {
804804

805805
@override
806806
String get settingsAppearanceFontWeightBold => 'Bold';
807+
808+
@override
809+
String get maintenanceHeadline => 'Under Maintenance';
810+
811+
@override
812+
String get maintenanceSubheadline =>
813+
'We are currently performing maintenance. Please check back later.';
814+
815+
@override
816+
String get updateRequiredHeadline => 'Update Required';
817+
818+
@override
819+
String get updateRequiredSubheadline =>
820+
'A new version of the app is available. Please update to continue using the app.';
821+
822+
@override
823+
String get updateRequiredButton => 'Update Now';
807824
}

lib/l10n/arb/app_ar.arb

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1034,5 +1034,25 @@
10341034
"settingsAppearanceFontWeightBold": "عريض",
10351035
"@settingsAppearanceFontWeightBold": {
10361036
"description": "Label for the bold font weight option"
1037+
},
1038+
"maintenanceHeadline": "تحت الصيانة",
1039+
"@maintenanceHeadline": {
1040+
"description": "Headline for the maintenance page"
1041+
},
1042+
"maintenanceSubheadline": "نقوم حاليًا بإجراء صيانة. يرجى التحقق مرة أخرى لاحقًا.",
1043+
"@maintenanceSubheadline": {
1044+
"description": "Subheadline for the maintenance page"
1045+
},
1046+
"updateRequiredHeadline": "التحديث مطلوب",
1047+
"@updateRequiredHeadline": {
1048+
"description": "Headline for the force update page"
1049+
},
1050+
"updateRequiredSubheadline": "يتوفر إصدار جديد من التطبيق. يرجى التحديث لمتابعة استخدام التطبيق.",
1051+
"@updateRequiredSubheadline": {
1052+
"description": "Subheadline for the force update page"
1053+
},
1054+
"updateRequiredButton": "التحديث الآن",
1055+
"@updateRequiredButton": {
1056+
"description": "Button text for the force update page"
10371057
}
10381058
}

lib/l10n/arb/app_en.arb

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1052,5 +1052,25 @@
10521052
"settingsAppearanceFontWeightBold": "Bold",
10531053
"@settingsAppearanceFontWeightBold": {
10541054
"description": "Label for the bold font weight option"
1055+
},
1056+
"maintenanceHeadline": "Under Maintenance",
1057+
"@maintenanceHeadline": {
1058+
"description": "Headline for the maintenance page"
1059+
},
1060+
"maintenanceSubheadline": "We are currently performing maintenance. Please check back later.",
1061+
"@maintenanceSubheadline": {
1062+
"description": "Subheadline for the maintenance page"
1063+
},
1064+
"updateRequiredHeadline": "Update Required",
1065+
"@updateRequiredHeadline": {
1066+
"description": "Headline for the force update page"
1067+
},
1068+
"updateRequiredSubheadline": "A new version of the app is available. Please update to continue using the app.",
1069+
"@updateRequiredSubheadline": {
1070+
"description": "Subheadline for the force update page"
1071+
},
1072+
"updateRequiredButton": "Update Now",
1073+
"@updateRequiredButton": {
1074+
"description": "Button text for the force update page"
10551075
}
10561076
}

lib/status/view/maintenance_page.dart

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import 'package:flutter/material.dart';
2+
import 'package:flutter_news_app_mobile_client_full_source_code/l10n/app_localizations.dart';
3+
import 'package:ui_kit/ui_kit.dart';
4+
5+
/// A page displayed to the user when the application is in maintenance mode.
6+
///
7+
/// This is a simple, static page that informs the user that the app is
8+
/// temporarily unavailable and asks them to check back later. It's designed
9+
/// to be displayed globally, blocking access to all other app features.
10+
class MaintenancePage extends StatelessWidget {
11+
/// {@macro maintenance_page}
12+
const MaintenancePage({super.key});
13+
14+
@override
15+
Widget build(BuildContext context) {
16+
final l10n = AppLocalizations.of(context);
17+
18+
// The Scaffold provides the basic Material Design visual layout structure.
19+
return Scaffold(
20+
body: Padding(
21+
// Use consistent padding from the UI kit.
22+
padding: const EdgeInsets.all(AppSpacing.lg),
23+
// The InitialStateWidget from the UI kit is reused here to provide a
24+
// consistent look and feel for full-screen informational states.
25+
child: InitialStateWidget(
26+
icon: Icons.build_circle_outlined,
27+
headline: l10n.maintenanceHeadline,
28+
subheadline: l10n.maintenanceSubheadline,
29+
),
30+
),
31+
);
32+
}
33+
}

0 commit comments

Comments
 (0)